[
  {
    "path": ".dockerignore",
    "content": ".git\n.github\n__pycache__/\n*.pyc\n*.pyo\n*.pyd\n*.swp\n*.swo\n*.tmp\nlogs/\nsaves/\n.env\n.env.*\n*.zip\n*.tar\n*.tar.gz\n*.7z\n.vscode/\n.idea/\n.DS_Store\nThumbs.db"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/submit_squid.yml",
    "content": "name: 🦑 Submit a Squid Save\ndescription: Share your evolved squid! (Must be a .zip containing uuid.txt)\nlabels: [\"squid-submission\"]\nbody:\n  - type: input\n    id: name\n    attributes:\n      label: Squid Nickname\n      placeholder: Squid\n    validations:\n      required: true\n  - type: input\n    id: save_url\n    attributes:\n      label: Save File (.zip)\n      description: Drag and drop your ZIP here, then copy the link.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/workflows/build-nuitka.yml",
    "content": "name: Build (Nuitka)\n\non:\n  workflow_dispatch:\n\njobs:\n  build:\n\n    strategy:\n      matrix:\n        os: [windows-latest, ubuntu-latest, macos-latest]\n\n    runs-on: ${{ matrix.os }}\n\n    defaults:\n      run:\n        shell: bash\n\n    steps:\n\n    - name: Checkout repo\n      uses: actions/checkout@v4\n\n    - name: Setup Python\n      uses: actions/setup-python@v5\n      with:\n        python-version: \"3.11\"\n        cache: pip\n\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install nuitka pyqt5 numpy\n\n    - name: Install macOS icon dependency\n      if: runner.os == 'macOS'\n      run: pip install imageio\n\n    # Standalone mode produces a folder of files rather than a single\n    # self-extracting binary. This avoids Defender false positives on Windows\n    # which flag onefile/packed executables as suspicious by default.\n    # Defender is also disabled during build to prevent it quarantining\n    # Nuitka's intermediate files mid-compile.\n    - name: Disable Windows Defender during build\n      if: runner.os == 'Windows'\n      shell: pwsh\n      run: |\n        Set-MpPreference -DisableRealtimeMonitoring $true\n        Add-MpPreference -ExclusionPath \"${{ github.workspace }}\"\n\n    - name: Build executable\n      run: |\n        if [[ \"${{ runner.os }}\" == \"Windows\" ]]; then\n          ICON_FLAG=\"--windows-icon-from-ico=images/icons/squidblack.ico\"\n        elif [[ \"${{ runner.os }}\" == \"Linux\" ]]; then\n          ICON_FLAG=\"--linux-icon=images/icons/squidblack.png\"\n        else\n          ICON_FLAG=\"--macos-app-icon=images/icons/squidblack.png\"\n        fi\n\n        python -m nuitka \\\n          --mode=standalone \\\n          --windows-console-mode=force \\\n          --assume-yes-for-downloads \\\n          --enable-plugin=pyqt5 \\\n          --disable-plugin=options-nanny \\\n          --output-dir=build \\\n          --output-filename=Dosidicus \\\n          --include-package=src \\\n          --include-package=plugins \\\n          --include-module=uuid \\\n          --include-module=src.brain_designer \\\n          --include-module=src.brain_designer_launcher \\\n          --include-module=src.brain_state_bridge \\\n          --include-module=src.designer_canvas \\\n          --include-module=src.designer_canvas_utils \\\n          --include-module=src.designer_constants \\\n          --include-module=src.designer_core \\\n          --include-module=src.designer_dialogs \\\n          --include-module=src.designer_logging \\\n          --include-module=src.designer_network_generator \\\n          --include-module=src.designer_outputs_panel \\\n          --include-module=src.designer_panels \\\n          --include-module=src.designer_sensor_discovery \\\n          --include-module=src.designer_templates \\\n          --include-module=src.designer_window \\\n          $ICON_FLAG \\\n          main.py\n\n    - name: Show build output\n      run: find build -maxdepth 2 | sort\n\n    - name: Create release directory\n      run: mkdir release\n\n    - name: Copy repository structure\n      run: |\n        for item in *; do\n          case \"$item\" in\n            release|dist|build|__pycache__|*.spec) ;;\n            *) cp -r \"$item\" release/ 2>/dev/null || true ;;\n          esac\n        done\n\n    - name: Remove python entrypoint\n      run: rm -f release/main.py || true\n\n    - name: Insert compiled executable\n      run: |\n        if [[ \"${{ runner.os }}\" == \"macOS\" ]]; then\n          APP=$(find build -maxdepth 1 -name \"*.app\" | head -1)\n          if [ -n \"$APP\" ]; then\n            cp -r \"$APP\" release/Dosidicus.app\n          else\n            cp -r build/main.dist/. release/\n          fi\n        else\n          cp -r build/main.dist/. release/\n        fi\n\n    # Ad-hoc signing suppresses some Gatekeeper warnings on macOS.\n    # This is free but NOT a substitute for full notarization ($99/yr Apple\n    # Developer account). Users on macOS will still need to right-click ->\n    # Open -> Open Anyway the first time. Document this in your README.\n    - name: Ad-hoc sign for macOS\n      if: runner.os == 'macOS'\n      run: |\n        APP=$(find release -maxdepth 1 -name \"*.app\" | head -1)\n        if [ -n \"$APP\" ]; then\n          codesign --deep --force --sign - \"$APP\"\n        else\n          codesign --deep --force --sign - release/Dosidicus\n        fi\n\n    # ─── Smoke tests ────────────────────────────────────────────────────────────\n    # Launch the binary, capture stdout, and look for the startup print from\n    # MainWindow.__init__: \"📄 Applied language from config:\"\n    # This fires after Qt initialises and all imports resolve, confirming the\n    # build is not silently broken.\n\n    - name: Smoke test (Linux)\n      if: runner.os == 'Linux'\n      run: |\n        QT_QPA_PLATFORM=offscreen ./release/Dosidicus > smoke.log 2>&1 &\n        PID=$!\n        echo \"Started PID $PID\"\n\n        # Poll for the expected startup line, up to 30s\n        for i in $(seq 1 30); do\n          sleep 1\n          if grep -q \"Applied language from config\" smoke.log 2>/dev/null; then\n            echo \"✅ Startup confirmed at ${i}s\"\n            echo \"--- stdout/stderr ---\"\n            cat smoke.log\n            kill $PID 2>/dev/null || true\n            exit 0\n          fi\n          # Bail early if the process already died\n          if ! kill -0 $PID 2>/dev/null; then\n            echo \"❌ Process exited before printing startup line\"\n            echo \"--- stdout/stderr ---\"\n            cat smoke.log\n            exit 1\n          fi\n        done\n\n        echo \"❌ Startup line not seen after 30s\"\n        echo \"--- stdout/stderr ---\"\n        cat smoke.log\n        kill $PID 2>/dev/null || true\n        exit 1\n\n    - name: Smoke test (macOS)\n      if: runner.os == 'macOS'\n      run: |\n        APP=$(find release -maxdepth 1 -name \"*.app\" | head -1)\n        if [ -n \"$APP\" ]; then\n          BIN=$(find \"$APP\" -type f -perm +111 -name \"Dosidicus\" | head -1)\n        else\n          BIN=release/Dosidicus\n        fi\n\n        \"$BIN\" > smoke.log 2>&1 &\n        PID=$!\n        echo \"Started PID $PID\"\n\n        for i in $(seq 1 30); do\n          sleep 1\n          if grep -q \"Applied language from config\" smoke.log 2>/dev/null; then\n            echo \"✅ Startup confirmed at ${i}s\"\n            echo \"--- stdout/stderr ---\"\n            cat smoke.log\n            kill $PID 2>/dev/null || true\n            exit 0\n          fi\n          if ! kill -0 $PID 2>/dev/null; then\n            echo \"❌ Process exited before printing startup line\"\n            echo \"--- stdout/stderr ---\"\n            cat smoke.log\n            exit 1\n          fi\n        done\n\n        echo \"❌ Startup line not seen after 30s\"\n        echo \"--- stdout/stderr ---\"\n        cat smoke.log\n        kill $PID 2>/dev/null || true\n        exit 1\n    # ────────────────────────────────────────────────────────────────────────────\n\n    # Zip may strip execute permissions - restore them before archiving\n    - name: Mark Linux binary executable\n      if: runner.os == 'Linux'\n      run: chmod +x release/Dosidicus\n\n    - name: Create zip\n      run: |\n        OS_NAME=$(echo \"${{ runner.os }}\" | tr '[:upper:]' '[:lower:]')\n        python -c \"import shutil; shutil.make_archive('Dosidicus${{ github.ref_name }}-${OS_NAME}', 'zip', 'release')\"\n\n    - name: Upload artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: nuitka-${{ runner.os }}\n        path: Dosidicus${{ github.ref_name }}-*.zip\n"
  },
  {
    "path": ".github/workflows/process_submission.yml",
    "content": "name: Squid Gallery Bot\non:\n  issues:\n    types: [opened]\n\njobs:\n  add-to-gallery:\n    if: contains(github.event.issue.labels.*.name, 'squid-submission')\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      issues: write\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v4\n\n      - name: Process Squid Save\n        env:\n          ISSUE_BODY: ${{ github.event.issue.body }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          ISSUE_NUMBER: ${{ github.event.issue.number }}\n        run: |\n          # 1. Extract URL and Nickname\n          FILE_URL=$(echo \"$ISSUE_BODY\" | grep -o 'https://github.com/[^)]*' | head -n 1)\n          NICKNAME=$(echo \"$ISSUE_BODY\" | grep -A 1 \"Squid Nickname\" | tail -n 1 | xargs | tr -d ' ')\n          \n          mkdir -p gallery\n          curl -L $FILE_URL -o \"temp_squid.zip\"\n\n          # 2. Verify ZIP and extract UUID\n          if unzip -l temp_squid.zip | grep -q \"uuid.txt\"; then\n            UUID=$(unzip -p temp_squid.zip uuid.txt | head -n 1 | xargs)\n            FINAL_NAME=\"${NICKNAME}_${UUID}.zip\"\n            \n            mv temp_squid.zip \"gallery/${FINAL_NAME}\"\n            \n            # 3. Commit to repo\n            git config user.name \"SquidBot\"\n            git config user.email \"bot@github.com\"\n            git add gallery/\n            git commit -m \"New Squid verified: ${NICKNAME} (${UUID})\"\n            git push\n            \n            gh issue comment $ISSUE_NUMBER --body \"✅ **Squid Verified!** $NICKNAME has been added to the gallery. UUID: \\`$UUID\\`\"\n            gh issue close $ISSUE_NUMBER\n          else\n            gh issue comment $ISSUE_NUMBER --body \"❌ **Validation Failed:** This ZIP does not contain a \\`uuid.txt\\`. Please check your save file and try again.\"\n            exit 1\n          fi\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contributing to Dosidicus 🦑\nDosidicus isn't just a repository; it’s an open-ended experiment in transparent artificial life. We are building a world where cognition isn't a black box, but a visible, hackable structure. Whether you are a neuro-enthusiast, a Python wizard, or a digital explorer, there is a place for you in the sandbox.\n\n### 🧠 1. Evolve the Core (src/)\nThe STRINg engine is the beating heart of the project. We’ve avoided heavy frameworks like TensorFlow to keep things \"bare metal\" and interpretable.\n\nThe Challenge: Optimize neural algorithms, refine Hebbian plasticity logic, or enhance the real-time UI.\n\nGoal: Make the simulation faster and the neural dynamics more expressive.\n\n### 🧩 2. Architect New Realities (plugins/)\nDosidicus is designed to be modular. Our plugin system allows you to inject new logic without touching the core engine.\n\nThe Challenge: Create a new plugin folder with a plugin.txt descriptor.\n\nIdeas: Environmental stressors, advanced \"Achievement\" triggers, or even a Twitch-integrated \"Chat-controlled Evolution.\"\n\n### 🧬 3. Map the Connectome (custom_brains/)\nHelp us build a library of diverse neural starting points. By contributing JSON templates to custom_brains/, you define the \"DNA\" that users experiment with.\n\nThe Challenge: Design a unique neural layout—perhaps a highly recurrent \"memory loop\" or a sensory-heavy \"predator\" build.\n\n### 📂 4. Share Your Specimens (example_squids/)\nIn this sandbox, Structure + Experience = Behavior. No two squids are identical.\n\nThe Challenge: Export your .json save files and share them. Did your squid develop a strange obsession with a specific corner of the map? Did it grow a massive neural cluster after a specific training session?\n\nAction: Submit your most interesting \"trained\" squids as examples for others to study.\n\n### 🌍 5. Bridge the Language Gap (translations/)\nWe want \"visible minds\" to be accessible to everyone.\n\nThe Challenge: Add a new language file (e.g., fr.py, jp.py) to the translations/ directory. Help us ensure that concepts like Neurogenesis and Activation Patterns translate clearly across cultures.\n\n### 🛠️ 6. Documentation & Educational Outreach\nIf a newcomer can’t understand how to \"read\" a squid’s mind, the transparency is lost.\n\nThe Challenge: Improve docstrings, write \"Field Guides\" for new users, or record a video of a specific behavioral emergence.\n\nGoal: Turn complex neuroscience concepts into playable tutorials.\n\n### 🚀 How to Get Started\nCheck the \"Ahead\" Forks: See what the community is already building in the Network Graph.\n\nOpen an Issue: Have a wild idea for a new neuron type? Let's discuss it first!\n\nSubmit a PR: We welcome all PRs, from single-typo fixes to massive engine overhauls.\n\nEvery commit is a mutation. Every contribution helps the system grow.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\nFROM python:3.11-slim AS base\nENV PYTHONDONTWRITEBYTECODE=1 \\\n    PYTHONUNBUFFERED=1 \\\n    PIP_DISABLE_PIP_VERSION_CHECK=1\nWORKDIR /app\n\n# Install core dependencies needed by ALL targets\nRUN python -m pip install --no-cache-dir numpy>=1.21\n\n# ------------------------------\n# Headless trainer target\n# ------------------------------\nFROM base AS headless\n# Only copy what is needed for headless training\nCOPY headless/ ./headless/\nCOPY custom_brains/ ./custom_brains/\n\nENTRYPOINT [\"python\", \"headless/headless_trainer.py\"]\nCMD [\"--ticks\", \"10000\", \"--output\", \"trained_brain.json\"]\n\n# ------------------------------\n# GUI target (requires X11)\n# ------------------------------\nFROM base AS gui\n# Install system libraries for GUI support\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    libgl1 \\\n    libgl1-mesa-dri \\\n    libglib2.0-0 \\\n    libx11-6 \\\n    libx11-xcb1 \\\n    libxkbcommon-x11-0 \\\n    libxkbcommon0 \\\n    libxext6 \\\n    libxrender1 \\\n    libxrandr2 \\\n    libxfixes3 \\\n    libxdamage1 \\\n    libxi6 \\\n    libxcomposite1 \\\n    libxcursor1 \\\n    libxss1 \\\n    libxtst6 \\\n    libfontconfig1 \\\n    libfreetype6 \\\n    libnss3 \\\n    libdbus-1-3 \\\n    libxcb1 \\\n    libxcb-icccm4 \\\n    libxcb-image0 \\\n    libxcb-keysyms1 \\\n    libxcb-randr0 \\\n    libxcb-render0 \\\n    libxcb-render-util0 \\\n    libxcb-shape0 \\\n    libxcb-shm0 \\\n    libxcb-sync1 \\\n    libxcb-xfixes0 \\\n    libxcb-xinerama0 \\\n    libxcb-xkb1 \\\n    libxcb-cursor0 \\\n    libxshmfence1 \\\n    libwayland-client0 \\\n    libwayland-cursor0 \\\n    libwayland-egl1 \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Install GUI-specific Python dependencies\nRUN python -m pip install --no-cache-dir PyQt5>=5.15\n\n# Copy the entire project for the GUI\nCOPY . .\n\nENV QT_X11_NO_MITSHM=1\nENTRYPOINT [\"python\", \"main.py\"]\n"
  },
  {
    "path": "Docs/Cognitive Sandbox Manifesto - Artificial Life and Transparent Neural Systems.md",
    "content": "\n <img src=\"https://github.com/user-attachments/assets/309ea7b0-a7c1-49f3-8f44-1b541734954d\" width=\"220\">\n\n\n\n\n## 1. Why This Exists\n\nMost artificial intelligence today is powerful, fast, and opaque: Large neural networks are trained on vast datasets producing remarkable outputs.\nHowever the learning process itself is hidden inside millions or billions of parameters.\n\n* You cannot watch a mind form - you can only inspect the result.\n\nDosidicus was created from a different question:\n\n* What if cognition could be SEEN emerging?\n\nNot as a metaphor or as a visualisation layered on top, but at the level of individual neurons.\n\n## 2. The Cognitive Sandbox\n\nDosidicus is a cognitive sandbox.\n\nA sandbox is not a finished product.\nIt is a contained environment where systems interact, evolve, and reveal behaviour.\n\nEach squid:\n\n* Is born with a randomly wired neural architecture\n* Starts with 8 neurons\n* Learns through Hebbian dynamics\n* Grows new structure through neurogenesis\n* Forms memories\n* Develops behavioural tendencies\n\nNo two squids are identical.\n\nEvery save file becomes a record of accumulated experience.\n\nThe sandbox does not prescribe intelligence, it allows structure to form through interaction.\n\n## 3. Transparency as a Design Principle\n\nMost AI systems are black boxes. Dosidicus rejects opacity as default. Every neuron is:\n\n* Visible\n* Inspectable\n* Directly stimulatable\n* Modifiable\n\nYou can see activation values and observe connection strengths change.\nYou can influence structural growth.\n\nThis is not industrial-scale AI, it is intentionally small-scale and transparent.\n\nThe goal is not performance but **visibility**.\n\nTransparency transforms AI from a service into an object of study.\n\n## 4. Artificial Life\n\nDosidicus is not an attempt at AGI. It is a constrained, evolving, micro-organism simulation which exists in a narrow world:\n\n* Hunger\n* Interaction\n* Stimulus\n* Memory\n* Simple drives\n\nYet within these constraints, patterns emerge.\n\nArtificial life is not about scale.\nIt is about process.\n\nA small system that adapts and accumulates experience over time can evoke something that feels alive — even when it is mechanistic.\nThis boundary between mechanism and perceived life is intentional.\n\n## 5. Attachment to Visible Minds\n\nHumans bond with simple systems.\n\nWe bond with:\n\n* Tamagotchi\n* Virtual pets\n* Pixel creatures\n* Even malfunctioning robots\n\nDosidicus introduces a twist:\n\nYou can see the internal cause of behaviour.\n\nIf your squid develops an aversion to something,\nyou can trace the neural path that led there.\n\nThis changes the psychology of attachment.\n\nInstead of caring for a scripted creature you are shaping a developing structure.\n\n* Responsibility becomes more concrete.\n* Every interaction leaves a trace.\n\n## 6. Retro as Constraint / computational meta-art\n\nDosidicus uses minimal animation.\nTwo tentacle frames.\nHand-drawn sprites.\nDeliberately simple presentation.\n\nIt is a design constraint.\n\nComplex graphics distract from internal complexity.\n\nThe squid is art.\n\nThe brain is system.\n\nTogether, they create **computational meta-art**:\na drawn creature whose behaviour emerges from real learning dynamics.\n\n <img src=\"https://github.com/user-attachments/assets/02119926-47f7-4bfb-96b9-457d470064e4\" width=\"800\">\n\n## 7. STRINg: The Simulation Engine\n\nUnder the hood runs a [custom engine](https://github.com/ViciousSquid/Dosidicus/wiki/Engine-overview):\n\n[STRINg](https://github.com/ViciousSquid/Dosidicus/wiki/Engine-overview)\n`S`imulated `T`amagotchi `R`eactions via `I`nferencing and `N`eurogenesis\n\nBuilt from scratch using NumPy.\n\n* No TensorFlow.\n* No PyTorch.\n\n#### Core properties:\n\n* Explicit neuron-level simulation\n* Hebbian plasticity\n* Structural growth (neurogenesis)\n* Dual memory system (short-term and long-term)\n* Headless training capability\n* Plugin extensibility\n\nSTRINg is optimised for interpretability not scale.\n\nIt treats neural networks not as static architectures, but as evolving structures.\n\n## 8. Educational Intent\n\nDosidicus functions as:\n\n* A learning tool for understanding neural dynamics\n* A demonstration of Hebbian learning\n* A sandbox for artificial life experimentation\n* A way to visualise structural plasticity\n* A gateway into neuroscience concepts through play\n\nInstead of teaching neural networks as equations alone, it allows learners to raise one.\n\nUnderstanding becomes experiential.\n\n## 9. Individuality Through Random Birth\n\nEvery squid begins differently.\n* Initial wiring is randomised.\n* Early experiences alter trajectory.\n* Small differences amplify over time.\n\nThis models a core biological principle:\n\n_**Structure + experience = behaviour.**_\n\nThe system does not claim biological realism.\n\nIt demonstrates structural sensitivity.\n\nNo two digital lives are identical.\n\n## 10. What This Is Not\n\n* Not Skynet.\n* Not a productivity AI.\n* Not a chatbot.\n* Not a pretrained monolith.\n* Not an optimised inference engine.\n\nIt is a visible micro-mind.\n\nContained.\nHackable.\nInspectable.\nEvolving.\n"
  },
  {
    "path": "Docs/README.md",
    "content": "\n_\"What if a Tamagotchi had a neural network and could learn stuff?\"_\n— [Gigazine](https://gigazine.net/gsc_news/en/20250505-dosidicus-electronicae/)\n\n<p align=\"left\">\n  <img src=\"https://img.shields.io/badge/AI-Neural_Network-9C27B0?style=flat&logo=mindmeister&logoColor=white\" height=\"20\" alt=\"AI\">\n  <img src=\"https://img.shields.io/badge/License-GPL_v2-blue.svg?style=flat\" height=\"20\" alt=\"GPL-2.0\">\n  <img src=\"https://img.shields.io/badge/Translations-7-228B22?style=flat&logo=google-translate&logoColor=white&labelColor=333333\" height=\"20\" alt=\"Translations\">\n    <a href=\"https://buymeacoffee.com/vicioussquid\"><img src=\"https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black\" height=\"20\" alt=\"Buy Me A Coffee\"></a>\n</p>\n\n# _Dosidicus electronicus_\n\n🦑 _A transparent neural sandbox disguised as a digital pet squid with a neural network you can **see thinking**_\n\nMicro neural engine for small autonomous agents that learn via Hebbian dynamics and grow new structure\n\n- Part **educational neuro tool**, part **sim game**, part **fever dream**\n- [Build-your-own neural network ](https://github.com/ViciousSquid/Dosidicus/wiki/Brain-Designer) - learn neuroscience by raising a squid that **might develop irrational fears**\n- Custom [simulation engine](https://github.com/ViciousSquid/Dosidicus/wiki/Engine-overview) using Numpy - **No Tensorflow or PyTorch**\n- Most AI is a **black box**; Dosidicus is **transparent** - every neuron is visible, stimulatable, understandable.\n- Starts with 8 neurons — grows via **neurogenesis** and rewires using **Hebbian learning**.\n- Includes `achievements` with **50** to collect!\n\n <img src=\"https://github.com/user-attachments/assets/23e98046-23a6-44a1-b4c8-a57abfff5501\" width=\"180\">\n \n- -----------------------------\n\n---\n\n### Getting Started\n\n| Page | Description |\n|------|-------------|\n| [Home](getting-started/Home.md) | Project overview |\n| [Care Guide](getting-started/Care-Guide.md) | How to look after your squid |\n| [Example Squids](getting-started/Example-Squids.md) | Pre-made squid configurations |\n| [Changelog](getting-started/Changelog.md) | Version history |\n\n---\n\n### STRINg Simulation Engine\n\n| Page | Description |\n|------|-------------|\n| [Engine Overview](engine/Engine-Overview.md) | High-level architecture |\n| [Decision Engine](engine/Decision-Engine.md) | How the squid makes choices |\n| [Data Flow Summary](engine/Data-Flow-Summary.md) | How data moves through the system |\n| [Plugin System](engine/Plugin-System.md) | Extending Dosidicus with plugins |\n| [Plugin Hooks](engine/Plugin-Hooks.md) | Available hook points |\n| [Save File Format](engine/Save-File-Format.md) | Structure of `.squid` save files |\n| [config.ini](engine/config.ini.md) | Configuration reference |\n| [Multiplayer](engine/Multiplayer.md) | Multiplayer support |\n\n---\n\n### Neural Network\n\n| Page | Description |\n|------|-------------|\n| [Technical Overview](neural-network/Technical-Overview.md) | Neural network architecture |\n| [Hebbian Learning](neural-network/Hebbian-Learning.md) | Weight update algorithm |\n| [STDP](neural-network/STDP.md) | Spike-Timing-Dependent Plasticity |\n| [Neurogenesis](neural-network/Neurogenesis.md) | Creating new neurons at runtime |\n| [Experience Buffer](neural-network/Experience-Buffer.md) | Short/long-term memory experiences |\n| [Vision System](neural-network/Vision-System.md) | Food detection via vision cone |\n| [Personality](neural-network/Personality.md) | The 7 personality types |\n| [AI Accelerator Support](neural-network/AI-Accelerator-Support.md) | Hardware acceleration options |\n\n---\n\n### Brain Tool\n\n| Page | Description |\n|------|-------------|\n| [Brain Designer](brain-tool/Brain-Designer.md) | GUI for designing custom brains |\n| [Brain Trainer (Headless)](brain-tool/Brain-Trainer-Headless.md) | CLI training without a GUI |\n| [Network Tab](brain-tool/Network-Tab.md) | Visualising the neuron network |\n| [Learning Tab](brain-tool/Learning-Tab.md) | Monitoring learning in real time |\n| [Memory Tab](brain-tool/Memory-Tab.md) | Inspecting memory contents |\n| [Decisions Tab](brain-tool/Decisions-Tab.md) | Watching decision-making live |\n| [Personality Tab](brain-tool/Personality-Tab.md) | Adjusting personality traits |\n| [Neuron Laboratory](brain-tool/Neuron-Laboratory.md) | Experimenting with individual neurons |\n\n---\n\n### Source Reference\n\nDocumentation for individual source files:\n\n| File | File |\n|------|------|\n| [main.py](source-reference/main.py.md) | [brain_tool.py](source-reference/brain_tool.py.md) |\n| [squid.py](source-reference/squid.py.md) | [brain_widget.py](source-reference/brain_widget.py.md) |\n| [tamagotchi_logic.py](source-reference/tamagotchi_logic.py.md) | [brain_worker.py](source-reference/brain_worker.py.md) |\n| [memory_manager.py](source-reference/memory_manager.py.md) | [brain_render_worker.py](source-reference/brain_render_worker.py.md) |\n| [vision_worker.py](source-reference/vision_worker.py.md) | [brain_neuron_hooks.py](source-reference/brain_neuron_hooks.py.md) |\n| [custom_brain_loader.py](source-reference/custom_brain_loader.py.md) | [brain_neuron_outputs.py](source-reference/brain_neuron_outputs.py.md) |\n| [designer_window.py](source-reference/designer_window.py.md) | [neurogenesis_show.py](source-reference/neurogenesis_show.py.md) |\n\n---\n\n### Extras\n\n| Page | Description |\n|------|-------------|\n| [Achievements](extras/Achievements.md) | Unlockable achievements |\n| [Easter Eggs](extras/Easter-Eggs.md) | Hidden features |\n| [Decoration Window](extras/Decoration-Window.md) | Customising the environment |\n| [SaveViewer](extras/SaveViewer.md) | Browser-based save file inspector |\n| [UUID](extras/UUID.md) | Squid identity system |\n"
  },
  {
    "path": "Docs/brain-tool/Brain-Designer.md",
    "content": "### Design your own (GPL!) squid brain!\n\nAdd new **neurons** / **layers** - see how they affect the squid's ability to process his environment\n \n<img src=\"https://github.com/user-attachments/assets/a488556a-1326-4a07-a0f0-b9c284d94af3\" width=\"70\">\n\nClick the brain button in the bottom right of the \n [network tab](../brain-tool/Network-Tab.md) to swich into Designer mode\n\n<img src=\"https://github.com/user-attachments/assets/5b2d88f2-53c0-4af8-ab81-7e501cfcfbf1\" width=\"800\">\n\nFROM LEFT TO RIGHT:\n\n1. -- Fast-Generate a random network with no prompt\n2. -- Generate a random network with controls\n3. -- Add a new Custom/Input Neuron\n4. -- Export the brain to a .json file\n5. -- Clear all connections\n6. -- Quick-generate a network from template\n\n--------------------------------------\n\nIf designer mode is launched from within the simulation (default behaviour):\n\n* The running brain will be automatically imported into the designer. \n* The purple '**Switch to Game**' button passes the brain from the designer to the running brain tool\n\n```\nSome buttons will not be available if the designer is launched standalone (main.py -designer)\n```\n\n---------------------------------\n#### CONTROLS:\n\n* <img src=\"https://github.com/user-attachments/assets/0f9eb326-eee9-4ada-a1a9-2d70d4770765\" width=\"100\"> to add a neuron\n\n\n* Mouse wheel on the canvas to zoom in/out, hold right mouse button to drag\n* Click on a neuron and hold to drag a connection line - release on another neuron\n* Select a link and use the mouse wheel to increase/decrease the connection weight (range: -1.00 to +1.00)\n* negative weights are inhibitory (red), positive weights are excitory (green). \n* Press **DEL** to delete a connection link\n* **SPACE** to change the direction of a connecting line (indicated with arrows)\n* Stronger weights are indicated with thicker lines.\n* **Ctrl-click-and drag** to reposition any neuron.\n* Use the Layers tab to create more complicated brains with additional layers\n* The Sensors tab shows available INPUT neurons\n* The Output tab lists Output bindings for bridging neuron activations to game behaviours:\n\nChoose a **source neuron**, a **trigger**, and **behaviour** for that trigger\n\nEXAMPLES:\n```\nIF .. can_see_food .. THEN .. change colour\nIF .. happiness = <30 .. THEN .. pick up a rock\nIF .. anxiety = >60 .. THEN .. seek plant\n```\n\nThese can be combined to create reactive behaviours (simulated _biological drives/**urges**_)\n\n<img src=\"https://github.com/user-attachments/assets/08f3ab4a-5790-4df6-b5dd-59547039cfe1\" width=\"300\">\n\n\n------------------------\n\n### `Generate sparse network`\n\nThe 'generate sparse network' button creates biologically-inspired neural networks using the core 8 neurons. Each generated brain is unique and randomised.\n\n\n<img src=\"https://github.com/user-attachments/assets/d15321ca-cc53-44f8-bc45-c4f787995f38\" width=\"300\">\n"
  },
  {
    "path": "Docs/brain-tool/Brain-Trainer-Headless.md",
    "content": "A headless training tool is included in the `headless` folder. This can be used to train brains in a time-accelerated environment \n\n* **Headless Operation**: No GUI required, runs purely on CPU\n* **Accelerated Time**: Runs 25,000-35,000+ ticks per second (vs ~1 tick/second in real-time)\n* **Neurogenesis**: Automatic creation of new neurons based on stress/novelty/reward triggers\n* **Hebbian Learning**: Continuous weight updates based on co-activation\n* **Training Scenarios**: Predefined scenarios for different training goals\n* **Export Trained Brains**: Save trained brains back to JSON for use in the main game\n\nDocumentation for this tool can be found here: https://github.com/ViciousSquid/Dosidicus/blob/v2.6.1.0__b1218_LatestVersion/headless/README_headless_trainer.md\n\n#### WORK IN PROGRESS, BUGGY, WORK IN PROGRESS, BUGGY, WORK IN PROGRESS, BUGGY\n\n\n#### `headless_launcher.html` is a user-friendly launcher for this tool: drag and drop or Browse for a brain and then select how to train it and for how long\n\n<img src=\"https://github.com/user-attachments/assets/3fdc4814-85ca-434f-a7c8-ef311131377b\" width=\"800\">"
  },
  {
    "path": "Docs/brain-tool/Decisions-Tab.md",
    "content": "\n![image](https://github.com/user-attachments/assets/a6b74e77-98db-4e97-b03e-a067b0814c77)\n\n <p> The <strong>Decisions Tab</strong> provides a fascinating, step-by-step visualization of your squid's thought process. It breaks down how the squid goes from assessing his environment and internal needs to making a final, actionable decision. This view is updated every time the squid makes a new choice. </p> \n\nDecisions reflected here are made by the [Decision Engine](../engine/Decision-Engine.md) which is driven by the [Neural Network](../neural-network/Technical-Overview.md)\n\n\n\n<h4>Interface Elements</h4> <ul> <li> <strong>Thought Process Path:</strong> A vertical flow-chart that visualizes the decision-making pipeline. <ol> <li><strong>📡 Sensing the World:</strong> This step shows the raw inputs the squid is currently processing. This includes his internal stats (hunger, happiness, etc.) and any objects he can see in his environment (food, poop, etc.).</li> \n<li><strong>⚖️ Calculating Base Urges:</strong> Based on the inputs, the tool calculates the initial \"weight\" or \"urge\" for each possible action. The action with the highest initial score is listed as the strongest urge.</li> <li><strong>🎭 Applying Personality & Memories:</strong> The base urges are then modified by the squid's personality and recent memories. For example, a \"Timid\" squid might have his urge to \"explore\" reduced, while a \"Greedy\" squid will have his urge to \"eat\" increased. This step shows how much each score was adjusted.</li> <li><strong>✅ Making the Final Decision:</strong> This step shows the final, adjusted scores for all possible actions. The action with the highest final score is the one the squid chooses.</li> </ol> </li> <li> <strong>Final Action Bar:</strong> A fixed bar at the bottom of the tab that prominently displays the squid's final chosen action (e.g., \"Eat\", \"Sleep\", \"Explore\") and his calculated confidence in that decision. </li> </ul> <hr>"
  },
  {
    "path": "Docs/brain-tool/Learning-Tab.md",
    "content": "<img src=\"https://github.com/user-attachments/assets/0c76149e-0975-4942-a10c-ca686f1d6f76\" width=\"700\">\n\n#### `Hebbian learning` is a principle grounded in the functioning of biological neural networks\n\n\n<p> The <strong>Learning Tab</strong> is your window into understanding how your squid learns. It focuses on the Hebbian learning principle: \"neurons that fire together, wire together.\" \n\nThis tab visualizes **neuron pairs** that were selected for learning (happens every 30 seconds by default)\n The connection strength between them (weight) was either **strengthened** or **weakened**, meaning they became more or less associated with each other.\n\n For example: if Hunger and Satisfaction both happened to be firing at the same time (perhaps the squid just had a positive eating experience) these two neurons would have their connection strengthened when the counter reaches zero\n\nOver time the same useful connections will keep strengthening and strong pathways will develop. This is how the squid 'learns' favourable behaviours\n\n\n----------------------------\n\n#### Further Reading:\nExternal links\n\n* https://medium.com/@reutdayan1/hebbian-learning-biologically-plausible-alternative-to-backpropagation-6ee0a24deb00\n* https://informatics.ed.ac.uk/sites/default/files/2024-03/Qiuye%20Zhang%20Lovelace%20Colloquium%20Poster.pdf\n* https://en.wikipedia.org/wiki/Hebbian_theory\n* https://www.youtube.com/watch?v=TvTQQO5yTa4\n\n\n"
  },
  {
    "path": "Docs/brain-tool/Memory-Tab.md",
    "content": "\n\n<img src=\"https://github.com/user-attachments/assets/3123aea8-0ba8-41a4-abba-7a559e65fac2\" width=\"600\">\n\n<p> The <strong>Memory Tab</strong> allows you to explore the memories your squid has formed. It separates memories into short-term (recent events) and long-term (consolidated, important events), giving you insight into what has recently affected your squid and what it has learned to remember. </p> <h4>Interface Elements</h4> <ul> <li> <strong>Memory Sub-Tabs:</strong> <ul> <li><strong>🧠 Short-Term:</strong> Displays recent memories that are still being processed. These are temporary and will decay over time unless they are significant.</li> <li><strong>📚 Long-Term:</strong> Displays memories that have been deemed important enough to be stored permanently. These memories have a lasting impact on the squid's behavior.</li> <li><strong>📊 Overview:</strong> Shows high-level statistics about the memory system, including the total count of short-term and long-term memories and a breakdown of memories by category (e.g., food, interaction, mental\\_state).</li> </ul> </li> <li> <strong>Memory Cards:</strong> Both STM and LTM are displayed as a series of cards. Each card is color-coded to indicate its emotional valence: <ul> <li><strong>Pastel Green:</strong> A positive memory (e.g., eating tasty food).</li> <li><strong>Pastel Red:</strong> A negative memory (e.g., being startled).</li> <li><strong>Pastel Yellow:</strong> A neutral memory.</li> </ul> </li> <li><strong>Card Content:</strong> Each card displays the memory's category, its formatted content, and the time it was created. Important memories are marked with a \"⭐ Important\" indicator. Clicking on a short-term memory card can increase its importance, making it more likely to be transferred to long-term memory.</li> </ul> <hr>"
  },
  {
    "path": "Docs/brain-tool/Network-Tab.md",
    "content": "The <strong>Network Tab</strong> provides a live, visual representation of the overall structure and health of the neural network in real-time. \n\n<img src=\"https://github.com/user-attachments/assets/0e5ecc7e-9b15-47d7-a454-276769635b58\" width=\"600\">\n\n\n- Each neural network is unique and randomly generated (with rules) when the squid is hatched\n- Red lines represent Excitory (positive) connections between neurons\n- Green lines represent Inhibitory (negative) connections\n- The thicker the line, the stronger the connection\n\n\n\n  <h4>Interface Elements</h4> <ul> <li> <strong>Neural Visualizer:</strong> The main canvas displaying the neurons as nodes and their connections as lines. The pulsing and glowing of neurons and links indicate activity and learning events. </li> <li> <strong>Metrics Bar:</strong> Located at the top, this bar provides at-a-glance statistics: <ul> <li><strong>Neurons:</strong> The total number of neurons currently in the brain.</li> <li><strong>Connections:</strong> The total number of weighted connections between neurons.</li> <li><strong>Network Health:</strong> Overall stability and efficiency of the network, primarily based on average connection strength.</li> </ul> </li> <li> <strong>Hebbian Timer:</strong> A countdown (e.g., \"Hebbian: 25\") showing the time remaining until the next Hebbian learning cycle is performed. </li> <li> <strong>Control Checkboxes:</strong> <ul> <li><strong>Show links:</strong> Toggles the visibility of the lines connecting the neurons.</li> <li><strong>Show weights:</strong> Toggles the display of the numerical weight on each connection line.</li> <li><strong>Enable pruning:</strong> Toggles the automatic removal of old, weak, or unused neurons to maintain network stability. Disabling this can lead to an unconstrained and potentially unstable network.</li> </ul></ul>\n\nThe big button with the brain on it opens the [Brain Designer](../brain-tool/Brain-Designer.md) which allows you to build and edit custom brains and behaviours\n"
  },
  {
    "path": "Docs/brain-tool/Neuron-Laboratory.md",
    "content": "### **Double click on any neuron** to view the Neuron Laboratory\nIt can be used to inspect individual neurons. The 'Edit Sandbox' tab allows direct adjustment of the live brain state. \n\nThis window also displays the Neurogenesis Experience buffer and neural patterns (last tab) that determine the squid's learned responses.\n\n\n<img src=\"https://github.com/user-attachments/assets/84506732-4613-423a-9268-ce4e474612b2\" width=\"600\">\n\n-----------------------------\n\n\n### Unlocking the 'sandbox' tab allows for full manual control of neuron values:\n \n\n\n<img src=\"https://github.com/user-attachments/assets/72f4e759-82b5-4e55-9f2b-23324a91bf82\" width=\"450\">\n\n"
  },
  {
    "path": "Docs/brain-tool/Personality-Tab.md",
    "content": "\n![image](https://github.com/user-attachments/assets/fb73ae47-efae-4127-85c7-b2da6236e9c7)\n\n <p> The <strong>Personality Tab</strong> details the innate character of your squid, which is randomly assigned at the beginning of a new game.\n--------------\n\n This [personality](../neural-network/Personality.md) has a significant and constant influence on its behaviour, decision-making, and needs. </p> <h4>Interface Elements</h4> <ul> <li><strong>Squid Personality:</strong> Displays the name of the determined personality (e.g., Timid, Adventurous, Lazy).</li> <li><strong>Description:</strong> Provides a paragraph explaining the general nature of this personality type.</li> <li><strong>Personality Modifiers:</strong> Lists the specific, hard-coded rules that this personality applies to the squid's brain. For example, it might detail how anxiety increases 50% faster or how curiosity is boosted.</li> <li><strong>Care Tips:</strong> Offers practical advice on how to best care for a squid with this specific personality, helping you keep it happy and healthy.</li> </ul> <hr>"
  },
  {
    "path": "Docs/engine/Data-Flow-Summary.md",
    "content": "## Data Flow Summary\n\n### [Main Loop](../source-reference/main.py.md)\n\n**Game Loop** → [`TamagotchiLogic`](../source-reference/tamagotchi_logic.py.md) feeds stats → `BrainWidget.update_brain_state()`\n\n---\n\n### Central Hub: [BrainWidget](../source-reference/brain_widget.py.md)\n\n| Component | Description |\n|-----------|-------------|\n| `state` dict | Neuron activations |\n| `weights` dict | Connection strengths |\n| Coordinates | All subsystems |\n\n---\n\n### Worker Threads\n\n| Worker | Responsibility | Output |\n|--------|----------------|--------|\n| [**BrainWorker**](../source-reference/brain_worker.py.md) | Hebbian learning, Neurogenesis | Signals → BrainWidget |\n| [**BrainRenderWorker**](../source-reference/brain_render_worker.py.md) | Offscreen painting | QImage → paintEvent |\n| [**NeuronOutputMonitor**](../source-reference/brain_neuron_outputs.py.md) | Threshold checks | Hooks → Squid behaviors |\n\n---\n\n### Signal Flow\n```\nBrainWorker ──────────┐\n                      │\n                      ▼\n                 BrainWidget ──────▶ Squid\n                      ▲\n                      │\nBrainRenderWorker ────┘\n```\n\n---\n\n### Complete Pipeline\n\n1. **Input Stage**\n   - [`BrainNeuronHooks`](../source-reference/brain_neuron_hooks.py.md) converts game events → neuron activations\n   - Sensors: `can_see_food`, `plant_proximity`, `is_fleeing`, etc.\n\n2. **Processing Stage**\n   - [`BrainWidget`](../source-reference/brain_widget.py.md) updates state dictionary\n   - [`BrainWorker`](../source-reference/brain_worker.py.md) performs Hebbian learning (weight updates)\n   - [`BrainWorker`](../source-reference/brain_worker.py.md) checks [neurogenesis](../neural-network/Neurogenesis.md) triggers\n\n3. **Output Stage**\n   - [`NeuronOutputMonitor`](../source-reference/brain_neuron_outputs.py.md) checks activation thresholds\n   - Fires output hooks → game behaviours\n   - Actions: `flee`, `seek_food`, `sleep`, `change_colour`, etc.\n\n4. **Rendering Stage**\n   - [`BrainRenderWorker`](../source-reference/brain_render_worker.py.md) receives state snapshot\n   - Renders to offscreen QImage\n   - Main thread blits cached image\n"
  },
  {
    "path": "Docs/engine/Decision-Engine.md",
    "content": "#### view source: _[decision_engine.py](https://github.com/ViciousSquid/Dosidicus/blob/2.6.1.2_LatestVersion/src/decision_engine.py)_ _version 2.6.1.2_\n\n## Overview\n\n```\n exploration of emergent behavioural complexity via dynamic, biologically-inspired neural architecture rather than a static state machine. \n```\n\nThe **Decision Engine** is the core action-selection system for Dosidicus. It is responsible for selecting and executing behaviour based on the squid’s *current neural state*, *physiological drives*, *memory influences*, and *personality modifiers*. \n\nUnlike traditional game AI systems (finite-state machines, behaviour trees, or rule stacks), the Decision Engine is **neural-first**: it does not directly reason about the world. Instead, *all perception and context must flow through the brain*.\n\nIn practical terms, this means behaviour is not scripted. It **emerges** from continuous internal signals competing for expression.\n\nThe engine is designed to be:\n\n* Explainable (full decision traces are recorded)\n* Extensible (new drives, memories, or actions integrate naturally)\n* Compatible with future learning systems (dopamine, reinforcement, plasticity)\n\n---\n\n## Design Philosophy\n\n### Neural-First Authority\n\nThe Decision Engine treats the brain as the **single source of truth**. It does not perform manual world queries (e.g. checking for food, scanning objects) to *decide* what to do. Instead, it consumes:\n\n* Perceptual neuron outputs (via [`BrainNeuronHooks`](../source-reference/brain_neuron_hooks.py.md))\n* Internal state neurons (hunger, anxiety, curiosity, etc.)\n* Learned and persistent neural values\n\nDirect world interaction is limited to *execution*, not *decision-making*.\n\n### Continuous Competition\n\nActions are not triggered by rules. Instead, all candidate actions receive **weights** derived from internal signals. These weights compete, and the strongest wins. Small differences matter, enabling hesitation, oscillation, and personality-driven variance.\n\n### Modulation, Not Commands\n\nMemory and personality do not issue instructions. They *bias* behaviour by scaling weights. This ensures:\n\n* Memories influence but do not dominate\n* Personalities remain relevant in all contexts\n* New behaviours automatically inherit modulation\n\n---\n\n## Decision Pipeline\n\nThe decision process is executed in six structured stages.\n\n---\n\n### 1. Perceptual & Brain State Construction\n\nAll perceptual input is retrieved via [`BrainNeuronHooks`](../source-reference/brain_neuron_hooks.py.md):\n\n* Temporal sensors are decayed each tick\n* No manual perception checks are allowed\n\nThe full brain state is constructed from:\n\n* Core neurons\n* Learned neurons\n* Perceptual inputs (merged defensively)\n\nThis combined state represents the squid’s *entire subjective reality* at the moment of decision.\n\n---\n\n### 2. Memory Influence\n\nActive memories are retrieved from the memory manager and converted into **multiplicative biases** on specific actions.\n\nExamples:\n\n* Positive food memories bias eating\n* Object interaction memories bias play and throwing\n* Startle memories suppress exploration and increase comfort-seeking\n\nMemory effects are:\n\n* Directional (positive or negative bias)\n* Non-deterministic\n* Stackable\n\nThis models habits, preferences, and learned aversions rather than explicit recall.\n\n---\n\n### 3. Physiological Urgency (Nonlinear Drives)\n\nPhysiological needs generate **exponential urgency curves**:\n\n* Hunger amplifies eating\n* Sleepiness amplifies sleeping\n\nNonlinear scaling ensures that high-need states *crowd out* other motivations rather than simply increasing priority linearly.\n\n#### Reflex Overrides\n\nCertain extreme states bypass competition entirely:\n\n* Exhaustion → forced sleep\n* Active sleep → no decision\n* Extreme external stimulus → startle response\n\nThese represent **reflex arcs**, not cognitive decisions.\n\n---\n\n### 4. Base Action Weight Construction\n\nEach candidate action receives a base weight derived from the brain state.\n\nActions include:\n\n* Exploring\n* Eating\n* Approaching plants (comfort-seeking)\n* Playing\n* Throwing objects\n* Sleeping\n* Fleeing\n\nWeights are influenced by:\n\n* Drives (hunger, curiosity, satisfaction)\n* Threat and anxiety\n* Perceptual confidence (e.g. food visibility)\n* Contextual suppressors (illness, external stimuli)\n\nThis stage defines *what the squid wants* before learning, memory, or personality intervene.\n\n---\n\n### 5. Memory & Personality Modulation\n\n#### Memory Modifiers\n\nMemory multipliers are applied to relevant actions, biasing selection without enforcing outcomes.\n\n#### Personality Modifiers\n\n[Personalities](../neural-network/Personality.md) act as **gain controls**:\n\n* **Adventurous**: boosts exploration and play\n* **Timid**: suppresses exploration, amplifies comfort-seeking\n* **Greedy**: amplifies eating\n* **Lazy**: suppresses energetic actions\n* **Energetic**: boosts play and exploration\n\nPersonality does not define behaviour — it shapes *how strongly* drives express themselves.\n\n#### Anxiety Coupling\n\nHigh anxiety further amplifies comfort-seeking behaviour, creating feedback between affect and action selection.\n\n---\n\n### 6. Stochastic Selection & Confidence\n\nAfter all modifiers:\n\n* Small stochastic noise is applied to prevent determinism\n* The highest-weighted action is selected\n\n#### Confidence Metric\n\nDecision confidence is computed as the relative margin between the top two competing actions.\n\nThis signal can be used for:\n\n* Animation blending\n* UI visualization\n* Learning-rate modulation\n* Behavioural hesitation\n\n---\n\n## Execution Phase\n\nOnce an action is selected, it is executed via `_execute_neural_decision`.\n\nKey principles:\n\n* Execution respects neural intent\n* World scanning is minimized\n* Fallback behaviours preserve personality flavour\n\nExecution returns a *descriptive outcome string*, not just an action label, enabling rich UI feedback.\n\n---\n\n## Decision Tracing & Visualization\n\nEach decision produces a full trace containing:\n\n* Raw perceptual inputs\n* Brain state snapshot\n* Base action weights\n* Memory influences\n* Urgency multipliers\n* Personality modifiers\n* Final adjusted weights\n* Selected action\n* Confidence score\n\nThis trace is exposed to the Brain Tool UI for inspection and debugging.\n\n---\n\n## What the Decision Engine Is (and Is Not)\n\n### It Is:\n\n* A neural-modulated action selection system\n* Continuous and explainable\n* Designed for emergent behaviour\n* Compatible with learning extensions\n\n### It Is Not:\n\n* A finite-state machine\n* A behaviour tree\n* A planner or lookahead system\n* A reinforcement learner (yet)\n\n---\n\n## Future Extensions\n\nThe Decision Engine is intentionally structured to support:\n\n* Dopaminergic reinforcement signals\n* Action-value learning\n* Noise modulation by arousal or confidence\n* Fully neural affordance perception\n\nThe hardest architectural work — unified perception, continuous competition, and traceability — is already in place.\n\n---\n\n## Summary\n\nThe Decision Engine forms the behavioural core of Dosidicus. By enforcing neural authority, continuous competition, and modulation-based influence, it produces behaviour that is adaptive, interpretable, and personality-consistent — without relying on brittle scripts or hard-coded modes.\n\nIt is not merely a controller, but a foundation for a growing cognitive system.\n"
  },
  {
    "path": "Docs/engine/Engine-Overview.md",
    "content": "## `S`imulated `T`amagotchi `R`eactions via `I`nferencing and `N`eurogenesis `(STRINg)`\n\n### simulation engine overview:\n\nThe architecture of Dosidicus is a \"Bottom-Up\" sensory system where raw environmental data is distilled into neural inputs, which are then filtered through the squid's [personality](https://github.com/ViciousSquid/Dosidicus/wiki/Personality) to produce behaviour.\n\nBuilt from scratch using NumPy.\n\n- No TensorFlow.\n- No PyTorch.\n\n### Core properties:\n* Explicit neuron-level simulation\n* Hebbian plasticity\n* Structural growth (neurogenesis)\n* Dual memory system (short-term and long-term)\n* Headless training capability\n* Plugin extensibility\n* STRINg is optimised for interpretability not scale.\n\nIt treats neural networks not as static architectures, but as evolving structures.\n\n---------------------\n\n\n- Network grows via **[neurogenesis](https://github.com/ViciousSquid/Dosidicus/wiki/Neurogenesis)** and self-trains via **[Hebbian learning](https://github.com/ViciousSquid/Dosidicus/wiki/Hebbian-learning)**\n\n- Automatic **pruning** of redundant neurons and weights (can be turned off)\n- [Experience buffer](https://github.com/ViciousSquid/Dosidicus/wiki/Experience-Buffer) records and encodes learned experiences\n- [decision_engine](https://github.com/ViciousSquid/Dosidicus/wiki/Decision-Engine) uses neural data to make decisions\n\n\n------------------\n\n* Beta (and optional) support for [AI accelerators via ONNX Runtime](https://github.com/ViciousSquid/Dosidicus/wiki/AI-accelerator-support)\n* _Experimental and a work in progress_\n* _Probably not the best way to do this!_ 😃\n\n---------------------------------\n\n\n\n\n## Read Next: [Data flow Summary](https://github.com/ViciousSquid/Dosidicus/wiki/Data-Flow-Summary) overview\n\n#### Further engine studies:\n\n- [Decision Engine](https://github.com/ViciousSquid/Dosidicus/wiki/Decision-Engine)\n- [Brain Widget](https://github.com/ViciousSquid/Dosidicus/wiki/brain_widget.py)\n\nExternal links\n\n* https://medium.com/@reutdayan1/hebbian-learning-biologically-plausible-alternative-to-backpropagation-6ee0a24deb00\n* https://informatics.ed.ac.uk/sites/default/files/2024-03/Qiuye%20Zhang%20Lovelace%20Colloquium%20Poster.pdf\n* https://en.wikipedia.org/wiki/Hebbian_theory\n* https://www.youtube.com/watch?v=TvTQQO5yTa4\n\n\n\n"
  },
  {
    "path": "Docs/engine/Multiplayer.md",
    "content": "**Basic Multiplayer functionality** has been implemented as a plugin (`plugins/multiplayer`) enabled via the Plugin manager\n\n\n_source code: https://github.com/ViciousSquid/Dosidicus/tree/2.5.0.0_latest_release/plugins/multiplayer_\n\n\n\nWhen Multiplayer is enabled:\n\n* the client will search for and automatically connect to other running networked clients on LAN\n\n* Squids can **leave their tanks** via an edge and appear in **other tanks** where they may attempt to steal rocks/decorations and bring them back home as trophies\n\n* Squids will automatically return home after a random duration (between 2 and 5 minutes)\n\n* Supports UDP (default) or TCP/IP (experimental, untested) via included control panel  (Plugins>Multiplayer menu)\n\n* 'Dashboard' is included to see connected clients\n\n* Experimental work in progress, please report issues"
  },
  {
    "path": "Docs/engine/Plugin-Hooks.md",
    "content": "The following hooks are available for [plugins](../engine/Plugin-System.md) to subscribe to:\n\n  ### Lifecycle hooks\n`on_startup`\n-called when the application launches\n\n`on_shutdown`\n-called when the application shuts down\n\n`on_new_game`\n-called when a new game is started\n\n`on_save_game`\n-called when a game is saved\n \n`on_load_game`\n-called when a game is loaded\n        \n  ### Simulation hooks\n`pre_update`\n-called immediately BEFORE the update of a frame (once per 1000 milliseconds)  \n\n`post_update`\n-called immediately AFTER the update of a frame  \n\n`on_speed_change`\n-called when the game speed is changed \n        \n  ### Squid state hooks\n`on_squid_state_change`\n-called when any of the squid's states change \n\n`on_hunger_change`\n-called every time the value of HUNGER changes \n\n`on_happiness_change`\n-called every time the value of HAPPINESS changes \n\n`on_cleanliness_change`\n-called every time the value of CLEANLINESS changes\n  \n`on_sleepiness_change`\n-called every time the value of SLEEPINESS changes \n \n`on_satisfaction_change`\n-called every time the value of SATISFACTION changes \n\n`on_anxiety_change`\n-called every time the value of ANXIETY changes  \n\n`on_curiosity_change`\n-called every time the value of ANXIETY changes  \n        \n  ### Action hooks\n`on_feed`\n-called when the squid is fed via Actions>Feed\n\n`on_clean`\n-called when the tank is cleaned via Actions>Clean\n\n`on_medicine`\n-called when squid is administered medicine via Actions>Medicine\n\n`on_sleep`\n-squid calls this when going to sleep\n \n`on_wake`\n-squid calls this when waking up\n\n`on_startle`\n-squid calls this upon being startled  \n        \n ### Interaction hooks\n`on_rock_pickup`\n-called when squid picks up a rock\n \n`on_rock_throw`\n-called when squid throws a rock\n \n`on_decoration_interaction`\n-called when squid interacts with a decoration item such as rock or plant\n  \n`on_ink_cloud`\n-squid calls this if he is sufficiently startled to produce an ink cloud (rare)  \n        \n ### Neural/memory hooks\n`on_neurogenesis`\n-called when a new neuron is created via neurogenesis\n  \n`on_memory_created`\n-called when a new memory is created  \n\n`on_memory_to_long_term`\n-called when a short term memory is transferred to long term memory  \n        \n ### UI hooks\n`on_menu_creation`\n-custom mod submenu (used with `register_menu_actions` [see below])  \n`on_message_display`\n-Displays a UI message \n        \n### Custom menu action hooks\n`register_menu_actions`\n-Creates a submenu underneath the 'Plugins' menu  \n\n### Designer Hooks\n\nregister_neuron_handler(name, handler, plugin_name, metadata) - plugins call this to register custom neurons"
  },
  {
    "path": "Docs/engine/Plugin-System.md",
    "content": "### Hooks\n\n\nThe application can be extended via **HOOKS** which are made available by the simulation engine when certain events occur\n* Available hooks are documented here: ../engine/Plugin-Hooks.md\n\nPlugins can subscribe to these hooks by calling the following methods in `src/plugin_manager.py` :\n\n### `register_hook`, `subscribe_to_hook`, `trigger_hook`\n<details>\n<summary>Show full methods</summary>\n\n```python\n\ndef register_hook(self, hook_name: str) -> None:\n        \"\"\"\n        Register a new hook that plugins can subscribe to.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.hooks[hook_name] = []\n            self.logger.debug(f\"Registered hook: {hook_name}\")\n\n```\n\n-----------------\n\n```python\n\ndef subscribe_to_hook(self, hook_name: str, plugin_name: str, callback: Callable) -> bool:\n        \"\"\"\n        Subscribe a plugin's callback to a specific hook.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.logger.warning(f\"Plugin {plugin_name} tried to subscribe to non-existent hook: {hook_name}\")\n            return False\n        \n        self.hooks[hook_name].append({\n            \"plugin\": plugin_name,\n            \"callback\": callback\n        })\n        self.logger.debug(f\"Plugin {plugin_name} subscribed to hook: {hook_name}\")\n        return True\n\n```\n\n--------------------\n\n```python\n\ndef trigger_hook(self, hook_name, **kwargs):\n        \"\"\"\n        Trigger a hook, calling all subscribed plugin callbacks.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.logger.warning(f\"Attempted to trigger non-existent hook: {hook_name}\")\n            return []\n        \n        results = []\n        for subscriber in self.hooks[hook_name]:\n            plugin_name = subscriber[\"plugin\"]\n            # Only trigger hooks for enabled plugins\n            if plugin_name.lower() not in self.enabled_plugins:\n                continue\n                \n            try:\n                callback = subscriber[\"callback\"]\n                result = callback(**kwargs)\n                results.append(result)\n            except Exception as e:\n                self.logger.error(f\"Error in plugin {plugin_name} for hook {hook_name}: {str(e)}\", exc_info=True)\n        \n        return results\n\n```\n\n\n</details>\n\n--------------------------\n\n### A Quick Guide to Creating a Plugin\n\n#### A really good starting place would be to look at [multiplayer main.py](https://github.com/ViciousSquid/Dosidicus/blob/2.4.5.1_latest_release/plugins/multiplayer/main.py) and [achievements main.py](https://github.com/ViciousSquid/Dosidicus/blob/2.4.5.1_latest_release/plugins/achievements/main.py) and then refer back to this guide\n\nThis guide will walk you through the essential steps to create a basic plugin that correctly initializes and registers with the system's `PluginManager` _(src/plugin_manager.py)_\n\n```\nThe plugin manager expects:\n* A folder in plugins/ directory (e.g., plugins/pluginname/)\n* A main.py file inside that folder\n* Module-level constants: PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR, PLUGIN_DESCRIPTION, PLUGIN_REQUIRES\n* An initialize(plugin_manager) function that:\n\n1. Creates the plugin instance\n1. Registers it with the plugin manager via plugin_manager.plugins[name] = {..., 'instance': instance}\n1. Returns `True` on success\n```\n\n### Step 1: Create the Plugin Folder Structure\nFirst, create a new folder for your plugin inside the plugins/ directory. The name of this folder will be your plugin's unique identifier (its key), so choose a simple, lowercase name.\n\n```\nDosidicus/\n├── plugins/\n│   ├── my_new_plugin/        <-- Your new plugin folder\n│   │   └── main.py           <-- Your plugin's entry point\n│   ├── auto_care/\n│   └── multiplayer/\n└── src/\n    └── ...\n```\n\n### Step 2: Define Plugin Metadata\nYou must define Module-level constants that the PluginManager uses to identify and load your plugin.\n\n* `PLUGIN_NAME`: (Required) The display name of your plugin.    ### **MUST **BE LOWERCASE!\n\n* `PLUGIN_DESCRIPTION`: A brief description of what your plugin does.\n\n* `PLUGIN_AUTHOR`: Your name or alias.\n\n* `PLUGIN_VERSION`: The version of your plugin.\n\n```python\n# plugins/my_new_plugin/main.py\n\n# --- Plugin Metadata --- # PLUGIN_NAME must be lowercase\nPLUGIN_NAME = \"my new plugin\"\nPLUGIN_DESCRIPTION = \"A simple plugin that demonstrates the basics.\"\nPLUGIN_AUTHOR = \"Seymour Butts\"\nPLUGIN_VERSION = \"1.0\"\n```\n\n### Step 3: Create the Main Plugin Class\nIt's best practice to encapsulate your plugin's logic within a class. This class will hold the state and functionality of your plugin, such as methods for enabling, disabling, and handling events (hooks).\n\n```python\n# plugins/my_new_plugin/main.py\n\nclass MyPlugin:\n    def __init__(self, plugin_manager, plugin_key):\n        self.plugin_manager = plugin_manager\n        self.plugin_key = plugin_key # e.g., \"my_new_plugin\"\n        self.is_enabled = False\n\n        # Subscribe to a system event (hook)\n        self.plugin_manager.subscribe_to_hook(\n            \"on_startup\",\n            self.plugin_key,\n            self.on_app_startup\n        )\n\n    def enable(self):\n        \"\"\"Called when the user enables the plugin.\"\"\"\n        print(f\"[{self.plugin_key}] Plugin Enabled!\")\n        self.is_enabled = True\n        return True # Return True on success\n\n    def disable(self):\n        \"\"\"Called when the user disables the plugin.\"\"\"\n        print(f\"[{self.plugin_key}] Plugin Disabled!\")\n        self.is_enabled = False\n        return True # Return True on success\n\n    def on_app_startup(self, **kwargs):\n        \"\"\"Callback for the on_startup hook.\"\"\"\n        if self.is_enabled:\n            print(f\"[{self.plugin_key}] Application has started up!\")\n```\n\n### Step 4: Implement the initialize Function\nThe PluginManager requires a global function named initialize in your main.py. This function's job is to:\n\n1. Get the plugin's unique key (the folder name).\n\n2. Create an instance of your main plugin class.\n\n3. Register the instance with the PluginManager.\n\nThis is the crucial step that connects your plugin to the main application.\n\n```python\n# plugins/my_new_plugin/main.py\nimport os\n\n# (Metadata and Class definition from above)\n# ...\n\ndef initialize(plugin_manager_instance):\n    \"\"\"\n    This function is called by the PluginManager to initialize the plugin.\n    \"\"\"\n    # Get the plugin's unique key from its directory name\n    # os.path.basename(os.path.dirname(__file__)) will return \"my_new_plugin\"\n    plugin_key = os.path.basename(os.path.dirname(__file__))\n\n    try:\n        # Create an instance of your main plugin class\n        plugin_instance = MyPlugin(plugin_manager_instance, plugin_key)\n\n        # Register the plugin instance with the manager\n        # This makes the manager aware of the plugin and its instance\n        plugin_manager_instance.plugins[plugin_key] = {\n            'instance': plugin_instance,\n            'is_enabled_by_default': False # ALWAYS FALSE NEVER CHANGE THIS\n        }\n\n        print(f\"[{plugin_key}] Plugin initialized successfully.\")\n        return True # IMPORTANT: Return True on successful initialization\n\n    except Exception as e:\n        print(f\"[{plugin_key}] Failed to initialize: {e}\")\n        return False # Return False on failure\n```\n\nWith these steps, you have a complete, well-structured plugin that the system can discover, load, and run. The user can then enable and disable it manually through the application's plugin management UI."
  },
  {
    "path": "Docs/engine/Save-File-Format.md",
    "content": "Save and load functionality is handled by the [SaveManager](https://github.com/ViciousSquid/Dosidicus/blob/2.4.4_stable/src/save_manager.py) class. \n\nIt uses a structured approach that packages all game data into a single, portable file. <h4>Save File Location and Types</h4> The SaveManager creates and manages files within a `saves` directory in the application's root folder. It maintains two distinct save slots: <ul> <li><code>`autosave.zip`</code>: This file is used for periodic, automatic saves that occur in the background.</li> <li><code>`save_data.zip`</code>: This file is used when the player manually saves their game through the File menu.</li> </ul> <h4>Save File Structure</h4> When `save_game()` is called, it bundles data into the following internal JSON files within the zip archive: <ul> <li><code>game_state.json</code>: Contains general game state information, such as the squid's core stats (hunger, happiness), and other top-level game variables.</li> <li><code>brain_state.json</code>: Stores the complete state of the neural network, including neurogenesis data and the `pattern buffer` of learned experiences.</li> <li><code>ShortTerm.json</code>: A snapshot of all memories currently in the squid's short-term memory.</li> <li><code>LongTerm.json</code>: A snapshot of all memories that have been consolidated into long-term memory.</li> <li><code>plugin_data.json</code>: Contains any persistent data that active plugins have chosen to save.</li> <li> <code>statistics.json</code>: Stores persistant statistics tracked over time such as squid age, distance swam, foods eaten, etc</li> <li><code>uuid.txt</code>: Unique squid identifier (128bit number)</li></ul> \n\n------------------------------------------\n\n[SaveViewer.html](../extras/SaveViewer.md) is available for easy viewing and comparisons of save files. \n\nThis tool can also convert old v1 saved games (pre 2.4.5.0) to the new v2 format (2.5.0.0+)\n\n"
  },
  {
    "path": "Docs/engine/config.ini.md",
    "content": "The [config.ini](https://github.com/ViciousSquid/Dosidicus/blob/2.4.4_stable/config.ini) file allows for the fine-tuning of various game mechanics without needing to alter the source code. It is structured into sections, each controlling a different aspect of the application's behavior. \n\n```\nNOTE: As of 2.6.1.0 there is a dedicated preferences window that exposes every setting. \nit can be accessed via the View menu  (View>Preferences)\nManual editing of the Config file still works but is not recommended\n```\n\n<h4>[General]</h4>\n\n`language`: = `en`  (Default) - Can be `de`, `en`, `es`, `fr`, `ja`, `ml` or `zh`\n\n<h4>[Debug]</h4> \n\n`multiplayer_debug`  Show debug messages when multiplayer is enabled (Defaults to False) [Developer feature]\n\n<h4>[RockInteractions]</h4> This section controls the logic for how the squid interacts with rock items. <ul> <li><code>pickup_probability</code>: A value from 0.0 to 1.0 representing the chance the squid will decide to pick up a rock it encounters.</li> <li><code>throw_probability</code>: A value from 0.0 to 1.0 representing the chance the squid will throw a rock it is currently carrying.</li> <li><code>min_carry_duration</code>: The minimum time in seconds the squid will carry a rock before considering throwing it.</li> <li><code>max_carry_duration</code>: The maximum time in seconds the squid will carry a rock.</li> <li><code>cooldown_after_throw</code>: Time in seconds the squid must wait after throwing a rock before it can throw another.</li> <li><code>happiness_boost</code>, <code>satisfaction_boost</code>, <code>anxiety\\reduction</code>: The numerical amount that the corresponding stats are changed when a positive rock interaction occurs.</li> </ul> <h4>[Neurogenesis]</h4> This is the main section for controlling the creation of new neurons. <ul> <li><code>enabled</code>: A boolean (True/False) that globally enables or disables the neurogenesis feature.</li> <li><code>cooldown</code>: The mandatory waiting period in seconds after a neuron is created before another one can be.</li> <li><code>max_neurons</code>: The maximum number of neurons the brain can have. Neurogenesis stops if this limit is reached and pruning is enabled.</li> </ul> <h5>[Neurogenesis.Novelty], [Neurogenesis.Stress], [Neurogenesis.Reward]</h5> These subsections control the specific triggers for creating new neurons. Each has the following parameters: <ul> <li><code>enabled</code>: Enables or disables this specific pathway for neurogenesis.</li> <li><code>threshold</code>: The value the corresponding counter must exceed to trigger neuron creation.</li> <li><code>decay_rate</code>: The rate at which the counter's value decreases over time (e.g., 0.80 means the counter retains 80% of its value after a decay cycle).</li> <li><code>max_counter</code>: The maximum value the counter can reach.</li> <li><code>*_modifier</code>: Personality-specific multipliers that make it easier or harder for certain personalities to trigger neurogenesis (e.g., adventurous_modifier = 1.2 makes adventurous squids 20% more sensitive to novelty).</li> </ul> <h5>[Neurogenesis.NeuronProperties]</h5> This section defines the default properties for newly created neurons. <ul> <li><code>base_activation</code>: The initial activation value for a new neuron.</li> <li><code>position_variance</code>: Determines the random offset when placing a new neuron on the brain map.</li> <li><code>default_connections</code>: A boolean to enable or disable the automatic creation of pre-wired connections for new neurons.</li> <li><code>connection_strength</code>, <code>reciprocal_strength</code>: The initial weights for the default connections that a new neuron forms.</li> </ul> <h5>[Neurogenesis.Appearance] & [Neurogenesis.VisualEffects]</h5> These sections control the visual feedback for neurogenesis. <ul> <li><code>*_color</code>: Defines the RGB color for each type of new neuron.</li> <li><code>*shape</code>: Defines the shape (triangle, square, circle) for each type of new neuron.</li> <li><code>highlight_duration</code>, <code>highlight_radius</code>: Controls the size and duration of the visual highlight effect when a neuron is created.</li> <li><code>pulse_effect</code>, <code>pulse_speed</code>: Configures the pulsing animation on the new neuron.</li> </ul> <hr>"
  },
  {
    "path": "Docs/extras/Achievements.md",
    "content": "Achievements are implemented via the [achievements plugin](https://github.com/ViciousSquid/Dosidicus/tree/2.5.0.0_latest_release/plugins/achievements) (Enabled by default)\n\n\n# Achievements List\n\n### **Total Achievements:** **50**         (41 visible, 9 secret)\n Worth 1125 max possible points\n\n---\n\n## 🍽️ Feeding Category (5)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🍽️ | First Bite | Feed the squid for the first time | 10 | 1 |\n| 🥄 | Regular Meals | Feed the squid 10 times | 15 | 1 |\n| 🍴 | Dedicated Caretaker | Feed the squid 50 times | 25 | 2 |\n| 👨‍🍳 | Master Chef | Feed the squid 100 times | 50 | 3 |\n| 🌟 | Culinary Legend | Feed the squid 500 times | 100 | 4 |\n\n---\n\n## 🧠 Neurogenesis Category (6)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🧠 | Brain Spark | Create the first neurogenesis neuron | 20 | 1 |\n| 🔮 | Neural Network | Create 10 neurons through neurogenesis | 30 | 2 |\n| 💫 | Expanding Mind | Create 50 neurons through neurogenesis | 50 | 3 |\n| 🌌 | Cerebral Powerhouse | Create 100 neurons through neurogenesis | 75 | 4 |\n| ⚡ | Strengthened Synapse | Level up a neuron for the first time | 15 | 1 |\n| 🌠 | Peak Performance | Level a neuron to maximum strength | 40 | 3 |\n\n---\n\n## 😴 Sleep Category (3)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 😴 | Sweet Dreams | The squid wakes from its first sleep | 10 | 1 |\n| 🛏️ | Well Rested | The squid has slept 10 times | 20 | 2 |\n| 💭 | Deep Dreamer | Squid entered REM sleep | 25 | 2 |\n\n---\n\n## 📅 Milestones Category (7)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| ⏰ | One Hour Old | Squid reached 1 hour old | 15 | 1 |\n| 📅 | Growing Up | Squid reached 10 hours old | 30 | 2 |\n| 🎂 | One Day Wonder | Squid survived for 24 hours | 50 | 3 |\n| 🏅 | Week Veteran | Squid has lived for one week | 100 | 4 |\n| 🎖️ | Month Veteran | Squid has lived for one month | 150 | 5 |\n| 😄 | Pure Bliss | Reach 100% happiness | 20 | 2 |\n| ⚖️ | Perfect Balance | All stats above 80% simultaneously | 40 | 3 |\n\n---\n\n## 🧹 Cleaning Category (3)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🧼 | First Scrub | Clean the tank for the first time | 10 | 1 |\n| ✨ | Spotless Environment | Clean the tank 25 times | 25 | 2 |\n| 🧹 | Germaphobe | Keep cleanliness above 90% for 1 hour straight | 30 | 2 |\n\n---\n\n## 💊 Health Category (3)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 💊 | First Aid | Give medicine for the first time | 10 | 1 |\n| 🩺 | Doctor Squid | Give medicine 10 times | 20 | 2 |\n| 💪 | Comeback Kid | Recover from critically low health (<20%) to full | 40 | 3 |\n\n---\n\n## 🎯 Interaction Category (14)\n\n### Rocks\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🪨 | Rock Collector | Pick up a rock for the first time | 10 | 1 |\n| ⛰️ | Stone Gatherer | Pick up 10 rocks | 15 | 1 |\n| 🏔️ | Boulder Hoarder | Pick up 50 rocks | 30 | 2 |\n| 🎯 | Skipping Stones | Throw a rock for the first time | 10 | 1 |\n| 🚀 | Rock Launcher | Throw 25 rocks | 20 | 2 |\n| 💨 | Catapult Master | Throw 100 rocks | 40 | 3 |\n\n### Decorations & Plants\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🪴 | Interior Decorator | Push a decoration for the first time | 10 | 1 |\n| 🏠 | Furniture Mover | Push decorations 10 times | 15 | 1 |\n| 🎨 | Feng Shui Master | Push decorations 50 times | 30 | 2 |\n| 🌱 | Green Thumb | Interact with a plant for the first time | 10 | 1 |\n| 🌿 | Garden Explorer | Interact with plants 10 times | 15 | 1 |\n| 🌳 | Botanist | Interact with plants 50 times | 30 | 2 |\n\n### General Interaction\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🔍 | Curious Inspector | Investigate 25 different objects | 25 | 2 |\n| 🕵️ | Master Detective | Investigate 100 different objects | 50 | 3 |\n\n---\n\n## 💩 Exploration Category (1)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 💩 | Mischief Maker | Squid threw a poop for the first time | 10 | 1 |\n\n---\n\n## 🖤 Ink Category (2)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🖤 | Smoke Screen | Squid releases ink cloud for the first time | 15 | 1 |\n| 🌫️ | Ink Master | Release 20 ink clouds | 25 | 2 |\n\n---\n\n## 💾 Memory Category (3)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 💾 | First Memory | Form the first memory | 15 | 1 |\n| 🗄️ | Long Term Thinking | Promote a memory to long-term storage | 25 | 2 |\n| 📚 | Photographic Memory | Have 50 memories stored | 40 | 3 |\n\n---\n\n## 😊 Emotional Category (4)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🤔 | Curious George | Curiosity reaches 100% | 15 | 1 |\n| 🧘 | Zen Master | Keep anxiety below 10% for 30 minutes | 30 | 2 |\n| 😱 | Startled! | Startle the squid for the first time | 10 | 1 |\n| 😰 | Nervous Wreck | Anxiety reaches 100% | 15 | 2 |\n\n---\n\n## 🔬 Meta Category (3)\n\n| Icon | Name | Condition | Points | Tier |\n|------|------|-----------|--------|------|\n| 🔬 | Brain Surgeon | Open the brain visualization tool | 10 | 1 |\n| ⏩ | Speed Demon | Run simulation at max speed for 10 minutes | 15 | 2 |\n| 🏆 | Completionist | Unlock 30 other achievements | 100 | 4 |\n\n---\n\n## 📊 Tier System\n\nThe tier system uses 5 levels with different colors:\n\n| Tier | Name | Color | Example Achievements |\n|------|------|-------|----------------------|\n| 1 | Bronze | #CD7F32 | First Bite, Brain Spark, Sweet Dreams |\n| 2 | Silver | #C0C0C0 | Regular Meals, Neural Network, Pure Bliss |\n| 3 | Gold | #FFD700 | Master Chef, Peak Performance, Perfect Balance |\n| 4 | Platinum | #E5E4E2 | Culinary Legend, Cerebral Powerhouse, Completionist |\n| 5 | Diamond | #B9F2FF | Month Veteran |\n\n---\n\n## Summary Statistics\n\n- **Total Achievements:** 50\n- **Visible Achievements:** 41\n- **Hidden Achievements:** 9\n- **Total Points Available:** 1,125\n- **Most Common Tier:** Tier 1 (Bronze)\n- **Most Common Category:** Interaction (14 achievements)\n"
  },
  {
    "path": "Docs/extras/Decoration-Window.md",
    "content": "\n![image](https://github.com/user-attachments/assets/bed7c083-80d3-4ecd-8375-dd6c8746d3d4)\n\n# Press `D` to open the decorations window\n*  ## or use the `View > Decorations` menu\n\n------------------------\n\nThe <strong>Decorations Window</strong> is your catalog for all the items you can use to furnish your squid's environment. Placing decorations is not just for cosmetic appeal; each item has a unique effect on the squid's mood and well-being, influencing his stats and behavior. This feature allows you to customize the tank and actively manage your squid's environment. </p> <h4>How to Use the Decorations Window</h4> <ol> <li><strong>Opening the Window:</strong> To open the catalog, navigate to the menu bar at the top of the main application window, click on <strong>View</strong>> <strong>Decorations</strong> (`Or simply press T`) \n<li> <strong>Adding an Item:</strong> To place a decoration, <strong>click and hold</strong> the desired item from the Decorations Window, <strong>drag</strong> it over to the main tank area, and <strong>release</strong> to drop it. </li> </ol> <h4>Interacting with Placed Decorations</h4> <p> Once an item has been dropped into the tank, you can manipulate it directly: </p> <ul> <li><strong>Select:</strong> Click on any decoration in the tank to select it. <li><strong>Move:</strong> Click and drag a selected item to move it.</li> <li><strong>Resize:</strong> Select an item use <strong>mouse wheel</strong> to scale it. Note that some items, like rocks, cannot be resized.</li> <li><strong>Delete:</strong> Select an item and press <strong>Delete</strong> to permanently remove it from the tank.</li> </ul> <h4>Decoration Effects</h4> <p> Every decoration has hidden properties defined in  [decoration_stats.json](https://github.com/ViciousSquid/Dosidicus/blob/2.4.4_stable/src/decoration_stats.json) - when the squid interacts with or is near a decoration, these properties can influence his statistics, such as happiness, anxiety, and curiosity. Experiment with different items to see how they affect your squid's mood and development!\n"
  },
  {
    "path": "Docs/extras/Easter-Eggs.md",
    "content": "<img src=\"https://github.com/user-attachments/assets/07f2c02f-b8b5-428c-8bf3-37b69cf188c7\" width=\"400\">\n\n On the About tab of the Brain Window, a colour can be selected for the squid.\n\nThis colour will be added to your save file and will persist across play sessions\n\n--------------\n\n<img src=\"https://github.com/user-attachments/assets/901805e2-48c1-4c44-b9bd-0b8e2d363000\" width=\"400\">\n\n**ml** (Millennial) is an available language (along with 7 others)\n\n--------------\n\nResizing the play window will startle the squid\n"
  },
  {
    "path": "Docs/extras/SaveViewer.md",
    "content": "#### SaveViewer.html\n\n* Quickly and easily open save files and view the contents.\n* `Network` tab visualises entire network\n* No need to open the game\n\n<img src=\"https://github.com/user-attachments/assets/2d665825-cf92-42e9-82df-8ce7c8063b7d\" width=\"600\">"
  },
  {
    "path": "Docs/extras/UUID.md",
    "content": "When a squid is created at the start of a new game he is assigned a unique fingerprint (also the name of his save file)\n\nIt looks like this:\n```example\nSquidSignature    ab148370-6cd7-4d7e-a3d2-e33b68c4b615\n```\n\n\nThis currently serves no purpose but in future it will be used for any/all/none of the following:\n\n* DNA / Genetic code\n* Encoding personality traits/behaviours\n* Use as node name in multiplayer\n* seed to always generate the exact same squid\n* Verification (used as a 'magic number' or checksum)"
  },
  {
    "path": "Docs/getting-started/Care-Guide.md",
    "content": "<h3>Care Guide: Nurturing Body and Mind</h3> <p> Caring for your squid is a unique experience that goes beyond simple pet simulation. You are not only responsible for its physical well-being but also for the growth and development of its simulated brain. This guide will walk you through the essentials of caring for your squid's core needs and enriching its environment to foster a healthy, intelligent, and happy companion. </p> <h4>Part 1: Managing Your Squid's Core Needs</h4> <p> Your squid has several fundamental needs that you must monitor and manage. These are handled primarily through the <strong>Actions</strong> menu in the main application window. </p> <ul> <li> <strong>Feeding Your Squid:</strong> <ul> <li><strong>How:</strong> Select <code>Actions > Feed</code> from the menu to drop food into the tank.</li> <li><strong>Why:</strong> Hunger is a primary driver of your squid's behavior. A hungry squid will become anxious and its happiness will decrease. Feeding it not only satisfies hunger but also increases happiness and satisfaction, creating a positive memory.</li> </ul> </li> <li> <strong>Cleaning the Environment:</strong> <ul> <li><strong>How:</strong> As your squid lives in its environment, it will produce poop. Select <code>Actions > Clean</code> or click on a poop and press DEL to delete it.</li> <li><strong>Why:</strong> A dirty environment is a major source of stress and anxiety for your squid. Keeping the tank clean is essential for maintaining high happiness and low stress levels.</li> </ul> </li> <li> <strong>Administering Medicine:</strong> <ul> <li><strong>How:</strong> If your squid becomes sick, select <code>Actions > Medicine</code>.</li> <li><strong>Why:</strong> Sickness can negatively impact all of your squid's stats. Promptly administering medicine is crucial for its recovery and overall health.</li> </ul> </li> </ul> <h4>Part 2: Enriching Your Squid's Mind & Stimulating the Brain</h4> <p> A healthy squid needs more than just food and a clean tank; it needs mental stimulation. Your actions directly influence the development of its neural network through learning (strengthening connections) and neurogenesis (creating new neurons). </p> <h5>Fostering Learning (Strengthening Connections)</h5> <p> Your squid's brain learns by associating events. When two of its neurons are active at the same time, the connection between them gets stronger. You can encourage this process through consistent care. </p> <p> <strong>Example:</strong> When your squid is hungry, its <code>hunger</code> neuron is highly active. When you feed it, its <code>satisfaction</code> neuron becomes active. The brain sees these two events happen together and strengthens the connection between them. Over time, the squid \"learns\" that eating leads to satisfaction. </p> <h5>Stimulating Neurogenesis (Creating New Neurons)</h5> <p> Neurogenesis is the birth of new neurons and is the key to enriching your squid's mind. You can trigger this process by providing specific types of stimulation that correspond to the three neurogenesis pathways. </p> <ul> <li> <strong>1. Provide Novelty:</strong> <ul> <li><strong>Goal:</strong> To create <code>novel</code> neurons that boost curiosity.</li> <li><strong>How-To:</strong> The best way to provide novelty is to regularly change the squid's environment. Open the <strong>Decorations</strong> window (from the <code>View</code> menu) and drag-and-drop new items like plants or different rocks into the tank. Each new object the squid investigates increases the novelty counter, and when it surpasses its threshold, a new neuron can be born.</li> </ul> </li> <li> <strong>2. Provide Rewards:</strong> <ul> <li><strong>Goal:</strong> To create <code>reward</code> neurons that reinforce happiness and satisfaction.</li> <li><strong>How-To:</strong> Positive reinforcement is key. Consistently feeding the squid when it's hungry, keeping its tank clean, and encouraging play (like interacting with rocks) will increase the reward counter. This teaches the squid which behaviors lead to positive outcomes.</li> </ul> </li> <li> <strong>3. Provide Healthy Challenges:</strong> <ul> <li><strong>Goal:</strong> To create <code>stress</code> neurons that act as coping mechanisms for unpleasant situations.</li> <li><strong>How-To:</strong> While chronic stress is harmful, allowing the squid to experience and overcome small, manageable stressors helps it build resilience. For instance, allowing it to get moderately hungry before feeding it can build the stress counter. When a <code>stress</code> neuron is created, it comes pre-wired with an inhibitory connection to the <code>anxiety</code> neuron, effectively making your squid better at managing anxiety in the future.</li> </ul> </li> </ul> <h5>Cater to Your Squid's Personality</h5> <p> Remember that every squid has a unique personality that affects its needs. Check the <strong>Personality Tab</strong> in the Brain Tool to understand your squid's specific traits and get tailored advice. </p> <p> <strong>Example:</strong> A <strong>Timid</strong> squid's anxiety will decrease significantly when it is near plants, making them an essential decoration for its well-being. In contrast, an <strong>Adventurous</strong> squid will thrive on a constantly changing environment with new decorations to investigate, which will be your primary method for providing novelty. </p>"
  },
  {
    "path": "Docs/getting-started/Changelog.md",
    "content": "### version 2.6.1.2\n`23 Feb 2026`\n\n* Optional [hardware AI accelerator support via ONNX Runtime](../neural-network/AI-Accelerator-Support.md) - _Experimental, disabled by default_\n\n* NEW: [brain_to_keras.py](https://github.com/ViciousSquid/Dosidicus/blob/2.6.1.2_LatestVersion/extras/brain_to_keras.py) (from the dev branch) in the `extras` folder - _attempts to convert a Dosidicus brain.json to Keras v3 (experimental)_\n\n* Improved Brain Tool short-term and long-term memory tabs: **more varied and verbose memories**\n* **Random Humboldt squid facts** can occasionally appear in status bar\n* FIXED: BrainTool Hebbian timers weren't in sync [(20)](https://github.com/ViciousSquid/Dosidicus/issues/20)\n* FIXED squid now properly goes to sleep when sleepiness=max\n\n-------------------------\n\n\n### version 2.6.1.1\n`20 Feb 2026`\n\n### Milestone 2\n* Added `linux_setup.sh`\n* Code optimisations & bug fixes\n\n-------------------------\n\n### version 2.6.1.0\n\n`21 Jan 2026`\n\n#### build 1219\n* Translation files for 7 languages: _English_, _French_, _Spanish_, _German_, _Chinese_, _Japanese_, _Millennial_\n* [Stable release for Windows](https://github.com/ViciousSquid/Dosidicus/releases/tag/v2.6.1.0)\n\n`18 Dec 2025`\n\n#### build 1218 **Milestone 2** Release \n* Integrated Designer into Brain Tool\n* Added ability to create custom neurons\n* NEW: [Headless brain trainer](https://github.com/ViciousSquid/Dosidicus/blob/v2.6.1.0__b1218_LatestVersion/headless/README_headless_trainer.md) with accelerated time epochs\n* NEW: Global preferences window\n* Added an additional 4 custom brains\n* French and Spanish Translations (_does not currently include Designer_)\n\n-------------------\n\n### version 2.6.0.3\n`11 Dec 2025`\n\n* Added FEED, CLEAN, MEDICINE buttons to UI\n* Added 5 example [custom brains](https://github.com/ViciousSquid/Dosidicus/tree/2.6.0.2_latest_release/custom_brains)\n* Neuron/font sizes and other UI elements now configurable via config.ini\n* FIXED: missing `update_score` method in `StatisticsWindow`\n* [Brain Designer](../brain-tool/Brain-Designer.md) can now import current running brain from Brain Tool\n* NEW: Neuron output bindings can be used to create simple IF THEN behaviours for the squid\n\n-------------------\n\n### version 2.6.0.2\n`8 Dec 2025`\n\n* NEW: [Brain Designer](../brain-tool/Brain-Designer.md) - create your own custom squid brains!\n* NEW: 5 custom brain templates that can be edited however you like [[dir](https://github.com/ViciousSquid/Dosidicus/tree/2.6.0.1_latest_release/custom_brains)]\n* NEW: [Example squid](../getting-started/Example-Squids.md) **Miroslav**\n\n-------------------\n\n### version 2.5.0.0\n`3 Dec 2025`\n\n* New [Save Viewer](../extras/SaveViewer.md)\n* Added 8 additional plant decorations & associated stats\n* Brain Network tab now has buttons for Experience Buffer and Neuron Laboratory\n* Fixed a bug where the brain state was not being restored properly from save\n* Added [showman wrapper](../source-reference/neurogenesis_show.py.md) for Neurogenesis\n* Code refactoring and removal of legacy cruft (pre version 2.4.X)\n* Feature-complete stable code-base \n\n-------------------\n\n### version 2.4.5.1 _patch_\n`25 Nov 2025`\n\n* [Engine](../engine/Engine-Overview.md) update - Neurogenesis and hebbian calculations now in own thread so UI remains responsive\n* **New**: Improved [multiplayer](../engine/Multiplayer.md) plugin!!\n* **New**: [Achievements](../extras/Achievements.md) (50 to collect)\n* **New**: Full interactive tutorial when starting a new game\n* Every squid is born with a `uuid` that stays with him his entire life\n* Added [Neuron Laboratory](../brain-tool/Neuron-Laboratory.md) via the View menu or by double clicking any neuron\n\n\n\n-------------------\n\n### version 2.4.5.0\n`15 Nov 2025`\n\n* Improved [plugin manager](../engine/Plugin-System.md)\n* Massively overhauled and [improved](../source-reference/neurogenesis_show.py.md) neurogenesis\n* Track statistics such as squid age, distance travelled, total foods eaten, etc\n* Improved load/save mechanism (backward compatible with v2.3 saves and earlier)\n* Hebbian now trains on 2 active neuron pairs at once\n* Redesigned Brain Tool [learning tab](../brain-tool/Learning-Tab.md)\n* Global counters now respect game speed\n* Arcade-style (High-Score) system\n* Animations throughout the UI\n\n\n-------------------\n\n### version 2.4.4.1\n`04 Sept 2025`\n\n* Code cleanup and stability improvements\n* WIP: Track statistics such as squid age, distance travelled, total foods eaten, etc\n* ADDED: Small chance of squid creating an ink cloud when startled\n* ADDED: Experimental [multiplayer](../engine/Multiplayer.md) plugin\n\n-------------------\n\n### version 2.4.3\n\nMilestone 1\n\n**Initial stable release**\n\n### **AUTHOR GOT A TATTOO** to celebrate 1 year of this project!\n\n<img src=\"https://github.com/user-attachments/assets/fe50e8d8-cb76-4b20-830a-ea6af28bb608\" width=\"250\">\n\n  <a href=\"https://www.buymeacoffee.com/vicioussquid\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/default-orange.png\" alt=\"Buy Me A Coffee\" height=\"41\" width=\"174\"></a>"
  },
  {
    "path": "Docs/getting-started/Example-Squids.md",
    "content": "```\nThe example_squids folder contains saved brains that have already accumulated some experiences:\n```\n-------------------------\n\n\n### [b071f720_593e_471f_8412_8ee84172a1b0.zip](https://github.com/ViciousSquid/Dosidicus/blob/2.6.0.0_latest_release/example_squids/b071f720_593e_471f_8412_8ee84172a1b0.zip) \n\n#### Squid name: Miroslav | Age: 5 | Number of neurons: 12\n\n* Young squid with default brain type (7 core neurons)\n* ${\\color{red}stress}$ neurons have developed in response to being extremely hungry in the past\n* ${\\color{yellow}novelty}$ neurons have developed - positive experiences related to investigation/exploration\n* many connections (111!) - strong brain, highly responsive, quick to learn (high plasticity)\n* `novelty_object_investigation` has extremely high multiplier of 14 suggesting episodes of manic excitement\n\n\n<img src=\"https://github.com/user-attachments/assets/4d1381d2-2afc-4893-a101-d2c45e6cf597\" width=\"500\">\n\n\n-------------------------\n\n"
  },
  {
    "path": "Docs/getting-started/Home.md",
    "content": "### _A transparent neural sandbox disguised as a digital pet_\nA micro neural engine for small autonomous agents that learn via Hebbian dynamics and grow new structure when exposed to novelty.\n\n----------------------------------\n\n##  [Manifesto](https://github.com/ViciousSquid/Dosidicus/wiki/Cognitive-Sandbox-Manifesto-%7C-Artificial-Life-and-Transparent-Neural-Systems) |   [Wiki](https://github.com/ViciousSquid/Dosidicus/wiki) | [Changelog](https://github.com/ViciousSquid/Dosidicus/wiki/changelog) \n\n----------------------------\n\n## Getting Started\nNew to Dosidicus? Start here to understand how to interact with your squid.\n* **[Care Guide / Getting Started](../getting-started/Care-Guide.md)**\n* **[Personalities](../neural-network/Personality.md)** — How different squid types behave.\n* **[Decoration Window](../extras/Decoration-Window.md)** — Managing the squid's environment.\n\n---\n\n\n### Biological Autonomy\n* **[Vision System](../neural-network/Vision-System.md)** — Realistic foraging and food detection.\n* **[Hebbian Learning](../neural-network/Hebbian-Learning.md)** — The algorithm behind 30-second learning cycles.\n* **[Neurogenesis](../neural-network/Neurogenesis.md)** — How the squid creates new neurons based on environment.\n* **[Decision Engine](../engine/Decision-Engine.md)** — Making choices based on hunger, sleep, and memory.\n\n### Engine Architecture & Logic\n* **[Engine Overview](../engine/Engine-Overview.md)** — High-level system architecture.\n* **[main.py](../source-reference/main.py.md)** — The main simulation loop.\n* **[tamagotchi_logic.py](../source-reference/tamagotchi_logic.py.md)** — Core needs and health management.\n* **[squid.py](../source-reference/squid.py.md)** — The physical squid class.\n* **[Memory System](https://github.com/ViciousSquid/Dosidicus/blob/main/Docs/Memory%20System.md)** & **[memory_manager.py](../source-reference/memory_manager.py.md)** — Managing experiences.\n\n---\n\n## Tools & Configuration\nFine-tune the simulation and monitor the squid's neural activity.\n\n### [The Brain Tool](../brain-tool/Network-Tab.md)\n* **[Network Tab](../brain-tool/Network-Tab.md)** | **[Learning Tab](../brain-tool/Learning-Tab.md)**\n* **[Memory Tab](../brain-tool/Memory-Tab.md)** | **[Decisions Tab](../brain-tool/Decisions-Tab.md)**\n* **[Personality Tab](../neural-network/Personality.md-tab)**\n\n### System Settings\n* **[config.ini](../engine/config.ini.md)** — Adjusting simulation parameters.\n* **[Save File Format](../engine/Save-File-Format.md)** — Structure of persisted data.\n* **[Plugin System](../engine/Plugin-System.md)** — Extending the engine's capabilities.\n"
  },
  {
    "path": "Docs/neural-network/AI-Accelerator-Support.md",
    "content": "#### New in v2.6.1.2 (experimental)\n\n[compute_backend.py](https://github.com/ViciousSquid/Dosidicus/blob/2.6.1.2_onnx/src/compute_backend.py) facilitates hardware acceleration for neural calculations by changing one line:\n\nin `config.ini`:\n\n```\n[Compute]\n\nbackend = numpy\n```\n\n Options:\n-    `numpy`  - default, no extra dependencies\n-    `onnx`   - enables hardware AI accelerator support via ONNX Runtime\n\nauto-selects `DirectML`, `OpenVINO`, `QNN`, or falls back to `numpy` if no runtime is present.\n\n---------------------------------\n\nrequires package to be installed (refer to the following list:)\n\n#### Recommended packages by platform:\n\n- **Windows** | _NVIDIA + AMD + Intel GPU + NPU (DirectML)_ | `pip install onnxruntime-directml`\n\n- **Windows** | _NVIDIA only (maximum CUDA performance)_ | `pip install onnxruntime-gpu`\n\n- **Windows** | _Qualcomm 8CX / SQX / Snapdragon (NPU)_ | `pip install onnxruntime-qnn`\n\n- **macOS** | _Apple Silicon and Intel Macs_ | `pip install onnxruntime`\n\n- **Linux** | _NVIDIA GPUs_ | `pip install onnxruntime-gpu`\n\n- **Linux** | _AMD GPUs_ | Use the ROCm/MIGraphX build (see [ONNX Runtime docs](https://onnxruntime.ai/docs/execution-providers/MIGraphX-ExecutionProvider.html))\n\n```\n\nIf no package is installed, the default `numpy` will be used (neural calculations performed on CPU)\nAny failure conditions will print to the console and we fall back to default `numpy`\nDetection should happen automatically.       ONNX SUPPORT IS NEW AND EXPERIMENTAL\n```\n\n-------------------------------\n\nThe Brain Tool Network Tab displays which backend is being used in (top right)"
  },
  {
    "path": "Docs/neural-network/Experience-Buffer.md",
    "content": "\nThe Experience Buffer is a core component in the system's learning and [neurogenesis](../neural-network/Neurogenesis.md) (neuron creation) module that functions as a memory for recent, significant events. It maintains a time-ordered log of the squid's context and uses this data to identify recurring patterns, which helps the system decide when and how to create new, functionally specialized neurons.\n\n#### How the Experience Buffer Works\nThe experience buffer is technically implemented as the `ExperienceBuffer` class, which operates in two main ways: maintaining a rolling log and tracking pattern recurrence.\n\n1. Rolling Log (deque): The buffer uses a deque (double-ended queue) to store a fixed, limited number of recent experiences (default maximum is 50 experiences). When a new experience is added, the oldest one is automatically discarded, ensuring the buffer only contains the latest, most relevant context.\n\n2. Pattern Tracking: When an experience is added, it is processed to generate three levels of **pattern signatures**, which are counted to track how often similar events occur:\n\n*  **Specific Pattern**: The most detailed signature, identifying a precise combination of `trigger`, `outcome`, and primary motivational neuron state.\n\n*  **Parent Pattern**: A broader pattern used for hierarchical grouping.\n\n*  **Core Pattern**: A minimal pattern used for \"fuzzy matching\" or identifying basic event categories.\n\nThe system analyses these counts to determine if a situation is a novel event or a recurring pattern, which heavily influences whether a new neuron is created, or if an existing neuron is strengthened.\n\n\n\n----------------\n\n#### Example Experiences (ExperienceContext)\n\n<img src=\"https://github.com/user-attachments/assets/ecd998a9-e4b9-44d2-8f37-9a04df58515c\" width=\"300\">\n\nEach recorded experience is captured as an ExperienceContext object, which is a snapshot of the squid's state and environment at the moment a significant event (the trigger) occurs.\n\n* `trigger_type` - The general category of the event: `novelty`, `stress`, or `reward`.\n* `outcome`\t- The result of the experience: `positive`, `negative`, or `neutral`.\n* `active_neurons`\tA dictionary of all current neuron activations (e.g., {`hunger`: 20, `anxiety`: 85}).\n* `recent_actions`\tA list of recent actions taken by the squid (e.g., [`approach_plant`, `hide`]).\n* `environmental_state`\tKey external facts at the time (e.g., {`food_count`: 0, `has_rock`: True}).\n\n\n"
  },
  {
    "path": "Docs/neural-network/Hebbian-Learning.md",
    "content": "The neural network in Dosidicus does not use traditional backpropagation for training. Instead, it employs a form of <strong>Hebbian learning</strong>, a biologically-inspired principle summarized as \"neurons that fire together, wire together.\" This method allows the network to learn associations and patterns organically based on the squid's concurrent states, without requiring a separate training phase. The entire process is managed within the <code>perform_hebbian_learning</code> and <code>update\\_connection</code> methods. </p> <h4>1. The Learning Cycle</h4> <p> Learning is not continuous but occurs in discrete cycles to ensure stability and reduce computational load. </p> <ul> <li> <strong>Timed Trigger:</strong> The learning cycle is initiated by a timer. The interval for this timer is configurable in <code>config.ini</code> under the <code>\\[Hebbian\\]</code> section's <code>learning_interval</code> parameter (default every 30000 milliseconds). </li> <li> <strong>Pre-Pruning:</strong> Before each learning cycle begins, a pruning function is called to remove extremely weak and old connections from the network. This helps maintain network efficiency by clearing out irrelevant pathways before new learning occurs. </li> </ul> <h4>2. The Core Learning Process</h4> <p> The <code>perform_hebbian_learning</code> method executes a precise sequence of steps to update the network's weights. </p> <ol> <li> <strong>Identify Active Neurons:</strong> The system first scans all neurons in the brain. Any neuron whose activation value is above the <code>active_threshold</code> defined in the configuration is considered \"active\" for this learning cycle. System-level neurons (e.g., <code>is_eating</code>, <code>direction</code>) are excluded from this process. </li> <li> <strong>Random Pair Sampling:</strong> If fewer than two neurons are active, the learning cycle is aborted. If there are enough active neurons, the system does <em>not</em> update all possible pairs. Instead, it randomly samples a small number of pairs (e.g., two) from the pool of active neurons. This stochastic approach introduces variability and prevents the network from over-stabilizing into rigid patterns. </li> <li> <strong>Update Connection Weight:</strong> For each selected pair, the <code>update_connection</code> method is called. This is where the core weight calculation happens: <ul> <li> <strong>Base Hebbian Rule:</strong> The fundamental change in weight is calculated by multiplying the two neurons' normalized activation values by a learning rate. This reinforces the connection between them. </li> <li> <strong>Dynamic Learning Rate:</strong> The learning rate is not static. If one of the neurons in the pair was recently created via neurogenesis, the learning rate is temporarily boosted (e.g., by a factor of 2.0). This allows new, specialized neurons to integrate into the network more quickly and form meaningful connections. </li> <li> <strong>Weight Decay:</strong> To prevent runaway weight growth and to help the network \"forget\" insignificant associations, a small decay factor is applied during the update. This factor, configured via <code>weight_decay</code> in <code>config.ini</code>, slightly reduces the magnitude of the connection's weight during each update. </li> <li> <strong>Clamping:</strong> The final calculated weight is always clamped to a range of \\[-1.0, 1.0\\] to keep it normalized and prevent extreme values from destabilizing the network. </li> </ul> </li> </ol> <h4>3. Visual Feedback</h4> <p> The learning process is tied directly to the application's user interface to provide clear, real-time feedback. </p> <ul> <li> <strong>Activity Log:</strong> In the \"Learning\" tab of the Brain Tool, a log entry is created for each learning event, explicitly stating which neuron pair had its connection strengthened or weakened and by how much. </li> <li> <strong>Network Animation:</strong> In the \"Network\" tab, the connection line between the learning pair will briefly glow or pulse. The color of the pulse indicates whether the weight increased (positive reinforcement) or decreased (negative reinforcement), providing an immediate visual cue of the learning event. </li> </ul>"
  },
  {
    "path": "Docs/neural-network/Neurogenesis.md",
    "content": "`neurogenesis.py` is the brains’ stem-cell layer: it decides when, why, and how new neurons appear, makes sure they are immediately functional, keeps the network within size & specialization limits, and cleans up the least useful ones—all through a single, auditable pipeline.\n\n#### Unified neuron creation\n* `create_neuron()` – the only public entry-point used by the UI, BrainWorker, save-load, etc.\n* `create_functional_neuron()` – internal helper that always produces a FunctionalNeuron.\n* All neurons are converted into FunctionalNeuron objects\n\n#### Context-aware experience tracking\n* `ExperienceContext` – a snapshot of why the neuron is being made (trigger type, brain state, environment, outcome, recent actions).\n* `ExperienceBuffer` – rolling FIFO buffer (≈ 50 experiences) that counts how often each specific / parent / core pattern recurs; used to decide when a neuron should actually be spawned.\n* `NeurogenesisTriggerSystem` – state-delta detection (novelty spikes, stress surges, reward rebounds).\n\n#### Functional specialization & wiring\n* Every neuron gets a specialization string (feeding_satisfaction, filth_avoidance, object_investigation, …) derived from the context.\n* `get_functional_connections()` – returns a weighted connection list to existing neurons so the new cell is immediately useful instead of random.\n* `_make_reciprocal_connections()` – guarantees that any outgoing connection ≥ 0.2 gets a matching incoming link so the new neuron can activate/be activated.\n\n#### Placement & visuals\n* `_calculate_functional_position()` – places the neuron near the neurons it will influence, not at random.\n* `_set_neuron_appearance()` – shape (diamond / square / triangle) and color palette encode type and specialization so users can “read” the brain at a glance.\n\n#### Soft & hard limits\n* Per-type caps (max_per_type) – e.g. max 3 stress, 5 novelty, 4 reward.\n* Per-specialization caps (max_per_specialization) – prevents 20 identical “hunger_stress_response” clones.\n* Global neuron cap (max_neurons) – total network size ceiling.\n* Cooldown – minimum seconds between any creation event.\n* Pattern-recurrence thresholds – neuron spawns only after a pattern has repeated 2–5× (depending on specificity).\n\n#### Strengthening instead of duplication\nIf a cap is hit, the system boosts an existing neuron (strength_multiplier, utility_score) rather than creating a redundant one.\n\n#### Pruning & housekeeping\n* `intelligent_pruning()` – removes the lowest-utility neuron that is > 5 min old, considering activation recency, uniqueness of specialization, and total synaptic weight.\n* `_rebuild_new_neurons_details_for_lab()` – guarantees the Laboratory “newest neurogenesis neurons” card always has origin data.\n\n#### State integration & runtime updates\n* `update_neuron_activations()` – every tick, functional neurons compute their value from incoming weights; stress neurons collectively suppress anxiety (bi-directional feedback).\n* Emits pulse animations for weights ≥ 0.15 (can be disabled).\n\n#### Persistence & save/load\n* `to_dict()` / `from_dict()` – serializes the entire `ExperienceBuffer`, every `FunctionalNeuron`, counters, and creation history.\n* `ensure_all_neurons_functional()` – on load, converts any legacy neurons discovered in [brain_widget](../source-reference/brain_widget.py.md) into FunctionalNeuron instances so the system stays unified.\n\n#### Achievement hooks\n`set_achievement_callbacks()` – lets the Achievements module receive “neuron created” and “neuron leveled” events for trophies."
  },
  {
    "path": "Docs/neural-network/Personality.md",
    "content": "Personality affects squid needs and behaviour. A random personality is assigned every time a new squid is born, and could be thought of as a sort of \"difficulty level\" with 'Lazy' being the easiest and 'Stubborn' being hardest.\n\nThere are seven different squid personalities that affect their needs and how they behave: \n\n* `Timid`: Higher chance of becoming anxious \n* `Adventurous`: Increased curiosity and exploration \n* `Lazy`: Slower movement and energy consumption \n* `Energetic`: Faster movement and higher activity levels \n* `Introvert`: Prefers solitude and quiet environments \n* `Greedy`: More focused on food and resources\n* `Stubborn`: Fussy and difficult \n\nOne of these is randomly chosen at launch\n\nA personality type can be forced at launch using the `-p` flag followed by the personality name above (example: `main.py -p lazy`) \n\nEach personality type presents unique challenges and requirements for the player to manage. Understanding and accommodating the specific needs and behaviors of each personality type is crucial for the player's success in caring for the squid and maintaining its well-being.\n\nHere's a description of the different personality types and their corresponding behaviors in the game:\n\n### `Timid` Personality:\n\n* Tendency to become anxious, especially when not near plants.\n* Moves slowly and prefers quiet, solitary environments.\n* Curiosity level is lower than other personalities.\n* Has a higher chance of becoming startled by decorations or other environmental factors.\n\n\n### `Adventurous` Personality:\n\n* Curious and exploratory, with a higher chance of entering the \"curious\" state.\n* Moves faster and is more active compared to other personalities.\n* Curiosity level is higher, leading to increased exploration and interaction with the environment.\n\n\n### `Lazy` Personality:\n\n* Moves and consumes energy at a slower pace.\n* Takes more time to fulfill their needs, such as eating and sleeping.\n* May be less responsive to environmental changes or stimuli.\n\n\n### `Energetic` Personality:\n\n* Moves and acts at a faster pace.\n* Tends to have higher activity levels and may expend energy more quickly.\n* May be more prone to restlessness or agitation.\n\n\n### `Introvert` Personality:\n\n* Prefers solitary environments and is content when alone.\n* May become unhappy or anxious when forced to interact with the environment or decorations.\n* Curiosity level is balanced, not too high or too low.\n\n\n### `Greedy` Personality:\n\n* Highly focused on food and resources, becoming anxious and hungry when those needs are not met.\n* Curiosity level may be higher, leading to more exploration, but this is primarily driven by the desire for food.\n* May become more aggressive or assertive in obtaining food or resources.\n\n\n### `Stubborn` Personality:\n\n* Only eats its favorite food (sushi), often refusing to consume any other type of food (cheese).\n* Displays the message \"Fussy squid does not like that type of food!\" when presented with non-favorite food.\n* Has a chance of refusing to sleep when its sleepiness is high, instead moving randomly.\n* Moves slowly and stubbornly when not actively searching for its favorite food.\n* Prioritizes sushi over cheese in its vision cone when searching for food.\n"
  },
  {
    "path": "Docs/neural-network/STDP.md",
    "content": "### Spike‐Timing‐Dependent Plasticity (STDP)\n\n_As of version 2.6.2.0_ this page has moved to the Wiki [[HERE]](https://github.com/ViciousSquid/Dosidicus/wiki/Spike%E2%80%90Timing%E2%80%90Dependent-Plasticity-(STDP))\n"
  },
  {
    "path": "Docs/neural-network/Technical-Overview.md",
    "content": "<h3>Neural Network Technical Overview</h3> \n\n<img width=\"598\" height=\"296\" alt=\"image\" src=\"https://github.com/user-attachments/assets/da9c7b03-9953-4892-9417-17d429d9a2fe\" />\n\n<p> The system's neural network is a unique, single-layer, fully-connected network architecture that dynamically grows through a process of neurogenesis.\n\nTraditional backpropagation is not used for learning, instead relying on a pure Hebbian model (../neural-network/Hebbian-Learning.md)\n\n...\n\n<h4>1. Core Architecture</h4> <ul><li>The network starts as a single-layer perceptron with 7 core, named neurons. These neurons represent the fundamental emotional and physical states of the squid:\n\n* Circular (Basic Needs): hunger, happiness, cleanliness, sleepiness\n* Square (Complex States): satisfaction, anxiety, curiosity\n\nEach neuron's activation value ranges from 0-100.\n\n Unlike a typical deep learning model, this network is not structured into distinct input, hidden, and output layers. Instead, all neurons exist on a single plane and are fully interconnected. Each connection between two neurons has a weight, initialized with a random value between -1 and 1, which represents the strength and nature (excitatory or inhibitory) of their relationship.</li> </ul> <h4>2. Learning Mechanism: Hebbian Learning</h4> <p> The network updates its connection weights using a Hebbian learning rule, which follows the principle \"neurons that fire together, wire together.\" </p> <ul> <li><strong>Learning Cycle:</strong> The learning process is not continuous but occurs in discrete cycles, triggered by a timer (by default, every 30 seconds).</li> <li><strong>Activation:</strong> During a learning cycle, any neuron whose activation value exceeds a predefined threshold (e.g., &gt; 50) is considered \"active.\".</li> <li><strong>Weight Update:</strong> The system randomly selects a few pairs of currently active neurons. The weight between these pairs is then adjusted according to the Hebbian rule: the change in weight is proportional to the product of the two neurons' activation values multiplied by a learning rate. This strengthens the connection between neurons that are concurrently active.</li> <li><strong>Weight Decay:</strong> To ensure network stability and prevent weights from growing indefinitely, a small weight decay is applied over time, gradually weakening all connections.</li> </ul> <h4>3. Dynamic Architecture: Neurogenesis</h4> <p> The network's most advanced feature is its ability to create new neurons, a process called neurogenesis. This allows the brain's architecture to grow and adapt based on the squid's experiences. </p> <ul> <li> <strong>Triggers:</strong> Neurogenesis is initiated by one of three counters exceeding a set threshold: <ol> <li><strong>Novelty:</strong> Increases when the squid encounters new objects or experiences.</li> <li><strong>Stress:</strong> Increases during stressful events.</li> <li><strong>Reward:</strong> Increases when the squid experiences a positive outcome.</li> </ol> </li> <li> <strong>Creation Process:</strong> When a counter surpasses its threshold and a cooldown period has passed, a new neuron is created. <ul> <li>The neuron is named based on its trigger (e.g., <code>novel\\_0</code>, <code>stress\\_0</code>).</li> <li>It is positioned visually on the network graph near other currently active neurons.</li> <li>Crucially, it is immediately connected to the existing network with a set of default weights. For example, a new 'reward' neuron automatically forms a strong positive connection to 'satisfaction' and 'happiness'.</li> </ul> </li> <li><strong>Dynamic Thresholds:</strong> The thresholds required to trigger neurogenesis are not static. They scale upwards as the network grows in size, preventing runaway neuron creation and promoting stability in a mature network.</li> </ul> <h4>4. Network Stability and Pruning</h4> <p> To manage the complexity of a dynamically growing network, the system employs pruning mechanisms to remove inefficient or irrelevant components. This feature is critical for long-term network health and is enabled by default. </p> <ul> <li><strong>Connection Pruning:</strong> The system can periodically remove connections whose absolute weight falls below a very low threshold, cleaning up insignificant links.</li> <li><strong>Neuron Pruning:</strong> When the network approaches its configured maximum neuron limit, it can trigger the pruning of entire neurons. This process targets newly created (non-core) neurons that have failed to form strong connections or remain largely inactive.</li> </ul>"
  },
  {
    "path": "Docs/neural-network/Vision-System.md",
    "content": "<p> The squid's ability to \"see\" and react to his environment is not based on simple proximity but on a simulated line-of-sight mechanic. This system is composed of two main parts: the <strong>View Cone</strong>, which is an attribute of the squid itself and represents his field of view, and the <strong>Vision Window</strong> (from <code>vision.py</code>), which is a debug tool that provides a first-person visualization of what the squid is currently perceiving.\n\n![image](https://github.com/user-attachments/assets/8bbb87ac-fef9-4093-bb22-a921ff0bc23f)\n\n\n </p> <h4>1. The View Cone: The Mechanism of Sight</h4> <p> The core of the vision system is the \"View Cone.\" This is not a visual effect but an invisible geometric shape (specifically, a <code>QPolygonF</code>) that is mathematically projected from the squid's current position and orientation. </p> <ul> <li> <strong>Dynamic and Directional:</strong> The View Cone is not static. It is recalculated every frame to match the squid's state. When the squid moves, the cone moves with it. More importantly, when the squid turns to face left, right, or up, the cone rotates accordingly. This ensures the squid can only perceive objects that are genuinely in his line of sight. </li> <li> <strong>Object Detection via Intersection:</strong> The system determines what the squid \"sees\" by performing a continuous series of geometric intersection tests. The logic iterates through every object in the environment (decorations, food, poop, etc.) and checks if that object's bounding box intersects with the squid's View Cone polygon. </li> <li> <strong>Informing the Brain:</strong> If an intersection is detected, the object is officially considered \"seen.\" This information is critical as it is fed directly into the squid's <strong>Decision Engine</strong>. For example, if \"food\" is seen, the desire to \"eat\" will likely increase. This allows the squid to make intelligent, context-aware decisions based on his immediate surroundings. The `has_food_visible` and `plant_proximity` inputs feed this system. </li> </ul> <h4>2. The Vision Window (vision.py)</h4> \n\n![image](https://github.com/user-attachments/assets/2d733297-acda-42d9-9a96-7591f1c3de12)\n\n<p> The Vision Window is a powerful debugging tool that renders the output of the View Cone, allowing you to see the world from the squid's perspective. </p> <h5>How It Works</h5> <p> The update_vision() method is the heart of this window and performs the following steps on each refresh: </p> <ol> <li><strong>Clear the Scene:</strong> It first clears its own display to ensure no leftover artifacts from the previous frame.</li> <li><strong>Get the View Cone:</strong> It fetches the squid's current View Cone polygon from the main game logic.</li> <li><strong>Draw the Cone:</strong> It draws a visual representation of the cone shape itself within its window, so you can see the precise boundaries of the squid's perception.</li> <li><strong>Render Seen Objects:</strong> It then performs the same intersection logic as the main decision engine. For every object whose bounding box intersects with the View Cone, it creates a copy of that object's pixmap and draws it inside the Vision Window. It also draws the bounding box of the seen object for clarity.</li> "
  },
  {
    "path": "Docs/source-reference/brain_neuron_hooks.py.md",
    "content": "#### brain_neuron_hooks.py\nThe **sensory input** system that bridges game events to neuron activations. It maintains a registry of handler functions that calculate activation values for input neurons based on the current game state. This is where environmental awareness enters the neural network—when the squid sees food, gets startled, or detects a nearby plant, these hooks translate those game conditions into numerical activations that propagate through the network.\n\n#### Handler Registry:\n\n* Maps neuron names to calculation functions\n* Built-in handlers for: `external_stimulus`, `can_see_food`, `plant_proximity`, `threat_level`, `is_eating`, `is_sleeping`, `is_fleeing`, `is_startled`, `pursuing_food`, `is_sick`\n\n#### Plugin Integration:\n\n* `register_handler(name, callable)` — allows plugins to add custom input neurons\n* `unregister_handler(name)` — removes custom handlers (built-ins protected)\n* Merges plugin handlers with built-in ones at runtime\n\n#### Event Tracking:\n\n* Maintains `event_tracker` dictionary for temporal calculations\n* Tracks window resizes, object spawns, user interactions with decay over time\n* `update_decay()` — decays event intensities each simulation tick\n\n#### Key Method:\n\n* `get_input_neuron_values()` — returns current activation values for all registered input sensors"
  },
  {
    "path": "Docs/source-reference/brain_neuron_outputs.py.md",
    "content": "#### brain_neuron_outputs.py\n**output system** that bridges neuron activations to game behaviours (bindings)\n\nWhen neurons fire above configurable thresholds, this system triggers corresponding game actions (hooks) like fleeing, seeking food, or changing colour. It completes the sensorimotor loop—inputs flow in through hooks, propagate through the network, and outputs emerge here to drive the squid's behaviour.\n\n#### NeuronOutputBinding Dataclass:\n\n* Binds a neuron to an output hook with threshold, trigger mode, and cooldown\n* Trigger modes: `THRESHOLD_RISING`, `THRESHOLD_FALLING`, `THRESHOLD_ABOVE`, `THRESHOLD_BELOW`, `ON_CHANGE`\nSerializable to/from dict for save/load support\n\n#### Standard Output Hooks:\n\n* Movement: `flee`, `seek_food`, `seek_plant`, `approach_rock`, `wander`\n* Actions: `throw_rock`, `pick_up_rock`, `ink_cloud`, `eat`, `change_color`\n* State Changes: `sleep`, `wake`, `startle`, `calm`\n* Stat Modifications: `boost_happiness`, `boost_curiosity`, `reduce_anxiety`\n\n#### NeuronOutputMonitor Class:\n\n* `monitor(activations)` — checks all bindings against current values, fires those meeting conditions\n* Respects cooldown timers to prevent rapid-fire triggering\n* Integrates with plugin system's hook dispatcher\n* Includes floating `NeuronLogWindow` for debugging fired outputs"
  },
  {
    "path": "Docs/source-reference/brain_render_worker.py.md",
    "content": "_Not to be confused with [brain_worker.py](../source-reference/brain_worker.py.md)_\n\n#### brain_render_worker.py (938 lines)\nAn offscreen rendering engine running in its own QThread that paints the entire brain visualization to a QImage buffer. The main thread simply blits this cached image during paintEvent, dramatically improving UI responsiveness. The worker receives immutable state snapshots and handles all the complex drawing logic—connections with animated pulses, neurons with various shapes, localized labels, and visual effects for learning events.\n\n#### RenderState Dataclass:\n\n* Immutable snapshot containing positions, states, colors, weights, and animation parameters\n* Created on main thread via `create_render_state_from_widget()` helper\n* Includes pre-calculated localized neuron labels to avoid i18n lookups during render\n\n#### Rendering Pipeline:\n\n`request_render(state)` — throttled to 10 FPS\n* Draws and animates neurons and connections\n"
  },
  {
    "path": "Docs/source-reference/brain_tool.py.md",
    "content": "#### Responsibilities\n\n* Builds the multi-tab window (Network, Learning, Memory, Decisions, Statistics, …).\n* Hosts the [BrainWidget](../source-reference/brain_widget.py.md) in the “Network” tab.\n* Exposes buttons, sliders, tables to stimulate neurons, export data, change learning rate, force a learning cycle, etc.\n* Persists / loads the whole brain state (weights, positions, neurogenesis history) to JSON.\n* Owns the [BrainWorker](../source-reference/brain_worker.py.md) thread instance (wraps the one inside BrainWidget) and restarts it if it crashes.\n* Bridges between the squid logic ([tamagotchi_logic](../source-reference/tamagotchi_logic.py.md)) and the brain widget:\n\n\n – every few seconds it copies the squid’s current `hunger`, `happiness`, `anxiety`… into the widget’s state so the network mirrors the squid.\n\n – when the network learns new weights, the squid can query them for [decision-making](../engine/Decision-Engine.md).\n\n\n\n#### Key internal objects\n\n* `self.brain_widget` – canvas widget ([brain_widget.py](../source-reference/brain_widget.py.md))\n* `self.tabs` – QTabWidget with all the inspector tabs\n* `self.config_manager` – central place for thresholds, intervals, colours, etc.\n* `self.tamagotchi_logic` – reference to the actual pet simulation (runs in the main game loop)"
  },
  {
    "path": "Docs/source-reference/brain_widget.py.md",
    "content": "### brain_widget.py\nThis is the main neural network visualization and coordination hub. It serves as the central controller that owns the authoritative brain state, coordinates background worker threads, and integrates all the subsystems that make the neural network function. Everything flows through this widget—stat updates from the game, rendering requests, learning signals, and neurogenesis events all converge here before being dispatched to the appropriate handlers.\n\n#### Core State Management:\n\n* Maintains the authoritative `state` dictionary with all neuron activations (hunger, happiness, anxiety, etc.)\n* Manages `weights` dictionary for connection strengths between neurons\n* Tracks `neuron_positions` for [visualization](../brain-tool/Network-Tab.md) layout\n\n#### Worker Coordination:\n\n* Receives an external [`BrainWorker`](../source-reference/brain_worker.py.md) via `set_brain_worker()` (avoids duplicate thread creation)\n* Owns a [`BrainRenderWorker`](../source-reference/brain_render_worker.py.md) for offscreen rendering\n* Coordinates signal/slot connections between workers and UI\n\n#### Subsystems Integrated:\n\n* [`EnhancedNeurogenesis`](../neural-network/Neurogenesis.md) for dynamic neuron creation\n* [`ExperienceBuffer`](../neural-network/Experience-Buffer.md) for tracking learning experiences\n* `EnhancedBrainTooltips` for hover information\n* Theming with animation styles (Vibrant, Subtle, etc.)\n* Brain state bridge for [designer](../brain-tool/Brain-Designer.md) synchronization\n\n#### Key Methods:\n\n* `update_brain_state(stats_dict)` — main entry point for stat updates from the game\n* `set_brain_worker()` — accepts external [worker](../source-reference/brain_worker.py.md) instance\n* `export_brain_state_for_designer()` — syncs state with the Brain Designer tool"
  },
  {
    "path": "Docs/source-reference/brain_worker.py.md",
    "content": "_Not to be confused with [brain_render_worker.py](../source-reference/brain_render_worker.py.md)_\n\n#### brain_worker.py\nA background QThread dedicated to handling computationally expensive brain logic that would otherwise block the UI. It operates on cached snapshots of brain state, processes tasks from a thread-safe queue, and emits results back to the main thread via signals. This separation keeps the simulation responsive even during complex Hebbian learning calculations or neurogenesis evaluations.\n\n#### Task Queue System:\n\n* Uses thread-safe `Queue` for task dispatch\n* Processes three task types: [`neurogenesis`](../neural-network/Neurogenesis.md), [`hebbian`](../neural-network/Hebbian-Learning.md), `state_update`\n* Supports pause/resume for game state changes\n\n#### Neurogenesis Checks:\n\n* Evaluates stress/novelty/reward triggers against configurable thresholds\n* Emits `neurogenesis_result` signal with creation recommendations\n* Handles emergency stress neuron creation when anxiety exceeds 90\n\n#### Hebbian Learning:\n\n* Selects top-k neuron pairs based on co-activation scores\n* Includes anti-loop mechanisms (randomization, cooldown penalties on recently-used pairs)\n* Can create new connections between previously unconnected co-active neurons\n* Emits weight updates back to main thread for application\n\n#### State Decay Processing:\n\n* Applies temporal decay toward baseline for non-input neurons\n* Adds small random noise for organic feel\n* Propagates connection effects through the network"
  },
  {
    "path": "Docs/source-reference/custom_brain_loader.py.md",
    "content": " #### custom_brain_loader.py\nThe brain architecture import/export system that allows custom neural network designs from the Brain Designer tool to be loaded into the live simulation. It handles parsing various brain file formats, applying the architecture to the running BrainWidget, and integrating with the save/load system so custom brains persist across game sessions. This is what makes the Brain Designer's output actually playable.\n\n#### Global State Tracking:\n\n* Tracks currently loaded custom brain name, definition, and source file path\n* `has_custom_brain()` / `get_custom_brain_name()` — API for querying current state\n\n#### BrainLoader Class:\n\n* `show_dialog()` — opens brain selection UI\n* `_parse(raw_data)` — normalizes different brain file formats into consistent structure\n* `_apply(parsed_brain)` — applies positions, weights, and output bindings to live BrainWidget\n* `reset_positions_to_default()` — restores original layout while preserving network structure\n\n#### Save/Load Integration:\n\n* `get_custom_brain_save_data()` — packages custom brain definition for game saves\n* `restore_custom_brain_from_save()` — restores custom brain when loading a save\n* `validate_custom_brain_save()` — checks if a save file's custom brain can be loaded\n* `show_custom_brain_load_warning()` — warns user when loading saves with custom brains\n\n#### BrainSelectDialog:\n\n* File browser UI for .json brain files in the brains folder\n* Shows metadata preview (neuron count, connection count, description)\n* Supports browsing to external files or opening the brains folder"
  },
  {
    "path": "Docs/source-reference/designer_window.py.md",
    "content": "#### designer_window.py\nThis is the primary application shell for the neural network Designer.\n\n#### Core Responsibilities:\n* **Application Orchestration**: Manages the main window, menus, toolbars, and the splitter layout containing the canvas and property panels.\n* **State Management**: Holds the active `BrainDesign` instance and coordinates \"Undo/Redo\" style refreshes across all sub-panels.\n* **Live Game Bridge**: If the game is running, it allows the user to \"Sync from Game\" (import the current brain) or \"Push to Game\" (export the design to the live squid).\n* **Network Generation**: Provides entry points for \"Chaos Mode\" or preset-based automated network generation using the `SparseNetworkGenerator`.\n\n#### Key Functions:\n\n* `setup_ui()` - Initializes the `BrainCanvas` and the vertical tabbed panels (Layers, Sensors, Properties, Connections, Outputs).\n* `push_to_game()` - Converts the visual design into a format the game understands and sends it via the `brain_state_bridge`.\n* `instant_random_generate()` - Triggers an instant \"Chaos\" shuffle of neuron positions and randomizes connections.\n* `load_from_brain_widget_state()` - Allows the editor to be opened mid-game by importing the current state of the squid's brain.\n\n#### Key internal objects:\n`ScrollingTicker`: A specialized UI widget that scrolls rich-text help messages and shortcuts.\n`BrainDesignerWindow`: The `QMainWindow` class that coordinates the canvas, side panels, and the bridge to the live game."
  },
  {
    "path": "Docs/source-reference/main.py.md",
    "content": "### The Simulation Loop (main.py)\nThe simulation loop is driven by a QTimer from the PyQt5 framework, which \"ticks\" at a consistent rate. Each tick represents one frame of the simulation.\n\nThe loop has two key responsibilities:\n\n**State Update**: On each tick, it calls the `update_game_state()` method. This function is responsible for everything that changes over time without direct user input. This includes:\n\n* Decrementing the squid's needs (hunger, cleanliness, etc.).\n\n* Calling the neural network to get the squid's next autonomous action.\n\n* Executing the squid's chosen action (e.g., moving, interacting with an object).\n\n* Updating animations.\n\n**Rendering**: After the state has been updated, the loop tells the GUI to repaint itself, ensuring that the user always sees the most current state of the simulation."
  },
  {
    "path": "Docs/source-reference/memory_manager.py.md",
    "content": "`MemoryManager` is designed to simulate a simple cognitive memory system with two main components:\n\n#### Data Persistence and Initialization\n* File Storage: Memory is persisted in two JSON files within a _memory directory: `ShortTerm.json` and `LongTerm.json`.\n\n* `__init__` Method: Initializes memory paths, loads existing memory using _load_and_convert_timestamps, and sets operational limits:\n\n* `self.short_term_limit` = 50: Maximum number of items in short-term memory (STM).\n\n* `self.short_term_duration` = 300 (5 minutes): The lifespan for a short-term memory item before it's considered for cleanup or transfer.\n\n* `_load_and_convert_timestamps`: A crucial helper method that handles loading memory from JSON. It's robust, attempting to convert string-based _ISO 8601_ timestamps (used for storage) into floating-point Unix timestamps (used for internal operations) to enable easy time-based comparisons.\n* `save_memory`: The counterpart to the loading function. It takes the in-memory list and converts the internal float timestamps back to _ISO 8601_ strings before saving to the JSON file with indentation (indent=4).\n\n-------------------------------\n\n#### Short-Term Memory (STM) Management\nSTM is dynamic, time-limited, and its items have calculated metrics to determine their longevity.\n\n`add_short_term_memory`: Adds a new memory item. If an item with the same category and key already exists, it reinforces the memory by increasing its importance by 0.5 and updating its timestamp.\n\nIt checks for immediate promotion: if importance reaches 3.0, it calls `transfer_to_long_term_memory`.\n\nIt enforces the `self.short_term_limit` by dropping the oldest memory (`pop(0)`) when the limit is exceeded (FIFO - First In, First Out).\n\n`get_short_term_memory`: Retrieves a memory by category and key. It verifies the memory is still within the `self.short_term_duration`. A successful access increases the memory's `access_count`.\n\n`cleanup_short_term_memory`: Removes expired memories (older than `self.short_term_duration`). If the list is still over the limit, it prunes based on a combined score of importance and access_count.\n\n`get_active_memories_data`: Retrieves and formats memories that are still valid, sorting them by a combination of importance and access_count in descending order.\n\n---------------------\n\n#### Long-Term Memory (LTM) Management\nLTM is for permanent or reinforced memories and is primarily concerned with deduplication.\n\n`add_long_term_memory`: Adds a memory. It checks for duplicates based on category and key. If a memory already exists, it is not duplicated; instead, its timestamp is updated to reflect the reinforcement.\n\n`get_all_long_term_memories`: Retrieves all LTM items, with an optional filter by category.\n\n---------------\n\n\n#### Transfer and Review Logic\nThis section defines the \"learning\" or \"consolidation\" process where temporary memories become permanent.\n\n* `review_and_transfer_memories`: The core decay and promotion loop. It iterates through expired STM items.\n\n* If an expired item meets the criteria in `should_transfer_to_long_term`, it's promoted using `transfer_to_long_term_memory`.\n\n* Otherwise, the expired memory is simply removed from the STM.\n\n* `periodic_memory_management`: A simple rate limiter that calls review_and_transfer_memories only if 30 seconds have passed since the last cleanup.\n\n* `should_transfer_to_long_term`: Defines the promotion criteria:\n\n1. importance >= 7 OR\n\n1. access_count >= 3 OR\n\n1. (importance >= 5 AND access_count >= 2)\n\n* `transfer_to_long_term_memory`: Moves a memory from STM to LTM (using `add_long_term_memory` to handle LTM deduplication) and then removes it from the STM.\n\n\n-------------------\n\n#### Utility and Formatting\n* `clear_short_term_memory` / `clear_all_memories`: Provides methods to reset one or both memory stores.\n\n* `update_memory_importance`: Allows external logic to manually adjust the importance of an STM item.\n\n* `format_memory`: A presentation method that takes a memory dictionary and returns an HTML-formatted string, color-coding the memory's interaction type (Positive/Negative/Neutral) based on its raw_value or category/key."
  },
  {
    "path": "Docs/source-reference/neurogenesis_show.py.md",
    "content": "`neurogenesis_show.py` is the **“showman” layer** that wraps the real `EnhancedNeurogenesis` engine and adds **player-facing spectacle** without touching any of the underlying biology, caps, or cooldown logic. The wrapper is enabled by default (via config.ini) and is meant to make neurogenesis more fun/game-like versus biologically accurate. \n\nDisable the wrapper and neurogenesis will behave in a **more scientifically accurate way** (better suited for actual biological/neuro experiments)\n\n#### 1. Guarantee the player *sees* a neuron\n\n- If the real engine **would** create a neuron, this wrapper **always lets it spawn** (respects the same caps/cooldowns).  \n- If the real engine **would NOT** create one, the showman can still **force an extra “dramatic” neuron** when something cool happens—**but only if showmanship is enabled in config**.\n\n\n#### 2. Detect “cool moments”\n\n`NeurogenesisEvent` enum + `_detect_dramatic_moment()`  \nMonitors every experience context for **visually obvious milestones**:\n\n| Event | Typical Threshold |\n|---|---|\n| `ANXIETY_SPIKE` | anxiety ≥ 70 |\n| `CURIOSITY_EXPLOSION` | curiosity ≥ 75 |\n| `HUNGER_SATISFIED` | reward trigger + eating action |\n| `MAX_HAPPINESS` | happiness ≥ 85 |\n| `SPOTLESS_TANK` | cleanliness ≥ 90 |\n| `FIRST_DECORATION_PUSH` | “decoration” or “push” in recent actions |\n\nThese are **intentionally permissive**—the goal is “player smiles,” not “perfect biology.”\n\n\n#### 3. Manual event triggers (achievement integration)\n\n`trigger_event(NeurogenesisEvent.FOO)`  \nLets **external systems** (achievements, tutorials, Easter-egg code) **request** a showman neuron.  \nReturns `True` if one was actually spawned (respects cooldown & config flag).\n\n\n#### 4. Cosmetic upgrades\n\n- **_rename_for_drama()** – replaces auto-generated names like `stress_anxiety_regulation_3` with cinematic ones:  \n  `trauma`, `wonder_trigger`, `satisfaction_burst`, `discovery_rush`, …  \n- **_burst_color()** – gives the newborn neuron a **10-second gold/crimson/mint “super-pulse”** instead of the normal 5-second blink.  \n- **_migrate_neuron()** – atomically renames **every dictionary key, animation tracker, visible set, weight tuple, etc.** so the new pretty name is safe everywhere.\n\n\n#### 5. Achievement callbacks\n\n`set_callbacks(on_dramatic_neuron=…, on_event_triggered=…)`  \nFires when:  \n- a showman neuron is actually spawned, or  \n- any `NeurogenesisEvent` is recorded\n\n#### 6. Config-aware passthrough\n\n- Reads `[Neurogenesis] showmanship = True/False` **each time** (hot-switchable).  \n- When disabled the class becomes a **transparent proxy**: every call falls straight through to the real engine with zero overhead.  \n- Public API surface **mirrors** `EnhancedNeurogenesis` (`create_neuron`, `should_create_neuron`, `get_global_cooldown_remaining`, …) so the rest of the codebase never knows a wrapper exists.\n\n\n#### 7. Bookkeeping\n\n- `last_showman_creation` – 15-second **cosmetic cooldown** (independent of the real engine’s 60-second biological cooldown).  \n- `_triggered_events` set – prevents **exact duplicate** “first” events within the same session.  \n- `reset_events()` – clears history for **new game** or **load save**.\n\n\n### TL;DR\n`neurogenesis_show.py` is the **marketing department** of the neuron factory: it **sprinkles confetti** (dramatic names, longer pulses, extra neurons on cool moments) while **never overruling** the real biologist downstairs—unless the player explicitly turns off the show, in which case it **vanishes silently**."
  },
  {
    "path": "Docs/source-reference/squid.py.md",
    "content": "`squid.py` file defines the Squid class, integrating various functionalities to simulate a living creature. It handles its own visual rendering and animation, calculates its movement, makes decisions based on its internal states and personality, and interacts with objects like food and decorations.\n\n#### Core Responsibilities:\n\n* **Physical Simulation**: Handles movement, animation frames, collision with boundaries, and \"Inking\" behaviors.\n* **Internal State**: Tracks \"Needs\" (Hunger, Happiness, etc.) and \"Goal Neurons\" (Satisfaction, Anxiety, Curiosity).\n* **Interaction Engine**: Manages picking up/throwing rocks, eating food, and reacting to other squids in a multiplayer environment.\n* **Perception**: Acts as the primary interface for the VisionWorker, translating raw geometric vision data into behavioral triggers.\n\n#### Key Functions:\n\n* `update_view_direction()` - Makes the squid \"scan\" the tank; includes a \"Hunger Bias\" that forces it to look toward food.\n* `eat()` - Processes food consumption, applies stat changes, and starts the \"Poop Timer\".\n* `check_boundary_exit()` - Detects if the squid has swum off-screen to transition to a neighbour's tank in multiplayer mode.\n* `startle_awake()` - Handles the logic for a rude awakening, including anxiety spikes and potential ink cloud creation.\n\n#### Key internal objects:\n* `self.mental_state_manager` – boolean flags + cooldowns\n* `self.memory_manager` – short-term & long-term memory lists\n* `self._decision_engine` – Q-learning / weight-based action picker\n* `self.statistics` – personal lifetime counters (age, food eaten, rocks thrown…)"
  },
  {
    "path": "Docs/source-reference/tamagotchi_logic.py.md",
    "content": "View source: [tamagotchi_logic.py](https://github.com/ViciousSquid/Dosidicus/blob/2.6.1.2_LatestVersion/src/tamagotchi_logic.py) version 2.6.1.2\n\nA **god-object** serving as the central game logic controller. Its primary purpose is to manage the core simulation loop, handle the squid's behaviour and needs, facilitate interactions with the environment, and integrate various game systems, including the neural network, memory, and save/load functionalities.\n\n\n#### Responsibilities\n* Owns the live pet simulation loop (`hunger`, `happiness`, `sickness`, `sleep`, etc.).\n* Spawns and manages world objects: food, poop, decorations, rocks.\n* Runs every-frame update (movement, collisions, timers, cooldowns).\n* Handles user actions: feed, clean, medicine, speed changes, window resize.\n* Coordinates save / load of the entire game state (squid, memories, decorations, brain).\n* Hosts the [plugin](../engine/Plugin-System.md) system (achievements, multiplayer, etc.) and fires hooks.\n* Owns statistics & scoring (distance swam, food eaten, startles, ink clouds…).\n* Bridges pet ← → brain:\n1. – copies squid stats into the neural network so the network mirrors the pet.\n2. – reads learned weights back from the network to influence future decisions.\n\n\n#### Key internal objects\n* `self.squid` – the pet instance ([squid.py](../source-reference/squid.py.md))\n* `self.brain_window` – the debug UI ([brain_tool.py](../source-reference/brain_tool.py.md))\n* `self.food_items` / `poop_items` / `rock_items` – lists of QGraphicsItems\n* `self.neurogenesis_triggers` – counters that tell the brain when to grow new neurons\n* `self.plugin_manager` – loads & runs plugins (multiplayer, achievements, …)\n\n\n----------------------------------\n\n* `TamagotchiLogic.__init__()` constructs the Squid and keeps a reference.\n* Squid receives a back-reference (`self.tamagotchi_logic`) so it can:\n1. – ask for nearby decorations / food\n2. – tell the logic when it threw a rock (for RL reward)\n3. – trigger plugin hooks\n\n#### Data flow every frame:\n* `TamagotchiLogic.update_simulation`()\n* → calls `squid.move_squid`()\n* → copies `squid.hunger` / `happiness` / `anxiety` … into a dict\n* → sends dict to `brain_window.update_brain`() (neural network)\n* Neural network learns, may create new neurons, returns updated weights.\n* `TamagotchiLogic` reads those weights and/or calls `squid.make_decision`() which uses them.\n* Save / load\n* `TamagotchiLogic.save_game`() asks `squid.save_state`() for the pet slice, then bundles it with brain data, memories, decorations, achievements.\nOn load the reverse happens; afterwards `sync_state_from_squid`() is called so the squid remains the single source of truth for core stats.\n\n#### Plugin hooks:\nAny time a Squid property changes (via its setters) it fires tamagotchi_logic.plugin_manager.trigger_hook(\"on_hunger_change\", …)\nso plugins (achievements, multiplayer, etc.) can react."
  },
  {
    "path": "Docs/source-reference/vision_worker.py.md",
    "content": "#### vision_worker.py\nThis is the computational engine of the squid's perception. It runs as a background thread (QThread) to ensure that geometric calculations do not \"freeze\" the main game animations.\n\n#### Core Responsibilities:\n* **Asynchronous Calculation**: Performs vision cone and proximity checks at a fixed frequency (20Hz) **independently of the main game loop**.\n* **Vision Cone Logic**: Determines if objects (food, rocks, plants) are within the squid's 80-degree field of view by calculating the angular difference between the squid's gaze and the object's position.\n* **Proximity Sensing**: Calculates \"tactile\" proximity to plants. Unlike the vision cone, this uses bounding-box edge distances to detect if the squid is touching or near a plant (0–100 range).\n* **Change Detection**: It monitors the environment and only \"alerts\" the squid via signals when something meaningful changes (e.g., food just appeared in view).\n\n#### Key Functions:\n\n* `_calculate_visibility()` - The math core; checks every scene object against the vision cone and calculates distances.\n* `update_squid_state()` - Receives the squid's current X/Y and gaze angle from the main thread.\n* `update_scene_objects()` - Receives a lightweight list of all items currently in the tank.\n* `_check_and_emit_changes()` - Fires signals like `food_visibility_changed` or `plant_proximity_changed` only when state shifts.\n\n#### Key internal objects\n\n* `SquidVisionState`: A data class containing a snapshot of the squid's position, size, and gaze angle.\n* `SceneObject`: A lightweight representation of a world object (food, plant, rock) used for visibility checks.\n* `VisionResult`: A container for the output of a vision cycle, listing visible items and proximity values.\n* `VisionWorker`: The QThread subclass that constantly calculates what is inside the vision cone.\n\n\n----------------------\n\n#### \"Producer-Consumer\" model:\n\n1.  **Input**: [`Squid.py`](../source-reference/squid.py.md) periodically sends its position to the `VisionWorker`.\n2.  **Processing**: `VisionWorker` (in the background) identifies what is visible and sends a `VisionResult` back to the Squid.\n3.  **Behaviour**: The Squid uses that result to decide if it should chase food or feel calm near a plant.\n4.  **Display**: If the VisionWindow is open, it reads those same results to display the \"Visible Objects\" list to the user.\n\n* **Note:** The VisionWorker includes a \"Circuit Breaker\" logic to prevent the squid from chasing \"ghost food\" that was just eaten but hasn't been cleared from the background thread's cache yet."
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README.md",
    "content": "\n_\"What if a Tamagotchi had a neural network and could learn stuff?\"_ - [Gigazine](https://gigazine.net/gsc_news/en/20250505-dosidicus-electronicae/) , [Hackaday](https://hackaday.com/2025/04/26/digital-squids-behavior-shaped-by-neural-network/)\n\n<p align=\"left\">\n  <img src=\"https://img.shields.io/badge/AI-Neural_Network-9C27B0?style=flat&logo=mindmeister&logoColor=white\" height=\"20\" alt=\"AI\">\n  <img src=\"https://img.shields.io/badge/License-GPL_v2-blue.svg?style=flat\" height=\"20\" alt=\"GPL-2.0\">\n  <img src=\"https://img.shields.io/badge/Translations-7-228B22?style=flat&logo=google-translate&logoColor=white&labelColor=333333\" height=\"20\" alt=\"Translations\">\n    <a href=\"https://buymeacoffee.com/vicioussquid\"><img src=\"https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black\" height=\"20\" alt=\"Buy Me A Coffee\"></a>\n</p>\n\n# _Dosidicus electronicus_\n\n_A transparent cognitive sandbox disguised as a digital pet squid with a neural network you can **see thinking**_\n\n- Part **educational neuro tool**, part **sim game**, part **fever dream**\n- A unique intersection of 1990s retro-gaming aesthetic and modern computational neuroscience.\n- [Build-your-own neural network](https://github.com/ViciousSquid/Dosidicus/wiki/Brain-Designer) - learn how an NN works  by **raising one as a pet**\n\n### Compiled binaries for Windows, Mac and Linux: [see Releases](https://github.com/ViciousSquid/Dosidicus/releases) page\n\n```bash\ncurl -sSL https://raw.githubusercontent.com/ViciousSquid/Dosidicus/2.6.2.0_LatestVersion/linux_setup.sh | bash\n```\n\n<img width=\"2482\" height=\"980\" alt=\"image\" src=\"https://github.com/user-attachments/assets/02119926-47f7-4bfb-96b9-457d470064e4\" />\n<img src=\"https://github.com/user-attachments/assets/496cec0d-0810-4f47-8618-11165e0dd50d\" width=\"380\">\n\n---\n\n## [Manifesto](https://github.com/ViciousSquid/Dosidicus/wiki/Cognitive-Sandbox-Manifesto-%7C-Artificial-Life-and-Transparent-Neural-Systems) | [Wiki](https://github.com/ViciousSquid/Dosidicus/wiki) | [Changelog](https://github.com/ViciousSquid/Dosidicus/wiki/changelog)\n\n---\n\n## **Myth & Mechanism**\n\nDosidicus is a digital squid born with a randomly wired brain.\n\nFeed him, stimulate neurons, watch him learn.\n\n- He starts with 8 neurons.\n- He grows new structure via **neurogenesis** and rewires using **Hebbian learning**\n- He forms memories.\n- He develops quirks.\n\nEvery squid is different.\nEvery save file is a cognitive history.\n\n#### Under the hood runs [**STRINg** simulation engine](https://github.com/ViciousSquid/Dosidicus/wiki/Engine-overview):\n\n* Built from scratch in NumPy\n* No TensorFlow. No PyTorch. No NEAT.\n* Fully visible neuron activations\n* Structural growth over time\n* Dual memory system\n* Headless training mode\n* Most AI is a black box: Dosidicus lets you see the mind forming - every neuron is visible, stimulatable, understandable.\n\nThe squid serves as a digital pioneer in our quest to understand the mechanisms of thought and the evolution of autonomy in a synthetic world.\n\nWant the full conceptual philosophy behind Dosidicus? Read the [Cognitive Sandbox Manifesto](https://github.com/ViciousSquid/Dosidicus/wiki/Cognitive-Sandbox-Manifesto-%7C-Artificial-Life-and-Transparent-Neural-Systems)\n\n---\n\n## Share Your Squid\n\nNo two squids are wired the same.\n\nEarly interactions permanently alter their structure.\nTiny differences amplify.\nHabits form. Fears emerge. Personalities drift.\n\nYour squid's brain is a cognitive history - shaped by you.\n\nSo share it.\n\n- Export save files and let others explore your squid's neural structure.\n- Post screenshots of strange activation patterns and unexpected growth.\n- Show bizarre learned behaviors (Why is yours afraid of poop?)\n- Compare cognitive histories and trace how experience shaped structure.\n\n- Did yours grow 40 neurons?\n- Did it develop a persistent avoidance loop?\n- Did you accidentally create a neurotic reward spiral?\n\nEvery squid is an experiment.\n\n---\n\n## Docker\n\nTwo targets are provided: `headless` (CLI trainer) and `gui` (PyQt5 app with X11).\n\nHeadless (recommended for containers):\n```bash\ndocker build -t dosidicus:headless --target headless .\ndocker run --rm -v ${PWD}/headless_output:/app/output dosidicus:headless --ticks 10000 --output /app/output/trained_brain.json\n```\n\nGUI (Linux host with X11 or WSLg):\n```bash\ndocker build -t dosidicus:gui --target gui .\ndocker run --rm \\\n  -e DISPLAY=$DISPLAY \\\n  -e QT_X11_NO_MITSHM=1 \\\n  -v /tmp/.X11-unix:/tmp/.X11-unix:rw \\\n  -v ${PWD}/saves:/app/saves \\\n  -v ${PWD}/logs:/app/logs \\\n  dosidicus:gui\n```\n\nCompose:\n```bash\ndocker compose up --build\ndocker compose --profile gui up --build\n```\n\nWSLg note: If the GUI fails to start with a Qt platform plugin error, try:\n```bash\nexport QT_QPA_PLATFORM=wayland\ndocker compose --profile gui up --build\n```\n\nNote: On Windows without WSLg, you will need an X server and a valid `DISPLAY` value to run the GUI container.\n\nNote: Attempting to build the Docker container on Windows ARM64 will fail because there is no pyqt5 wheel [[32]](https://github.com/ViciousSquid/Dosidicus/pull/32) -  Use the prebuilt binary from [releases](https://github.com/ViciousSquid/Dosidicus/releases/tag/v2.6.2.0) instead\n\nTroubleshooting (quick):\n- If `DISPLAY` is empty in WSL: WSLg is not active. Use WSLg or run an X server on Windows.\n- If Docker errors mention `docker_engine`/pipe not found: start Docker Desktop and ensure WSL integration is enabled.\n- If GUI still exits with Qt plugin errors: rebuild the image (`docker compose --profile gui build --no-cache`) and retry.\n\n---\n\n## Project Overview\n\n-  41,636 lines, one developer, 28 months, GPL 2.0 license\n\n- **Dependencies:**\n  - Python ^3.9\n  - PyQt5 ^5.15 (GUI framework)\n  - numpy ^1.21 (neural network computations)\n  - **OPTIONAL** onnxruntime or onnxruntime-directml ([more info](https://github.com/ViciousSquid/Dosidicus/wiki/AI-accelerator-support))\n- **Core Structure:** Modular codebase in `src/` including brain designer, decision engine, learning algorithms, personality traits, memory management, UI components, and interaction systems. Entry point via `main.py`.\n\n### Key Project Components\n- **Plugin System:** Extensible architecture with built-in plugins for achievements (tracking milestones) and multiplayer (networked interactions).\n- **Save System:** Persistent saves in `saves/` for pet states, autosaves, and achievement logs.\n- **Headless Mode:** Standalone training and simulation in `headless/` for GUI-less operation, ideal for background training or server environments (experimental)\n- **Custom Brains:** Library of pre-configured neural networks in `custom_brains/` (e.g., \"Plant-Seeker\", \"Insomniac\") for quick behavior setup.\n- **Memory Management:** Dual memory system (`_memory/`) with long-term and short-term storage for learning persistence.\n- **Examples and Tools:** Example squids, configuration files (`config.ini`), and version tracking.\n\n---\n\n### A year ago I got a **tattoo of this project** to celebrate its first development milestone!\n\n<img src=\"https://github.com/user-attachments/assets/fe50e8d8-cb76-4b20-830a-ea6af28bb608\" width=\"250\">\n\n---\n\n![Visitors](https://api.visitorbadge.io/api/visitors?path=ViciousSquid&label=UNIQUE%20VISITORS&countColor=%2326313f&style=flat)\n"
  },
  {
    "path": "config.ini",
    "content": "[General]\r\nlanguage = en\r\n\r\n[Debug]\r\nmultiplayer_debug = False\r\n\r\n[Compute]\r\nbackend = numpy\r\n\r\n[Display]\r\nneuron_label_font_size = 8\r\nneuron_radius = 14\r\nconnection_line_width = 1.5\r\nbutton_font_size = 16\r\nbutton_width = 140\r\nbutton_height = 50\r\nbutton_spacing = 20\r\n\r\n[RockInteractions]\r\npickup_probability = 0.7\r\nthrow_probability = 0.4\r\nmin_carry_duration = 4.0\r\nmax_carry_duration = 10.0\r\ncooldown_after_throw = 10.0\r\nhappiness_boost = 9\r\nsatisfaction_boost = 11\r\nanxiety_reduction = 9\r\nmemory_decay_rate = 0.98\r\nmax_rock_memories = 10\r\n\r\n[Neurogenesis]\r\nenabled = True\r\nshowmanship = True\r\npruning_enabled = True\r\ncooldown = 60.0\r\nper_type_cooldown = 30.0\r\nmax_novelty_neurons = 5\r\npattern_threshold = 3\r\nexperience_buffer_size = 50\r\nmin_utility_for_keep = 0.2\r\nmax_neurons = 128\r\ninitial_neuron_count = 7\r\nmax_hebbian_pairs = 2\r\n\r\n[Neurogenesis.Novelty]\r\nenabled = True\r\nthreshold = 3.0\r\ndecay_rate = 0.85\r\nmax_counter = 10.0\r\nmin_curiosity = 0.3\r\nadventurous_modifier = 1.2\r\ntimid_modifier = 0.8\r\n\r\n[Neurogenesis.Stress]\r\nenabled = True\r\nthreshold = 2.0\r\ndecay_rate = 0.85\r\nmax_counter = 10.0\r\nmin_anxiety = 0.4\r\ntimid_modifier = 1.5\r\nenergetic_modifier = 0.7\r\n\r\n[Neurogenesis.Reward]\r\nenabled = True\r\nthreshold = 2.5\r\ndecay_rate = 0.85\r\nmax_counter = 8.0\r\nmin_satisfaction = 0.5\r\nboost_multiplier = 1.1\r\n\r\n[Neurogenesis.SpecialisationCaps]\r\nnovelty_object_investigation = 8\r\n\r\n[Neurogenesis.NeuronProperties]\r\nbase_activation = 0.5\r\nposition_variance = 75\r\ndefault_connections = True\r\nconnection_strength = 0.35\r\nreciprocal_strength = 0.15\r\nrandomize_start_positions = True\r\ncanvas_padding = 60\r\ncentering_force = 0.02\r\nforce_bounds = True\r\n\r\n[Neurogenesis.VisualEffects]\r\nhighlight_duration = 5.0\r\nhighlight_radius = 40\r\npulse_effect = True\r\npulse_speed = 0.5\r\nanimation_style = pattern_1\r\n\r\n[Neurogenesis.Appearance]\r\nnovelty_color = 255,255,150\r\nstress_color = 255,150,150\r\nreward_color = 150,255,150\r\nnovelty_shape = triangle\r\nstress_shape = square\r\nreward_shape = circle\r\n\r\n[Hebbian]\r\nlearning_interval = 30\r\nbase_learning_rate = 0.12\r\nweight_decay = 0.01\r\nmin_weight = -1.0\r\nmax_weight = 1.0\r\nmax_hebbian_pairs = 2\r\n\r\n[LinkBlink]\r\ninterval_min = 8.0\r\ninterval_max = 20.0\r\nblink_duration = 2.0\r\n\r\n[Animation]\r\nstyle = vibrant\r\n\r\n[Designer]\r\ndesigner_min_neuron_distance = 90\r\ndesigner_max_neuron_distance = 200\r\n\r\n[Facts]\r\nenabled = True\r\ninterval_minutes = 6\r\ndisplay_seconds = 12\r\n\r\n[Sleep]\r\nrecovery_per_second = 28.0\r\nhappiness_boost = 0.45\r\nsatisfaction_boost = 0.35\r\nsink_speed = 0.58\r\nbottom_padding = 25\r\nbob_amount = 1.2\r\n\r\n\r\n"
  },
  {
    "path": "custom_brains/Bathtub.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Bathtub\",\r\n    \"description\": \"\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"10-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        392.0,\r\n        63.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        260.0,\r\n        151.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"sleepiness->anxiety\": 0.125,\r\n    \"hunger->satisfaction\": -0.344,\r\n    \"cleanliness->happiness\": 0.296,\r\n    \"sleepiness->happiness\": -0.355,\r\n    \"can_see_food->hunger\": 0.25,\r\n    \"happiness->satisfaction\": 0.164,\r\n    \"anxiety->curiosity\": -0.375,\r\n    \"sleepiness->curiosity\": -0.17,\r\n    \"satisfaction->hunger\": -0.064,\r\n    \"curiosity->happiness\": 0.299,\r\n    \"satisfaction->anxiety\": -0.34,\r\n    \"can_see_food->curiosity\": 0.191,\r\n    \"anxiety->happiness\": -0.418,\r\n    \"hunger->happiness\": -0.189,\r\n    \"happiness->anxiety\": -0.228,\r\n    \"anxiety->satisfaction\": -0.13,\r\n    \"satisfaction->happiness\": 0.364,\r\n    \"anxiety->sleepiness\": 0.096\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      392.0,\r\n      63.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      260.0,\r\n      151.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"sleepiness|anxiety\": 0.125,\r\n    \"hunger|satisfaction\": -0.344,\r\n    \"cleanliness|happiness\": 0.296,\r\n    \"sleepiness|happiness\": -0.355,\r\n    \"can_see_food|hunger\": 0.25,\r\n    \"happiness|satisfaction\": 0.164,\r\n    \"anxiety|curiosity\": -0.375,\r\n    \"sleepiness|curiosity\": -0.17,\r\n    \"satisfaction|hunger\": -0.064,\r\n    \"curiosity|happiness\": 0.299,\r\n    \"satisfaction|anxiety\": -0.34,\r\n    \"can_see_food|curiosity\": 0.191,\r\n    \"anxiety|happiness\": -0.418,\r\n    \"hunger|happiness\": -0.189,\r\n    \"happiness|anxiety\": -0.228,\r\n    \"anxiety|satisfaction\": -0.13,\r\n    \"satisfaction|happiness\": 0.364,\r\n    \"anxiety|sleepiness\": 0.096\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        392.0,\r\n        63.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        260.0,\r\n        151.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Change_colour_when_see_food.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Change_colour_when_see_food\",\r\n    \"description\": \"Green when food is visible / red when not\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"novelty_object_investigation\": {\r\n      \"position\": [\r\n        515.2635423006693,\r\n        185.04889429904628\r\n      ],\r\n      \"type\": \"hidden\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"cleanliness->happiness\": 0.141,\r\n    \"satisfaction->happiness\": 0.37,\r\n    \"can_see_food->satisfaction\": 0.24,\r\n    \"can_see_food->hunger\": 0.29,\r\n    \"curiosity->satisfaction\": 0.212,\r\n    \"satisfaction->curiosity\": 0.36463690000000004,\r\n    \"novelty_object_investigation->hunger\": -0.7280000000000001,\r\n    \"novelty_object_investigation->happiness\": 0.5152000000000035,\r\n    \"novelty_object_investigation->satisfaction\": 0.8,\r\n    \"novelty_object_investigation->curiosity\": 0.7,\r\n    \"novelty_object_investigation->anxiety\": -0.4,\r\n    \"hunger->novelty_object_investigation\": -0.7280000000000001,\r\n    \"happiness->novelty_object_investigation\": 0.5152000000000035,\r\n    \"satisfaction->novelty_object_investigation\": 0.98308,\r\n    \"curiosity->novelty_object_investigation\": 0.7,\r\n    \"anxiety->novelty_object_investigation\": -0.4,\r\n    \"can_see_food->can_see_food\": -0.8,\r\n    \"novelty_object_investigation->novelty_object_investigation\": -0.8\r\n  },\r\n  \"state\": {\r\n    \"can_see_food\": false,\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"novelty_object_investigation\": 0.0\r\n  },\r\n  \"neuron_shapes\": {\r\n    \"can_see_food\": \"square\",\r\n    \"hunger\": \"circle\",\r\n    \"happiness\": \"circle\",\r\n    \"cleanliness\": \"circle\",\r\n    \"sleepiness\": \"circle\",\r\n    \"satisfaction\": \"circle\",\r\n    \"anxiety\": \"circle\",\r\n    \"curiosity\": \"circle\",\r\n    \"novelty_object_investigation\": \"diamond\"\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [\r\n    {\r\n      \"neuron_name\": \"can_see_food\",\r\n      \"output_hook\": \"neuron_output_change_color\",\r\n      \"threshold\": 90.0,\r\n      \"trigger_mode\": \"above\",\r\n      \"cooldown\": 1.0,\r\n      \"enabled\": true,\r\n      \"hook_params\": {\r\n        \"red\": 170,\r\n        \"green\": 255,\r\n        \"blue\": 127\r\n      }\r\n    },\r\n    {\r\n      \"neuron_name\": \"can_see_food\",\r\n      \"output_hook\": \"neuron_output_change_color\",\r\n      \"threshold\": 40.0,\r\n      \"trigger_mode\": \"below\",\r\n      \"cooldown\": 1.0,\r\n      \"enabled\": true,\r\n      \"hook_params\": {\r\n        \"red\": 255,\r\n        \"green\": 255,\r\n        \"blue\": 255\r\n      }\r\n    }\r\n  ],\r\n  \"neuron_positions\": {\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ],\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"novelty_object_investigation\": [\r\n      515.2635423006693,\r\n      185.04889429904628\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"cleanliness|happiness\": 0.141,\r\n    \"satisfaction|happiness\": 0.37,\r\n    \"can_see_food|satisfaction\": 0.24,\r\n    \"can_see_food|hunger\": 0.29,\r\n    \"curiosity|satisfaction\": 0.212,\r\n    \"satisfaction|curiosity\": 0.36463690000000004,\r\n    \"novelty_object_investigation|hunger\": -0.7280000000000001,\r\n    \"novelty_object_investigation|happiness\": 0.5152000000000035,\r\n    \"novelty_object_investigation|satisfaction\": 0.8,\r\n    \"novelty_object_investigation|curiosity\": 0.7,\r\n    \"novelty_object_investigation|anxiety\": -0.4,\r\n    \"hunger|novelty_object_investigation\": -0.7280000000000001,\r\n    \"happiness|novelty_object_investigation\": 0.5152000000000035,\r\n    \"satisfaction|novelty_object_investigation\": 0.98308,\r\n    \"curiosity|novelty_object_investigation\": 0.7,\r\n    \"anxiety|novelty_object_investigation\": -0.4,\r\n    \"can_see_food|can_see_food\": -0.8,\r\n    \"novelty_object_investigation|novelty_object_investigation\": -0.8\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\",\r\n      \"shape\": \"square\"\r\n    },\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"novelty_object_investigation\": {\r\n      \"name\": \"novelty_object_investigation\",\r\n      \"neuron_type\": \"hidden\",\r\n      \"position\": [\r\n        515.2635423006693,\r\n        185.04889429904628\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"diamond\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Dense_connections.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Dense connections\",\r\n    \"description\": \"Many many young connections\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"22-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        95.0,\r\n        46.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"is_custom\": false,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"happiness->anxiety\": -0.2,\r\n    \"can_see_food->curiosity\": 0.27,\r\n    \"sleepiness->curiosity\": -0.309,\r\n    \"can_see_food->anxiety\": -0.137,\r\n    \"hunger->can_see_food\": -0.035,\r\n    \"satisfaction->anxiety\": -0.216,\r\n    \"happiness->satisfaction\": 0.32,\r\n    \"cleanliness->satisfaction\": 0.035,\r\n    \"hunger->anxiety\": 0.333,\r\n    \"hunger->curiosity\": 0.204,\r\n    \"happiness->curiosity\": 0.227,\r\n    \"happiness->can_see_food\": -0.297,\r\n    \"cleanliness->anxiety\": -0.141,\r\n    \"satisfaction->curiosity\": 0.187,\r\n    \"cleanliness->happiness\": 0.391,\r\n    \"anxiety->can_see_food\": 0.206,\r\n    \"anxiety->happiness\": -0.229,\r\n    \"satisfaction->cleanliness\": -0.207,\r\n    \"curiosity->hunger\": -0.142,\r\n    \"curiosity->happiness\": 0.082,\r\n    \"sleepiness->anxiety\": -0.025,\r\n    \"can_see_food->hunger\": 0.347,\r\n    \"curiosity->sleepiness\": 0.084,\r\n    \"anxiety->curiosity\": -0.309,\r\n    \"anxiety->satisfaction\": 0.257,\r\n    \"happiness->cleanliness\": -0.187,\r\n    \"satisfaction->happiness\": -0.313,\r\n    \"curiosity->satisfaction\": -0.173,\r\n    \"hunger->satisfaction\": -0.42,\r\n    \"anxiety->sleepiness\": 0.197,\r\n    \"anxiety->hunger\": -0.239,\r\n    \"happiness->sleepiness\": -0.025,\r\n    \"hunger->happiness\": -0.131,\r\n    \"curiosity->can_see_food\": -0.256,\r\n    \"sleepiness->satisfaction\": -0.229,\r\n    \"anxiety->cleanliness\": 0.156,\r\n    \"satisfaction->hunger\": 0.046,\r\n    \"sleepiness->happiness\": -0.246,\r\n    \"can_see_food->happiness\": 0.232,\r\n    \"curiosity->anxiety\": -0.037,\r\n    \"satisfaction->sleepiness\": 0.066\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"neuron_shapes\": {\r\n    \"hunger\": \"circle\",\r\n    \"happiness\": \"circle\",\r\n    \"cleanliness\": \"circle\",\r\n    \"sleepiness\": \"circle\",\r\n    \"satisfaction\": \"circle\",\r\n    \"anxiety\": \"circle\",\r\n    \"curiosity\": \"circle\",\r\n    \"can_see_food\": \"square\"\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      95.0,\r\n      46.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"happiness|anxiety\": -0.2,\r\n    \"can_see_food|curiosity\": 0.27,\r\n    \"sleepiness|curiosity\": -0.309,\r\n    \"can_see_food|anxiety\": -0.137,\r\n    \"hunger|can_see_food\": -0.035,\r\n    \"satisfaction|anxiety\": -0.216,\r\n    \"happiness|satisfaction\": 0.32,\r\n    \"cleanliness|satisfaction\": 0.035,\r\n    \"hunger|anxiety\": 0.333,\r\n    \"hunger|curiosity\": 0.204,\r\n    \"happiness|curiosity\": 0.227,\r\n    \"happiness|can_see_food\": -0.297,\r\n    \"cleanliness|anxiety\": -0.141,\r\n    \"satisfaction|curiosity\": 0.187,\r\n    \"cleanliness|happiness\": 0.391,\r\n    \"anxiety|can_see_food\": 0.206,\r\n    \"anxiety|happiness\": -0.229,\r\n    \"satisfaction|cleanliness\": -0.207,\r\n    \"curiosity|hunger\": -0.142,\r\n    \"curiosity|happiness\": 0.082,\r\n    \"sleepiness|anxiety\": -0.025,\r\n    \"can_see_food|hunger\": 0.347,\r\n    \"curiosity|sleepiness\": 0.084,\r\n    \"anxiety|curiosity\": -0.309,\r\n    \"anxiety|satisfaction\": 0.257,\r\n    \"happiness|cleanliness\": -0.187,\r\n    \"satisfaction|happiness\": -0.313,\r\n    \"curiosity|satisfaction\": -0.173,\r\n    \"hunger|satisfaction\": -0.42,\r\n    \"anxiety|sleepiness\": 0.197,\r\n    \"anxiety|hunger\": -0.239,\r\n    \"happiness|sleepiness\": -0.025,\r\n    \"hunger|happiness\": -0.131,\r\n    \"curiosity|can_see_food\": -0.256,\r\n    \"sleepiness|satisfaction\": -0.229,\r\n    \"anxiety|cleanliness\": 0.156,\r\n    \"satisfaction|hunger\": 0.046,\r\n    \"sleepiness|happiness\": -0.246,\r\n    \"can_see_food|happiness\": 0.232,\r\n    \"curiosity|anxiety\": -0.037,\r\n    \"satisfaction|sleepiness\": 0.066\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        95.0,\r\n        46.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\",\r\n      \"shape\": \"square\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"custom_neurons\": [],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Feed-Forward-Hidden-Layer.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Feed-Forward-Hidden-Layer\",\r\n    \"description\": \"Hidden layer of 4 neurons, improves learning\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.1\",\r\n    \"created\": \"18-12-25\",\r\n    \"modified\": \"22-12-25\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        40.0,\r\n        50\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        220.0,\r\n        50\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        400.0,\r\n        50\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        580.0,\r\n        50\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        220.0,\r\n        350\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        400.0,\r\n        350\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        580.0,\r\n        350\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        760.0,\r\n        50\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"is_custom\": false,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hidden0_0\": {\r\n      \"position\": [\r\n        130.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"custom\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hidden0_1\": {\r\n      \"position\": [\r\n        310.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"custom\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hidden0_2\": {\r\n      \"position\": [\r\n        490.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"custom\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hidden0_3\": {\r\n      \"position\": [\r\n        670.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"custom\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"hunger->hidden0_0\": 0.15527318685678937,\r\n    \"hunger->hidden0_1\": -0.32662200356313464,\r\n    \"hunger->hidden0_2\": 0.3690305534442575,\r\n    \"hunger->hidden0_3\": 0.08771704005552605,\r\n    \"happiness->hidden0_0\": 0.43563670388631415,\r\n    \"happiness->hidden0_1\": 0.3747222471549809,\r\n    \"happiness->hidden0_2\": 0.21675935283830716,\r\n    \"happiness->hidden0_3\": 0.30553842478041715,\r\n    \"cleanliness->hidden0_0\": -0.45817893703227386,\r\n    \"cleanliness->hidden0_1\": -0.3924017649498124,\r\n    \"cleanliness->hidden0_2\": 0.34017744816296347,\r\n    \"cleanliness->hidden0_3\": 0.22903537539505892,\r\n    \"sleepiness->hidden0_0\": 0.42053087299722425,\r\n    \"sleepiness->hidden0_1\": -0.11093503092117496,\r\n    \"sleepiness->hidden0_2\": 0.0022831860748842026,\r\n    \"sleepiness->hidden0_3\": -0.46929846779909523,\r\n    \"can_see_food->hidden0_0\": 0.49996147166880156,\r\n    \"can_see_food->hidden0_1\": 0.12164356013940125,\r\n    \"can_see_food->hidden0_2\": 0.3932867930701862,\r\n    \"can_see_food->hidden0_3\": -0.2591350891409684,\r\n    \"hidden0_0->satisfaction\": 0.3553682697768865,\r\n    \"hidden0_0->anxiety\": 0.4303336060210178,\r\n    \"hidden0_0->curiosity\": -0.10878219621490581,\r\n    \"hidden0_1->satisfaction\": -0.17268262288844005,\r\n    \"hidden0_1->anxiety\": -0.21608691390489854,\r\n    \"hidden0_1->curiosity\": -0.42810633830239697,\r\n    \"hidden0_2->satisfaction\": -0.30226978830452644,\r\n    \"hidden0_2->anxiety\": 0.2120823591662554,\r\n    \"hidden0_2->curiosity\": -0.3888682945493871,\r\n    \"hidden0_3->satisfaction\": -0.4596086232970492,\r\n    \"hidden0_3->anxiety\": -0.3153435843511939,\r\n    \"hidden0_3->curiosity\": 0.14797491080390412\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false,\r\n    \"hidden0_0\": 0.0,\r\n    \"hidden0_1\": 0.0,\r\n    \"hidden0_2\": 0.0,\r\n    \"hidden0_3\": 0.0\r\n  },\r\n  \"neuron_shapes\": {\r\n    \"hunger\": \"circle\",\r\n    \"happiness\": \"circle\",\r\n    \"cleanliness\": \"circle\",\r\n    \"sleepiness\": \"circle\",\r\n    \"satisfaction\": \"circle\",\r\n    \"anxiety\": \"circle\",\r\n    \"curiosity\": \"circle\",\r\n    \"can_see_food\": \"square\",\r\n    \"hidden0_0\": \"pentagon\",\r\n    \"hidden0_1\": \"pentagon\",\r\n    \"hidden0_2\": \"pentagon\",\r\n    \"hidden0_3\": \"pentagon\"\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      40.0,\r\n      50\r\n    ],\r\n    \"happiness\": [\r\n      220.0,\r\n      50\r\n    ],\r\n    \"cleanliness\": [\r\n      400.0,\r\n      50\r\n    ],\r\n    \"sleepiness\": [\r\n      580.0,\r\n      50\r\n    ],\r\n    \"satisfaction\": [\r\n      220.0,\r\n      350\r\n    ],\r\n    \"anxiety\": [\r\n      400.0,\r\n      350\r\n    ],\r\n    \"curiosity\": [\r\n      580.0,\r\n      350\r\n    ],\r\n    \"can_see_food\": [\r\n      760.0,\r\n      50\r\n    ],\r\n    \"hidden0_0\": [\r\n      130.0,\r\n      200.0\r\n    ],\r\n    \"hidden0_1\": [\r\n      310.0,\r\n      200.0\r\n    ],\r\n    \"hidden0_2\": [\r\n      490.0,\r\n      200.0\r\n    ],\r\n    \"hidden0_3\": [\r\n      670.0,\r\n      200.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"hunger|hidden0_0\": 0.15527318685678937,\r\n    \"hunger|hidden0_1\": -0.32662200356313464,\r\n    \"hunger|hidden0_2\": 0.3690305534442575,\r\n    \"hunger|hidden0_3\": 0.08771704005552605,\r\n    \"happiness|hidden0_0\": 0.43563670388631415,\r\n    \"happiness|hidden0_1\": 0.3747222471549809,\r\n    \"happiness|hidden0_2\": 0.21675935283830716,\r\n    \"happiness|hidden0_3\": 0.30553842478041715,\r\n    \"cleanliness|hidden0_0\": -0.45817893703227386,\r\n    \"cleanliness|hidden0_1\": -0.3924017649498124,\r\n    \"cleanliness|hidden0_2\": 0.34017744816296347,\r\n    \"cleanliness|hidden0_3\": 0.22903537539505892,\r\n    \"sleepiness|hidden0_0\": 0.42053087299722425,\r\n    \"sleepiness|hidden0_1\": -0.11093503092117496,\r\n    \"sleepiness|hidden0_2\": 0.0022831860748842026,\r\n    \"sleepiness|hidden0_3\": -0.46929846779909523,\r\n    \"can_see_food|hidden0_0\": 0.49996147166880156,\r\n    \"can_see_food|hidden0_1\": 0.12164356013940125,\r\n    \"can_see_food|hidden0_2\": 0.3932867930701862,\r\n    \"can_see_food|hidden0_3\": -0.2591350891409684,\r\n    \"hidden0_0|satisfaction\": 0.3553682697768865,\r\n    \"hidden0_0|anxiety\": 0.4303336060210178,\r\n    \"hidden0_0|curiosity\": -0.10878219621490581,\r\n    \"hidden0_1|satisfaction\": -0.17268262288844005,\r\n    \"hidden0_1|anxiety\": -0.21608691390489854,\r\n    \"hidden0_1|curiosity\": -0.42810633830239697,\r\n    \"hidden0_2|satisfaction\": -0.30226978830452644,\r\n    \"hidden0_2|anxiety\": 0.2120823591662554,\r\n    \"hidden0_2|curiosity\": -0.3888682945493871,\r\n    \"hidden0_3|satisfaction\": -0.4596086232970492,\r\n    \"hidden0_3|anxiety\": -0.3153435843511939,\r\n    \"hidden0_3|curiosity\": 0.14797491080390412\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        40.0,\r\n        50\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        220.0,\r\n        50\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        400.0,\r\n        50\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        580.0,\r\n        50\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        220.0,\r\n        350\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        400.0,\r\n        350\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        580.0,\r\n        350\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        760.0,\r\n        50\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\",\r\n      \"shape\": \"square\",\r\n      \"is_custom\": false\r\n    },\r\n    \"hidden0_0\": {\r\n      \"name\": \"hidden0_0\",\r\n      \"neuron_type\": \"custom\",\r\n      \"position\": [\r\n        130.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        147,\r\n        112,\r\n        219\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"pentagon\",\r\n      \"is_custom\": true\r\n    },\r\n    \"hidden0_1\": {\r\n      \"name\": \"hidden0_1\",\r\n      \"neuron_type\": \"custom\",\r\n      \"position\": [\r\n        310.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        147,\r\n        112,\r\n        219\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"pentagon\",\r\n      \"is_custom\": true\r\n    },\r\n    \"hidden0_2\": {\r\n      \"name\": \"hidden0_2\",\r\n      \"neuron_type\": \"custom\",\r\n      \"position\": [\r\n        490.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        147,\r\n        112,\r\n        219\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"pentagon\",\r\n      \"is_custom\": true\r\n    },\r\n    \"hidden0_3\": {\r\n      \"name\": \"hidden0_3\",\r\n      \"neuron_type\": \"custom\",\r\n      \"position\": [\r\n        670.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        147,\r\n        112,\r\n        219\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"pentagon\",\r\n      \"is_custom\": true\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"custom_neurons\": [\r\n    \"hidden0_0\",\r\n    \"hidden0_1\",\r\n    \"hidden0_2\",\r\n    \"hidden0_3\"\r\n  ],\r\n  \"required_complete\": true\r\n\r\n}\r\n"
  },
  {
    "path": "custom_brains/Feeling-Blue.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Feeling-Blue\",\r\n    \"description\": \"Turn actually blue when depressed\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"16-12-25\",\r\n    \"modified\": \"22-12-25\"\r\n  },\r\n  \"neurons\": {\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        378.0,\r\n        456.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"hunger\": {\r\n      \"position\": [\r\n        200.0,\r\n        64.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        370.0,\r\n        224.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        452.0,\r\n        68.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        739.0,\r\n        -6.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        177.0,\r\n        269.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        746.0,\r\n        134.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        726.0,\r\n        405.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"connector_rescue\": {\r\n      \"position\": [\r\n        601.96,\r\n        241.90666666666667\r\n      ],\r\n      \"type\": \"connector\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": false,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"can_see_food->hunger\": 0.2,\r\n    \"can_see_food->happiness\": 0.5,\r\n    \"can_see_food->satisfaction\": -0.017445686087134815,\r\n    \"can_see_food->curiosity\": 0.06631607984264454,\r\n    \"hunger->cleanliness\": -0.43801581951295265,\r\n    \"hunger->sleepiness\": -0.5012994136772835,\r\n    \"happiness->sleepiness\": 0.566154703734282,\r\n    \"cleanliness->sleepiness\": 0.8100292263454689,\r\n    \"satisfaction->curiosity\": 0.00986623355092231,\r\n    \"anxiety->connector_rescue\": 0.5723832132411578,\r\n    \"curiosity->connector_rescue\": -0.4078549395278624,\r\n    \"connector_rescue->hunger\": -0.42918421260854756,\r\n    \"cleanliness->anxiety\": -0.05\r\n  },\r\n  \"state\": {\r\n    \"can_see_food\": false,\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"connector_rescue\": 0.0\r\n  },\r\n  \"neuron_shapes\": {\r\n    \"can_see_food\": \"square\",\r\n    \"hunger\": \"circle\",\r\n    \"happiness\": \"circle\",\r\n    \"cleanliness\": \"circle\",\r\n    \"sleepiness\": \"circle\",\r\n    \"satisfaction\": \"circle\",\r\n    \"anxiety\": \"circle\",\r\n    \"curiosity\": \"circle\",\r\n    \"connector_rescue\": \"hexagon\"\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [\r\n    {\r\n      \"neuron_name\": \"happiness\",\r\n      \"output_hook\": \"neuron_output_change_color\",\r\n      \"threshold\": 30.0,\r\n      \"trigger_mode\": \"falling\",\r\n      \"cooldown\": 1.0,\r\n      \"enabled\": true,\r\n      \"hook_params\": {\r\n        \"red\": 129,\r\n        \"green\": 228,\r\n        \"blue\": 255\r\n      }\r\n    }\r\n  ],\r\n  \"neuron_positions\": {\r\n    \"can_see_food\": [\r\n      378.0,\r\n      456.0\r\n    ],\r\n    \"hunger\": [\r\n      200.0,\r\n      64.0\r\n    ],\r\n    \"happiness\": [\r\n      370.0,\r\n      224.0\r\n    ],\r\n    \"cleanliness\": [\r\n      452.0,\r\n      68.0\r\n    ],\r\n    \"sleepiness\": [\r\n      739.0,\r\n      -6.0\r\n    ],\r\n    \"satisfaction\": [\r\n      177.0,\r\n      269.0\r\n    ],\r\n    \"anxiety\": [\r\n      746.0,\r\n      134.0\r\n    ],\r\n    \"curiosity\": [\r\n      726.0,\r\n      405.0\r\n    ],\r\n    \"connector_rescue\": [\r\n      601.96,\r\n      241.90666666666667\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"can_see_food|hunger\": 0.2,\r\n    \"can_see_food|happiness\": 0.5,\r\n    \"can_see_food|satisfaction\": -0.017445686087134815,\r\n    \"can_see_food|curiosity\": 0.06631607984264454,\r\n    \"hunger|cleanliness\": -0.43801581951295265,\r\n    \"hunger|sleepiness\": -0.5012994136772835,\r\n    \"happiness|sleepiness\": 0.566154703734282,\r\n    \"cleanliness|sleepiness\": 0.8100292263454689,\r\n    \"satisfaction|curiosity\": 0.00986623355092231,\r\n    \"anxiety|connector_rescue\": 0.5723832132411578,\r\n    \"curiosity|connector_rescue\": -0.4078549395278624,\r\n    \"connector_rescue|hunger\": -0.42918421260854756,\r\n    \"cleanliness|anxiety\": -0.05\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        378.0,\r\n        456.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\",\r\n      \"shape\": \"square\"\r\n    },\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        200.0,\r\n        64.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        370.0,\r\n        224.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        452.0,\r\n        68.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        739.0,\r\n        -6.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        177.0,\r\n        269.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        746.0,\r\n        134.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        726.0,\r\n        405.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\"\r\n    },\r\n    \"connector_rescue\": {\r\n      \"name\": \"connector_rescue\",\r\n      \"neuron_type\": \"connector\",\r\n      \"position\": [\r\n        601.96,\r\n        241.90666666666667\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        0,\r\n        0,\r\n        0\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"custom\",\r\n      \"shape\": \"hexagon\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n\r\n}\r\n"
  },
  {
    "path": "custom_brains/Grasshopper.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Grasshopper\",\r\n    \"description\": \"\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"10-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"hunger->anxiety\": 0.142,\r\n    \"satisfaction->anxiety\": -0.295,\r\n    \"hunger->curiosity\": -0.026,\r\n    \"happiness->satisfaction\": 0.087,\r\n    \"sleepiness->satisfaction\": -0.158,\r\n    \"happiness->curiosity\": 0.198,\r\n    \"sleepiness->anxiety\": 0.163,\r\n    \"anxiety->curiosity\": -0.393,\r\n    \"sleepiness->curiosity\": -0.358,\r\n    \"can_see_food->hunger\": 0.354,\r\n    \"curiosity->satisfaction\": 0.098,\r\n    \"can_see_food->happiness\": 0.301,\r\n    \"hunger->satisfaction\": -0.191,\r\n    \"hunger->happiness\": -0.296,\r\n    \"satisfaction->happiness\": 0.185,\r\n    \"cleanliness->anxiety\": -0.151,\r\n    \"anxiety->happiness\": -0.202\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"hunger|anxiety\": 0.142,\r\n    \"satisfaction|anxiety\": -0.295,\r\n    \"hunger|curiosity\": -0.026,\r\n    \"happiness|satisfaction\": 0.087,\r\n    \"sleepiness|satisfaction\": -0.158,\r\n    \"happiness|curiosity\": 0.198,\r\n    \"sleepiness|anxiety\": 0.163,\r\n    \"anxiety|curiosity\": -0.393,\r\n    \"sleepiness|curiosity\": -0.358,\r\n    \"can_see_food|hunger\": 0.354,\r\n    \"curiosity|satisfaction\": 0.098,\r\n    \"can_see_food|happiness\": 0.301,\r\n    \"hunger|satisfaction\": -0.191,\r\n    \"hunger|happiness\": -0.296,\r\n    \"satisfaction|happiness\": 0.185,\r\n    \"cleanliness|anxiety\": -0.151,\r\n    \"anxiety|happiness\": -0.202\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Healthy_Interests.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Healthy interests\",\r\n    \"description\": \"Nice strong reciprocal basic layout\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"22-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        95.0,\r\n        46.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"is_custom\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"is_custom\": false,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"sleepiness->anxiety\": 0.212,\r\n    \"satisfaction->anxiety\": -0.302,\r\n    \"can_see_food->hunger\": 0.443,\r\n    \"anxiety->sleepiness\": -1.0,\r\n    \"satisfaction->happiness\": 0.7000000000000001,\r\n    \"anxiety->satisfaction\": -0.183,\r\n    \"hunger->happiness\": -0.181,\r\n    \"can_see_food->happiness\": 0.359\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"neuron_shapes\": {\r\n    \"hunger\": \"circle\",\r\n    \"happiness\": \"circle\",\r\n    \"cleanliness\": \"circle\",\r\n    \"sleepiness\": \"circle\",\r\n    \"satisfaction\": \"circle\",\r\n    \"anxiety\": \"circle\",\r\n    \"curiosity\": \"circle\",\r\n    \"can_see_food\": \"square\"\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      95.0,\r\n      46.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"sleepiness|anxiety\": 0.212,\r\n    \"satisfaction|anxiety\": -0.302,\r\n    \"can_see_food|hunger\": 0.443,\r\n    \"anxiety|sleepiness\": -1.0,\r\n    \"satisfaction|happiness\": 0.7000000000000001,\r\n    \"anxiety|satisfaction\": -0.183,\r\n    \"hunger|happiness\": -0.181,\r\n    \"can_see_food|happiness\": 0.359\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        95.0,\r\n        46.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\",\r\n      \"shape\": \"circle\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\",\r\n      \"shape\": \"square\",\r\n      \"is_custom\": false,\r\n      \"bias\": 0.0\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"custom_neurons\": [],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/L'insomniaque.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"L'insomniaque\",\r\n    \"description\": \"\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"10-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"anxiety->sleepiness\": -0.95,\r\n    \"curiosity->sleepiness\": -0.8,\r\n    \"hunger->sleepiness\": -0.5,\r\n    \"can_see_food->sleepiness\": -0.5,\r\n    \"sleepiness->anxiety\": 0.2,\r\n    \"happiness->satisfaction\": 0.10000000000000007,\r\n    \"satisfaction->happiness\": 0.10000000000000007\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"anxiety|sleepiness\": -0.95,\r\n    \"curiosity|sleepiness\": -0.8,\r\n    \"hunger|sleepiness\": -0.5,\r\n    \"can_see_food|sleepiness\": -0.5,\r\n    \"sleepiness|anxiety\": 0.2,\r\n    \"happiness|satisfaction\": 0.10000000000000007,\r\n    \"satisfaction|happiness\": 0.10000000000000007\r\n  },\r\n  \"layer_structure\": [\r\n    {\r\n      \"name\": \"Sensors\",\r\n      \"layer_type\": \"input\",\r\n      \"y_position\": 150,\r\n      \"color\": [\r\n        200,\r\n        200,\r\n        220,\r\n        80\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"Racing Mind\",\r\n      \"layer_type\": \"hidden\",\r\n      \"y_position\": 200,\r\n      \"color\": [\r\n        200,\r\n        200,\r\n        220,\r\n        80\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"State\",\r\n      \"layer_type\": \"output\",\r\n      \"y_position\": 350,\r\n      \"color\": [\r\n        200,\r\n        200,\r\n        220,\r\n        80\r\n      ]\r\n    }\r\n  ],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Minimal.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Minimal\",\r\n    \"description\": \"\",\r\n    \"author\": \"Rufus Pearce\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"10-12-25\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        392.0,\r\n        63.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        260.0,\r\n        151.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"can_see_food->satisfaction\": 0.13,\r\n    \"sleepiness->anxiety\": 0.184,\r\n    \"sleepiness->happiness\": -0.063,\r\n    \"satisfaction->happiness\": 0.288,\r\n    \"hunger->happiness\": -0.151,\r\n    \"cleanliness->happiness\": 0.404,\r\n    \"happiness->satisfaction\": 0.307,\r\n    \"satisfaction->curiosity\": 0.202,\r\n    \"anxiety->curiosity\": -0.305\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      392.0,\r\n      63.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      260.0,\r\n      151.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"can_see_food|satisfaction\": 0.13,\r\n    \"sleepiness|anxiety\": 0.184,\r\n    \"sleepiness|happiness\": -0.063,\r\n    \"satisfaction|happiness\": 0.288,\r\n    \"hunger|happiness\": -0.151,\r\n    \"cleanliness|happiness\": 0.404,\r\n    \"happiness|satisfaction\": 0.307,\r\n    \"satisfaction|curiosity\": 0.202,\r\n    \"anxiety|curiosity\": -0.305\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        392.0,\r\n        63.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        260.0,\r\n        151.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/Plant-Seeker.json",
    "content": "{\r\n  \"version\": \"2.0\",\r\n  \"format\": \"dosidicus\",\r\n  \"metadata\": {\r\n    \"name\": \"Untitled\",\r\n    \"description\": \"\",\r\n    \"author\": \"\",\r\n    \"version\": \"1.0\",\r\n    \"created\": \"\",\r\n    \"modified\": \"\"\r\n  },\r\n  \"neurons\": {\r\n    \"hunger\": {\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"happiness\": {\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"cleanliness\": {\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"sleepiness\": {\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"satisfaction\": {\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"anxiety\": {\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"curiosity\": {\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"type\": \"core\",\r\n      \"is_binary\": false,\r\n      \"is_core\": true,\r\n      \"is_sensor\": false,\r\n      \"activation\": 50.0\r\n    },\r\n    \"can_see_food\": {\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": true,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    },\r\n    \"plant_proximity\": {\r\n      \"position\": [\r\n        890.0,\r\n        264.0\r\n      ],\r\n      \"type\": \"sensor\",\r\n      \"is_binary\": false,\r\n      \"is_core\": false,\r\n      \"is_sensor\": true,\r\n      \"activation\": 0.0\r\n    }\r\n  },\r\n  \"connections\": {\r\n    \"hunger->satisfaction\": -0.092,\r\n    \"anxiety->curiosity\": -0.344,\r\n    \"satisfaction->anxiety\": -0.098,\r\n    \"hunger->happiness\": -0.19,\r\n    \"sleepiness->satisfaction\": -0.201,\r\n    \"anxiety->happiness\": -0.059,\r\n    \"anxiety->satisfaction\": -0.15,\r\n    \"satisfaction->hunger\": -0.185,\r\n    \"can_see_food->curiosity\": 0.114,\r\n    \"sleepiness->curiosity\": -0.243,\r\n    \"satisfaction->happiness\": 0.397,\r\n    \"hunger->anxiety\": 0.348,\r\n    \"happiness->curiosity\": 0.088,\r\n    \"can_see_food->hunger\": 0.041,\r\n    \"curiosity->happiness\": 0.247,\r\n    \"sleepiness->anxiety\": 0.18,\r\n    \"cleanliness->anxiety\": -0.137,\r\n    \"cleanliness->happiness\": 0.2,\r\n    \"plant_proximity->happiness\": 0.2,\r\n    \"plant_proximity->anxiety\": -0.3,\r\n    \"can_see_food->happiness\": 0.25000000000000006\r\n  },\r\n  \"state\": {\r\n    \"hunger\": 50.0,\r\n    \"happiness\": 50.0,\r\n    \"cleanliness\": 50.0,\r\n    \"sleepiness\": 50.0,\r\n    \"satisfaction\": 50.0,\r\n    \"anxiety\": 50.0,\r\n    \"curiosity\": 50.0,\r\n    \"can_see_food\": false,\r\n    \"plant_proximity\": 0.0\r\n  },\r\n  \"excluded_neurons\": [],\r\n  \"output_bindings\": [],\r\n  \"neuron_positions\": {\r\n    \"hunger\": [\r\n      127.0,\r\n      81.0\r\n    ],\r\n    \"happiness\": [\r\n      361.0,\r\n      81.0\r\n    ],\r\n    \"cleanliness\": [\r\n      627.0,\r\n      81.0\r\n    ],\r\n    \"sleepiness\": [\r\n      840.0,\r\n      81.0\r\n    ],\r\n    \"satisfaction\": [\r\n      271.0,\r\n      380.0\r\n    ],\r\n    \"anxiety\": [\r\n      491.0,\r\n      389.0\r\n    ],\r\n    \"curiosity\": [\r\n      701.0,\r\n      386.0\r\n    ],\r\n    \"can_see_food\": [\r\n      50.0,\r\n      200.0\r\n    ],\r\n    \"plant_proximity\": [\r\n      890.0,\r\n      264.0\r\n    ]\r\n  },\r\n  \"weights\": {\r\n    \"hunger|satisfaction\": -0.092,\r\n    \"anxiety|curiosity\": -0.344,\r\n    \"satisfaction|anxiety\": -0.098,\r\n    \"hunger|happiness\": -0.19,\r\n    \"sleepiness|satisfaction\": -0.201,\r\n    \"anxiety|happiness\": -0.059,\r\n    \"anxiety|satisfaction\": -0.15,\r\n    \"satisfaction|hunger\": -0.185,\r\n    \"can_see_food|curiosity\": 0.114,\r\n    \"sleepiness|curiosity\": -0.243,\r\n    \"satisfaction|happiness\": 0.397,\r\n    \"hunger|anxiety\": 0.348,\r\n    \"happiness|curiosity\": 0.088,\r\n    \"can_see_food|hunger\": 0.041,\r\n    \"curiosity|happiness\": 0.247,\r\n    \"sleepiness|anxiety\": 0.18,\r\n    \"cleanliness|anxiety\": -0.137,\r\n    \"cleanliness|happiness\": 0.2,\r\n    \"plant_proximity|happiness\": 0.2,\r\n    \"plant_proximity|anxiety\": -0.3,\r\n    \"can_see_food|happiness\": 0.25000000000000006\r\n  },\r\n  \"layer_structure\": [],\r\n  \"neuron_details\": {\r\n    \"hunger\": {\r\n      \"name\": \"hunger\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        127.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"happiness\": {\r\n      \"name\": \"happiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        361.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"cleanliness\": {\r\n      \"name\": \"cleanliness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        627.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"sleepiness\": {\r\n      \"name\": \"sleepiness\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        840.0,\r\n        81.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"satisfaction\": {\r\n      \"name\": \"satisfaction\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        271.0,\r\n        380.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"anxiety\": {\r\n      \"name\": \"anxiety\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        491.0,\r\n        389.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"curiosity\": {\r\n      \"name\": \"curiosity\",\r\n      \"neuron_type\": \"core\",\r\n      \"position\": [\r\n        701.0,\r\n        386.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        150,\r\n        220\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": false,\r\n      \"category\": \"core\"\r\n    },\r\n    \"can_see_food\": {\r\n      \"name\": \"can_see_food\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        50.0,\r\n        200.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        100,\r\n        180,\r\n        100\r\n      ],\r\n      \"description\": \"Required neuron\",\r\n      \"is_binary\": true,\r\n      \"category\": \"required\"\r\n    },\r\n    \"plant_proximity\": {\r\n      \"name\": \"plant_proximity\",\r\n      \"neuron_type\": \"sensor\",\r\n      \"position\": [\r\n        890.0,\r\n        264.0\r\n      ],\r\n      \"layer_index\": 0,\r\n      \"color\": [\r\n        150,\r\n        200,\r\n        220\r\n      ],\r\n      \"description\": \"\",\r\n      \"is_binary\": false,\r\n      \"category\": \"sensor\"\r\n    }\r\n  },\r\n  \"sensors_used\": [\r\n    \"can_see_food\",\r\n    \"plant_proximity\"\r\n  ],\r\n  \"required_complete\": true\r\n}"
  },
  {
    "path": "custom_brains/readme.md",
    "content": "# Custom brains:\n\nIncluded in this folder:\n\n\n## \"Dense connections\" (The Reactive Brain)\nThis brain is characterized by high volatility and direct emotional feedback loops.\n\nEmotional Interdependence: Almost every core drive is wired directly to another. For example, cleanliness has a strong positive weight toward happiness (0.391), meaning this squid finds significant joy in being clean.\n\nThe \"Anxiety\" Hub: anxiety is extremely \"loud\" in this brain. hunger significantly drives anxiety (0.333), but interestingly, satisfaction helps suppress it (-0.216). This squid likely feels \"hangry\" very easily but calms down quickly once it's content.\n\nHigh-Friction Curiosity: anxiety has a strong inhibitory link to curiosity (-0.309). This squid is likely \"Timid\"; when it gets nervous, it completely stops exploring.\n\nCognitive Style: Because there are no hidden layers, this brain doesn't \"think\" before it acts. It is purely reflexive. If it sees food, that signal hits curiosity (0.27) and happiness (0.232) instantly.\n\n\n-------------------------------------\n\n## \"Feed-Forward-Hidden-Layer\" (The Analytical Brain)\nThis version (v1.1) represents a significant leap in \"cognitive\" complexity by introducing a hidden processing layer (hidden0_0 through hidden0_3).\n\nThe \"Filter\" Layer: Information doesn't just jump from a feeling to an action. It passes through four \"pentagon\" neurons first. This allows the squid to balance multiple conflicting needs (like being both hungry and sleepy) before deciding how it feels.\n\n### Structured Influence:\n\n- `Hidden0_0` (The Positivity Driver): This neuron is strongly activated by happiness (0.435) and can_see_food (0.499). It then heavily drives satisfaction and anxiety. It seems to be the \"Excitement\" processor.\n\n- `Hidden0_3` (The Inhibitor): This neuron is strongly suppressed by sleepiness (-0.469) and can_see_food (-0.259). When it is active, it heavily suppresses satisfaction (-0.459).\n\n\n\n- Better Learning Potential: By using hidden layers, this brain can learn non-linear relationships. It can \"understand\" that seeing food is good when hungry, but maybe less important when it's exhausted.\n\n- Cognitive Style: This is a \"contemplative\" brain. It’s better at prioritizing and likely shows more stable, less erratic behavior than the \"Dense Connections\" model.\n\n\n------------------------\n\n## Plant-Seeker: \nThis is a more complex specialized brain. It uses a unique plant_proximity sensor to drive its internal state. It has a very strong link where hunger spikes anxiety (0.348), creating a squid that is highly motivated to find those plants when its stomach is empty.\n\n\n-------------------\n\n## Change_colour_when_see_food:\n This squid has a very high novelty_object_investigation drive. Interestingly, investigating novelty significantly reduces its hunger (-0.728), suggesting that for this child, curiosity and discovery are almost as fulfilling as eating.\n\n This squid has an output binding (set in the designer) that makes it change colour when it can see food\n\n\n--------------------\n\n\n## Feeling-Blue:\n\n This structure is a study in depression. It features a fascinating connector_rescue neuron. It is wired so that cleanliness and happiness both have powerful positive influences on sleepiness (0.81 and 0.566 respectively), creating a squid that likely retreats into sleep when it feels good—or perhaps uses sleep as a primary emotional regulator.\n\n\n------------------\n\n\n## Grasshopper:\n\n A high-anxiety model. Its anxiety has a very strong inhibitory effect on curiosity (-0.393), meaning this squid likely \"freezes\" or stops exploring the moment it feels stressed.\n\n\n----------------\n\n\n## Healthy Interests:\n\n This is one of your most balanced \"offspring.\" It features strong reciprocal links, like satisfaction driving happiness (0.7), creating a stable positive feedback loop that helps the squid maintain a \"healthy\" mental state.\n\n\n\n\n------------------\n\n\n## L'insomniaque:\n\n This squid is physically incapable of rest. We have wired almost every drive—anxiety (-0.95), curiosity (-0.8), and even hunger (-0.5)—to inhibit sleepiness. This is a squid that stays awake as long as it has any internal stimulation.\n\n\n\n-----------------\n\n\n## Bathtub:\n This model seems focused on the joy of hygiene. It has a notable positive weight from cleanliness to happiness (0.296), making it a squid that specifically finds its \"zen\" in being clean.\n\n\n---------------\n\n\n## Minimal: \nThis is the \"blank slate\" child. With very few active connections, it represents the base species before the environment and neurogenesis begin their work. It is the perfect starting point for watching how a brain grows from nothing.\n\n\n\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  headless:\n    build:\n      context: .\n      target: headless\n    volumes:\n      - ./headless_output:/app/output\n    command: [\"--ticks\", \"10000\", \"--output\", \"/app/output/trained_brain.json\"]\n\n  gui:\n    build:\n      context: .\n      target: gui\n    environment:\n      - DISPLAY=${DISPLAY}\n      - WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-wayland-0}\n      - XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/mnt/wslg/runtime-dir}\n      - QT_X11_NO_MITSHM=1\n    volumes:\n      - /tmp/.X11-unix:/tmp/.X11-unix:rw\n      - /mnt/wslg:/mnt/wslg\n      - ./saves:/app/saves\n      - ./logs:/app/logs\n    profiles:\n      - gui\n"
  },
  {
    "path": "example_squids/readme.md",
    "content": "### `Example_squids` folder\n\nput these example squids in the `saves` folder and launch the simulation\n\nONLY ONE squid can be in the save folder at a time\n\nPlease see this wiki article:  https://github.com/ViciousSquid/Dosidicus/wiki/Example-squids\n"
  },
  {
    "path": "extras/SaveViewer.html",
    "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Dosidicus Save Viewer</title>\r\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js\"></script>\r\n    <style>\r\n        :root {\r\n            --bg-dark: #1a1a1a;\r\n            --bg-panel: #252525;\r\n            --bg-header: #2d2d2d;\r\n            --text-main: #e0e0e0;\r\n            --text-dim: #b0b0b0;\r\n            --accent: #3d6a8b;\r\n            --accent-hover: #4d7a9b;\r\n            --border: #444;\r\n            --code-font: 'Consolas', 'Monaco', monospace;\r\n            --squid-pink: #FF69B4;\r\n            --squid-pink-hover: #FF8ABF;\r\n        }\r\n\r\n        body {\r\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\r\n            background-color: var(--bg-dark);\r\n            color: var(--text-main);\r\n            margin: 0;\r\n            display: flex;\r\n            flex-direction: column;\r\n            height: 100vh;\r\n            overflow: hidden;\r\n        }\r\n\r\n        header {\r\n            background-color: var(--bg-header);\r\n            border-bottom: 1px solid var(--border);\r\n            padding: 15px 20px;\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 20px;\r\n            flex-shrink: 0;\r\n        }\r\n\r\n        h1 { margin: 0; font-size: 1.2rem; color: #fff; }\r\n\r\n        button {\r\n            background-color: var(--accent);\r\n            color: white;\r\n            border: none;\r\n            padding: 8px 16px;\r\n            border-radius: 4px;\r\n            cursor: pointer;\r\n            font-weight: bold;\r\n            font-size: 0.9rem;\r\n        }\r\n        button:hover { background-color: var(--accent-hover); }\r\n\r\n        /* === ENLARGED PINK OPEN BUTTON (matches 🦑 squid emoji pink) === */\r\n        .open-save-btn {\r\n            background-color: var(--squid-pink);\r\n            color: white;\r\n            font-size: 1.15rem;\r\n            padding: 14px 32px;\r\n            border-radius: 6px;\r\n            font-weight: 700;\r\n            letter-spacing: 0.5px;\r\n            box-shadow: 0 4px 12px rgba(255, 105, 180, 0.3);\r\n        }\r\n        .open-save-btn:hover {\r\n            background-color: var(--squid-pink-hover);\r\n            transform: scale(1.03);\r\n        }\r\n\r\n        .export-btn {\r\n            background-color: #2e7d32;\r\n            margin-left: 10px;\r\n        }\r\n        .export-btn:hover { background-color: #388e3c; }\r\n        \r\n        input[type=\"file\"] { display: none; }\r\n\r\n        .tabs {\r\n            background-color: var(--bg-header);\r\n            display: flex;\r\n            overflow-x: auto;\r\n            flex-shrink: 0;\r\n            border-bottom: 1px solid var(--border);\r\n        }\r\n\r\n        .tab-btn {\r\n            background: none;\r\n            border: none;\r\n            color: var(--text-dim);\r\n            padding: 12px 20px;\r\n            cursor: pointer;\r\n            font-size: 1rem;\r\n            border-bottom: 2px solid transparent;\r\n            white-space: nowrap;\r\n        }\r\n\r\n        .tab-btn:hover { background-color: #353535; }\r\n        .tab-btn.active {\r\n            color: #fff;\r\n            border-bottom-color: var(--accent);\r\n            background-color: var(--bg-dark);\r\n        }\r\n\r\n        #content-area {\r\n            flex-grow: 1;\r\n            overflow-y: auto;\r\n            padding: 20px;\r\n            position: relative;\r\n        }\r\n\r\n        .tab-content { display: none; }\r\n        .tab-content.active { display: block; }\r\n\r\n        #welcome-screen {\r\n            display: flex;\r\n            flex-direction: column;\r\n            align-items: center;\r\n            justify-content: center;\r\n            height: 100%;\r\n            text-align: center;\r\n            color: var(--text-dim);\r\n        }\r\n        #welcome-screen .icon { font-size: 64px; margin-bottom: 20px; }\r\n\r\n        .section {\r\n            margin-bottom: 20px;\r\n            border-radius: 6px;\r\n            overflow: hidden;\r\n        }\r\n\r\n        .section-header {\r\n            background-color: #2d5a7b;\r\n            padding: 10px 15px;\r\n            font-weight: bold;\r\n            cursor: pointer;\r\n            user-select: none;\r\n            display: flex;\r\n            align-items: center;\r\n        }\r\n        .section-header:hover { background-color: #3d6a8b; }\r\n        .section-header::before {\r\n            content: '▶';\r\n            display: inline-block;\r\n            margin-right: 10px;\r\n            font-size: 0.8em;\r\n            transition: transform 0.2s;\r\n        }\r\n        .section.open .section-header::before { transform: rotate(90deg); }\r\n\r\n        .section-content {\r\n            background-color: #1e1e1e;\r\n            padding: 15px;\r\n            display: none;\r\n        }\r\n        .section.open .section-content { display: block; }\r\n\r\n        .kv-row {\r\n            display: flex;\r\n            padding: 4px 0;\r\n            border-bottom: 1px solid #2a2a2a;\r\n        }\r\n        .kv-key {\r\n            color: #7fbadc;\r\n            font-weight: bold;\r\n            min-width: 220px;\r\n            width: 220px;\r\n        }\r\n        .kv-val { color: #e0e0e0; flex-grow: 1; word-break: break-all; }\r\n\r\n        #network-canvas {\r\n            background-color: #1a1a1a;\r\n            border-radius: 4px;\r\n            cursor: grab;\r\n        }\r\n        #network-canvas:active { cursor: grabbing; }\r\n\r\n        .json-tree { font-family: var(--code-font); font-size: 0.9rem; }\r\n        .json-tree ul { list-style: none; padding-left: 20px; margin: 0; }\r\n        .json-tree li { margin: 2px 0; }\r\n        .caret { cursor: pointer; user-select: none; color: #888; display: inline-block; width: 15px; }\r\n        .caret-down::before { content: \"▼\"; font-size: 0.7em; }\r\n        .caret-right::before { content: \"▶\"; font-size: 0.7em; }\r\n        .nested { display: none; }\r\n        .active { display: block; }\r\n        .key { color: #9cdcfe; }\r\n        .string { color: #ce9178; }\r\n        .number { color: #b5cea8; }\r\n        .boolean { color: #569cd6; }\r\n        .null { color: #569cd6; }\r\n\r\n        .memory-card {\r\n            background-color: #252525;\r\n            border-left: 3px solid #3d6a8b;\r\n            margin: 5px 0;\r\n            padding: 10px;\r\n            border-radius: 4px;\r\n        }\r\n        .tag {\r\n            display: inline-block;\r\n            padding: 2px 8px;\r\n            border-radius: 3px;\r\n            font-size: 0.75rem;\r\n            font-weight: bold;\r\n            color: white;\r\n            text-transform: uppercase;\r\n            margin-right: 10px;\r\n        }\r\n        .tag.environment { background-color: #4CAF50; }\r\n        .tag.behavior { background-color: #9C27B0; }\r\n        .tag.food { background-color: #FF9800; }\r\n        .tag.interaction { background-color: #2196F3; }\r\n        \r\n        .stat-group-title {\r\n            color: #7fbadc;\r\n            font-weight: bold;\r\n            margin-top: 15px;\r\n            margin-bottom: 5px;\r\n            font-size: 1.1em;\r\n        }\r\n\r\n        .bio-summary {\r\n            background: #2d5a7b;\r\n            padding: 18px;\r\n            border-radius: 8px;\r\n            margin-bottom: 25px;\r\n            font-size: 1.15em;\r\n            line-height: 1.5;\r\n        }\r\n        .memory-dashboard { display: flex; flex-direction: column; gap: 15px; }\r\n        .filter-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }\r\n        .memory-columns {\r\n            display: flex;\r\n            gap: 20px;\r\n            flex-wrap: wrap;\r\n        }\r\n        .memory-column {\r\n            flex: 1;\r\n            min-width: 380px;\r\n        }\r\n        .memory-list {\r\n            max-height: 520px;\r\n            overflow-y: auto;\r\n            border: 1px solid #444;\r\n            border-radius: 6px;\r\n            padding: 8px;\r\n        }\r\n        .summary-card {\r\n            background: #252525;\r\n            padding: 14px;\r\n            border-radius: 6px;\r\n            border-left: 4px solid var(--accent);\r\n            display: inline-block;\r\n            margin-right: 10px;\r\n        }\r\n        .network-summary {\r\n            margin-top: 20px;\r\n            display: grid;\r\n            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\r\n            gap: 12px;\r\n        }\r\n        .summary-item {\r\n            background: #252525;\r\n            padding: 12px;\r\n            border-radius: 6px;\r\n            border-left: 4px solid #3d6a8b;\r\n        }\r\n        .progress-container {\r\n            height: 14px;\r\n            background: #333;\r\n            border-radius: 7px;\r\n            overflow: hidden;\r\n            margin: 6px 0;\r\n        }\r\n        .progress-bar {\r\n            height: 100%;\r\n            background: linear-gradient(to right, #4CAF50, #8BC34A);\r\n            transition: width 0.4s ease;\r\n        }\r\n        .tooltip {\r\n            position: absolute;\r\n            background: #252525;\r\n            border: 1px solid #666;\r\n            padding: 8px 12px;\r\n            border-radius: 4px;\r\n            pointer-events: none;\r\n            z-index: 9999;\r\n            font-size: 0.85rem;\r\n            box-shadow: 0 4px 12px rgba(0,0,0,0.6);\r\n            display: none;\r\n            white-space: nowrap;\r\n        }\r\n        table.neuro-table {\r\n            width: 100%;\r\n            border-collapse: collapse;\r\n            margin-top: 10px;\r\n        }\r\n        table.neuro-table th, table.neuro-table td {\r\n            padding: 8px;\r\n            text-align: left;\r\n            border-bottom: 1px solid #333;\r\n        }\r\n        table.neuro-table th { background: #2d5a7b; }\r\n        .metadata-grid {\r\n            display: grid;\r\n            grid-template-columns: 1fr 1fr;\r\n            gap: 12px;\r\n        }\r\n    </style>\r\n</head>\r\n<body>\r\n\r\n<header>\r\n    <h1>Save Viewer v2.1.3</h1>\r\n    \r\n    <button class=\"open-save-btn\" onclick=\"document.getElementById('file-input').click()\">\r\n        📂 Open Save File (.zip)\r\n    </button>\r\n    \r\n    <input type=\"file\" id=\"file-input\" accept=\".zip\" onchange=\"handleFileSelect(event)\">\r\n    <span id=\"file-name\" style=\"color: var(--text-dim);\">No file loaded</span>\r\n    \r\n    <button id=\"export-stats-btn\" class=\"export-btn\" style=\"display:none\" onclick=\"exportStatistics()\">📊 Stats CSV</button>\r\n    <button id=\"export-memories-btn\" class=\"export-btn\" style=\"display:none\" onclick=\"exportMemories()\">💭 Memories JSON</button>\r\n    <button id=\"export-brain-btn\" class=\"export-btn\" style=\"display:none\" onclick=\"exportBrainPNG()\">🧠 Brain PNG</button>\r\n</header>\r\n\r\n<div class=\"tabs\" id=\"tabs-nav\"></div>\r\n\r\n<div id=\"content-area\">\r\n    <div id=\"welcome-screen\">\r\n        <div class=\"icon\">🦑</div>\r\n        <h2>Dosidicus Save Viewer v2.1.3</h2>\r\n        <p>https://github.com/ViciousSquid/Dosidicus</p>\r\n        <p>Use the pink button to load a squid</p>\r\n    </div>\r\n</div>\r\n\r\n<script>\r\n    let saveData = {};\r\n    let currentCanvas = null;\r\n    let tooltip = null;\r\n\r\n    // ==================== STRICT UUID VALIDATION ====================\r\n    async function handleFileSelect(event) {\r\n        const file = event.target.files[0];\r\n        if (!file) return;\r\n\r\n        document.getElementById('file-name').textContent = file.name;\r\n        \r\n        const zip = new JSZip();\r\n        try {\r\n            const contents = await zip.loadAsync(file);\r\n            saveData = {};\r\n\r\n            let uuidText = '';\r\n            if (contents.files['uuid.txt']) {\r\n                uuidText = (await contents.files['uuid.txt'].async('string')).trim();\r\n                saveData['uuid_txt'] = uuidText;\r\n            }\r\n\r\n            if (!uuidText || !uuidText.startsWith('SquidSignature')) {\r\n                alert(`❌ INVALID SAVE FILE\\n\\n` +\r\n                      `This zip does not contain a valid uuid.txt file.\\n\\n` +\r\n                      `A real Dosidicus save must start with:\\n` +\r\n                      `SquidSignature    xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\\n\\n` +\r\n                      `File rejected.`);\r\n                document.getElementById('file-name').textContent = '❌ Invalid save (no SquidSignature)';\r\n                return;\r\n            }\r\n\r\n            const promises = [];\r\n            for (const filename in contents.files) {\r\n                if (filename.endsWith('.json')) {\r\n                    const p = contents.files[filename].async('string').then(text => {\r\n                        try {\r\n                            const key = filename.replace('.json', '');\r\n                            saveData[key] = JSON.parse(text);\r\n                        } catch (e) {}\r\n                    });\r\n                    promises.push(p);\r\n                }\r\n            }\r\n            await Promise.all(promises);\r\n\r\n            buildUI();\r\n\r\n            document.getElementById('export-stats-btn').style.display = 'inline-block';\r\n            document.getElementById('export-memories-btn').style.display = 'inline-block';\r\n            document.getElementById('export-brain-btn').style.display = saveData.brain_state ? 'inline-block' : 'none';\r\n\r\n        } catch (err) {\r\n            alert(\"Error reading zip file: \" + err.message);\r\n            console.error(err);\r\n        }\r\n    }\r\n    // ===============================================================\r\n\r\n    function exportStatistics() {\r\n        if (!saveData.statistics) return alert(\"No statistics data\");\r\n        let csv = \"Key,Value\\n\";\r\n        Object.entries(saveData.statistics).forEach(([k,v]) => csv += `\"${k.replace(/\"/g,'\"\"')}\",\"${v}\"\\n`);\r\n        downloadFile(csv, \"statistics.csv\", \"text/csv\");\r\n    }\r\n    function exportMemories() {\r\n        const all = [...(saveData.ShortTerm||[]), ...(saveData.LongTerm||[])];\r\n        downloadFile(JSON.stringify(all, null, 2), \"memories.json\", \"application/json\");\r\n    }\r\n    function exportBrainPNG() {\r\n        if (!currentCanvas) return;\r\n        const link = document.createElement('a');\r\n        link.download = 'brain_network.png';\r\n        link.href = currentCanvas.toDataURL('image/png');\r\n        link.click();\r\n    }\r\n    function downloadFile(content, filename, mime) {\r\n        const blob = new Blob([content], {type: mime});\r\n        const url = URL.createObjectURL(blob);\r\n        const a = document.createElement('a');\r\n        a.href = url; a.download = filename; a.click();\r\n        URL.revokeObjectURL(url);\r\n    }\r\n\r\n    function buildUI() {\r\n        const tabsNav = document.getElementById('tabs-nav');\r\n        const contentArea = document.getElementById('content-area');\r\n        tabsNav.innerHTML = '';\r\n        contentArea.innerHTML = '';\r\n\r\n        createTab('Overview', '📋 Overview', renderOverview, true);\r\n        if (saveData['brain_state']) createTab('Network', '🕸️ Network', renderNetwork);\r\n        if (saveData['ShortTerm'] || saveData['LongTerm']) createTab('Memories', '💭 Memories', renderMemoryDashboard);\r\n        if (saveData['achievements']) createTab('Achievements', '🏆 Achievements', renderEnhancedAchievements);\r\n        createTab('Metadata', '📋 Metadata', renderSaveMetadata);\r\n\r\n        const fileOrder = ['game_state', 'brain_state', 'statistics'];\r\n        fileOrder.forEach(key => {\r\n            if (saveData[key]) {\r\n                const icon = getIconForKey(key);\r\n                createTab(key, `${icon} ${key}.json`, () => renderJsonView(key, saveData[key]));\r\n            }\r\n        });\r\n\r\n        Object.keys(saveData).forEach(key => {\r\n            if (!fileOrder.includes(key) && key !== 'uuid_txt' && !['ShortTerm','LongTerm','achievements'].includes(key)) {\r\n                createTab(key, `📄 ${key}.json`, () => renderJsonTree(saveData[key]));\r\n            }\r\n        });\r\n    }\r\n\r\n    function getIconForKey(key) {\r\n        const icons = {'game_state': '🦑', 'brain_state': '🧠', 'statistics': '📊'};\r\n        return icons[key] || '📄';\r\n    }\r\n\r\n    function createTab(id, label, renderFunction, isActive = false) {\r\n        const nav = document.getElementById('tabs-nav');\r\n        const contentArea = document.getElementById('content-area');\r\n\r\n        const btn = document.createElement('button');\r\n        btn.className = `tab-btn ${isActive ? 'active' : ''}`;\r\n        btn.textContent = label;\r\n        btn.onclick = () => switchTab(id);\r\n        nav.appendChild(btn);\r\n\r\n        const div = document.createElement('div');\r\n        div.id = `tab-${id}`;\r\n        div.className = `tab-content ${isActive ? 'active' : ''}`;\r\n        contentArea.appendChild(div);\r\n\r\n        if (renderFunction) {\r\n            const content = renderFunction();\r\n            if (content) div.appendChild(content);\r\n        }\r\n    }\r\n\r\n    function switchTab(id) {\r\n        document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));\r\n        document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\r\n        const contentDiv = document.getElementById(`tab-${id}`);\r\n        if (contentDiv) contentDiv.classList.add('active');\r\n        \r\n        const allBtns = document.querySelectorAll('.tab-btn');\r\n        for (let btn of allBtns) {\r\n            if (btn.textContent.includes(id)) {\r\n                btn.classList.add('active');\r\n                break;\r\n            }\r\n        }\r\n    }\r\n\r\n    function createCollapsible(title, contentNode, startOpen = true) {\r\n        const section = document.createElement('div');\r\n        section.className = `section ${startOpen ? 'open' : ''}`;\r\n        const header = document.createElement('div');\r\n        header.className = 'section-header';\r\n        header.textContent = title;\r\n        header.onclick = () => section.classList.toggle('open');\r\n        const content = document.createElement('div');\r\n        content.className = 'section-content';\r\n        content.appendChild(contentNode);\r\n        section.appendChild(header);\r\n        section.appendChild(content);\r\n        return section;\r\n    }\r\n\r\n    function createKVRow(key, val) {\r\n        const row = document.createElement('div');\r\n        row.className = 'kv-row';\r\n        const k = document.createElement('div');\r\n        k.className = 'kv-key';\r\n        k.textContent = key + ':';\r\n        const v = document.createElement('div');\r\n        v.className = 'kv-val';\r\n        v.textContent = val;\r\n        row.appendChild(k);\r\n        row.appendChild(v);\r\n        return row;\r\n    }\r\n\r\n    function createNeuronRow(name, value) {\r\n        const row = document.createElement('div');\r\n        row.className = 'kv-row';\r\n        const k = document.createElement('div');\r\n        k.className = 'kv-key';\r\n        k.style.color = '#b0b0b0';\r\n        k.textContent = name.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\r\n        const v = document.createElement('div');\r\n        v.className = 'kv-val';\r\n        v.textContent = typeof value === 'number' ? Math.round(value * 100) / 100 : value;\r\n        v.style.fontWeight = 'bold';\r\n        row.appendChild(k);\r\n        row.appendChild(v);\r\n        return row;\r\n    }\r\n\r\n    function formatAge(seconds) {\r\n        if (!seconds) return \"0s\";\r\n        const d = Math.floor(seconds / 86400);\r\n        const h = Math.floor((seconds % 86400) / 3600);\r\n        const m = Math.floor((seconds % 3600) / 60);\r\n        const s = Math.floor(seconds % 60);\r\n        return `${d}d ${h}h ${m}m ${s}s`.replace(/^0d /, '').replace(/^0h /, '').replace(/^0m /, '');\r\n    }\r\n\r\n    function createBiography() {\r\n        const stats = saveData.statistics || {};\r\n        const squid = saveData.game_state?.squid || {};\r\n        const enhanced = (saveData.brain_state || {}).enhanced_neurogenesis || {};\r\n        \r\n        let distanceRaw = 0;\r\n        for (const k in stats) {\r\n            if (k.toLowerCase().includes('distance') || k.toLowerCase().includes('swum')) {\r\n                distanceRaw = stats[k];\r\n                break;\r\n            }\r\n        }\r\n        const distance = (distanceRaw / 100).toFixed(1);\r\n\r\n        const poops = stats.poops_created || 0;\r\n        const poopText = poops === 1 ? '1 poop' : `${poops} poops`;\r\n\r\n        const bio = document.createElement('div');\r\n        bio.className = 'bio-summary';\r\n        bio.innerHTML = `\r\n            <strong>🦑 Squid Biography</strong><br>\r\n            This squid lived <strong>${formatAge(stats.total_age_seconds || 0)}</strong>, \r\n            swam <strong>${distance} meters</strong>, \r\n            created <strong>${poopText}</strong>, \r\n            experienced <strong>${stats.startles_experienced || 0} startles</strong>, \r\n            and developed <strong>${enhanced.novelty_neuron_count || 0} novelty neurons</strong>.\r\n            ${squid.personality ? ` Personality: <strong>${squid.personality}</strong>` : ''}\r\n        `;\r\n        return bio;\r\n    }\r\n\r\n    function renderOverview() {\r\n        const container = document.createElement('div');\r\n        container.appendChild(createBiography());\r\n\r\n        const squidData = saveData.game_state?.squid || {};\r\n        const stats = saveData.statistics || {};\r\n        \r\n        const infoDiv = document.createElement('div');\r\n        infoDiv.appendChild(createKVRow('Name', squidData.name || 'Unknown'));\r\n        infoDiv.appendChild(createKVRow('Personality', squidData.personality || 'Unknown'));\r\n        infoDiv.appendChild(createKVRow('Health', squidData.is_sick ? 'Sick 🤒' : `Healthy (${squidData.health}%)`));\r\n        infoDiv.appendChild(createKVRow('Age', formatAge(stats.total_age_seconds || 0)));\r\n        if(squidData.uuid) infoDiv.appendChild(createKVRow('UUID', squidData.uuid));\r\n        container.appendChild(createCollapsible('🦑 Squid Information', infoDiv));\r\n\r\n        const neuronDiv = document.createElement('div');\r\n        const keys = ['hunger', 'happiness', 'cleanliness', 'sleepiness', 'satisfaction', 'anxiety', 'curiosity', 'health'];\r\n        keys.forEach(k => {\r\n            if (squidData[k] !== undefined) neuronDiv.appendChild(createNeuronRow(k, squidData[k]));\r\n        });\r\n        container.appendChild(createCollapsible('🧠 Core Neuron Values', neuronDiv));\r\n\r\n        const brain = saveData.brain_state || {};\r\n        const enhanced = brain.enhanced_neurogenesis || {};\r\n        const funcNeurons = enhanced.functional_neurons || {};\r\n        const neuroContainer = document.createElement('div');\r\n        neuroContainer.appendChild(createKVRow('Total Functional Neurons', Object.keys(funcNeurons).length));\r\n        neuroContainer.appendChild(createKVRow('Novelty Neurons', enhanced.novelty_neuron_count || 0));\r\n        if (Object.keys(funcNeurons).length > 0) {\r\n            const table = document.createElement('table');\r\n            table.className = 'neuro-table';\r\n            table.innerHTML = `<thead><tr><th>Name</th><th>Type</th><th>Utility</th><th>Connections</th></tr></thead><tbody></tbody>`;\r\n            const tbody = table.querySelector('tbody');\r\n            for (const [name, data] of Object.entries(funcNeurons)) {\r\n                const row = document.createElement('tr');\r\n                row.innerHTML = `<td><span style=\"color:#4CAF50\">🧬 ${name}</span></td><td>${data.neuron_type || 'unknown'}</td><td>${(data.utility_score || 0).toFixed(2)}</td><td>${data.connections ? Object.keys(data.connections).length : '—'}</td>`;\r\n                tbody.appendChild(row);\r\n            }\r\n            neuroContainer.appendChild(table);\r\n        }\r\n        container.appendChild(createCollapsible('🧬 Neurogenesis Deep Dive', neuroContainer));\r\n\r\n        const statsDiv = document.createElement('div');\r\n        const groups = {\r\n            \"📊 General\": ['total_age_seconds', 'total_playtime_seconds', 'distance_swum'],\r\n            \"🍣 Food & Consumption\": ['cheese_eaten', 'sushi_eaten', 'poops_created', 'food_thrown'],\r\n            \"🤝 Interactions\": ['startles_experienced', 'ink_clouds_created', 'rocks_thrown', 'rocks_carried', 'decorations_interacted'],\r\n            \"🧠 Neural\": ['current_neurons', 'max_neurons_reached', 'novelty_neurons_created', 'hebbian_updates', 'neurons_pruned'],\r\n            \"💤 Sleep & Health\": ['total_sleep_seconds', 'average_sleep_quality', 'health_restored', 'sickness_episodes'],\r\n            \"🌍 Exploration\": ['distance_swum', 'time_moving', 'unique_locations_visited']\r\n        };\r\n        for (const [groupName, keyList] of Object.entries(groups)) {\r\n            const title = document.createElement('div');\r\n            title.className = 'stat-group-title';\r\n            title.textContent = groupName;\r\n            statsDiv.appendChild(title);\r\n            keyList.forEach(k => {\r\n                if (stats[k] !== undefined) {\r\n                    let val = stats[k];\r\n                    const pretty = k.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\r\n                    if (k.includes('distance')) val = (val / 100).toFixed(1) + ' m';\r\n                    else if (k.includes('seconds')) val = formatAge(val);\r\n                    statsDiv.appendChild(createKVRow(pretty, val));\r\n                }\r\n            });\r\n        }\r\n        const usedKeys = Object.values(groups).flat();\r\n        Object.keys(stats).filter(k => !usedKeys.includes(k)).forEach(k => {\r\n            const pretty = k.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\r\n            statsDiv.appendChild(createKVRow(pretty, stats[k]));\r\n        });\r\n        container.appendChild(createCollapsible('📊 Full Statistics', statsDiv));\r\n\r\n        return container;\r\n    }\r\n\r\n    function renderNetwork() {\r\n        const container = document.createElement('div');\r\n        container.style.height = '100%';\r\n        container.style.display = 'flex';\r\n        container.style.flexDirection = 'column';\r\n\r\n        const controlsDiv = document.createElement('div');\r\n        controlsDiv.style.margin = '8px 0 12px 0';\r\n        controlsDiv.style.display = 'flex';\r\n        controlsDiv.style.alignItems = 'center';\r\n        controlsDiv.style.gap = '12px';\r\n        controlsDiv.style.flexWrap = 'wrap';\r\n\r\n        const label = document.createElement('label');\r\n        label.style.color = '#ccc';\r\n        label.style.fontSize = '0.95rem';\r\n        label.style.cursor = 'pointer';\r\n        const checkbox = document.createElement('input');\r\n        checkbox.type = 'checkbox';\r\n        checkbox.checked = true;\r\n        label.appendChild(checkbox);\r\n        label.appendChild(document.createTextNode(' Show connection weight labels'));\r\n        controlsDiv.appendChild(label);\r\n\r\n        const resetBtn = document.createElement('button');\r\n        resetBtn.textContent = 'Reset View';\r\n        resetBtn.style.padding = '4px 12px';\r\n        resetBtn.style.fontSize = '0.85rem';\r\n        resetBtn.onclick = () => { scale = initialScale; offsetX = initialOffsetX; offsetY = initialOffsetY; redrawNetwork(); };\r\n        controlsDiv.appendChild(resetBtn);\r\n\r\n        container.appendChild(controlsDiv);\r\n\r\n        const canvas = document.createElement('canvas');\r\n        canvas.id = 'network-canvas';\r\n        canvas.width = 1100;\r\n        canvas.height = 720;\r\n        canvas.style.width = '100%';\r\n        canvas.style.maxWidth = '1100px';\r\n        currentCanvas = canvas;\r\n        container.appendChild(canvas);\r\n\r\n        const summaryDiv = document.createElement('div');\r\n        summaryDiv.className = 'network-summary';\r\n        container.appendChild(summaryDiv);\r\n\r\n        const brain = saveData.brain_state || {};\r\n        const positions = brain.neuron_positions || {};\r\n        const weightsList = brain.weights_list || [];\r\n        const states = brain.neuron_states || brain.state || {};\r\n\r\n        const weights = weightsList.filter(w => Array.isArray(w) && w.length === 3)\r\n                                 .map(w => ({src: w[0], dst: w[1], weight: w[2]}));\r\n\r\n        let scale = 1;\r\n        let offsetX = 0;\r\n        let offsetY = 0;\r\n        let initialScale = 1;\r\n        let initialOffsetX = 0;\r\n        let initialOffsetY = 0;\r\n        let isDragging = false;\r\n        let lastMouseX = 0;\r\n        let lastMouseY = 0;\r\n        let showWeightLabels = true;\r\n\r\n        checkbox.addEventListener('change', () => {\r\n            showWeightLabels = checkbox.checked;\r\n            redrawNetwork();\r\n        });\r\n\r\n        function calculateInitialView() {\r\n            let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\r\n            for (const key in positions) {\r\n                const [x, y] = positions[key];\r\n                minX = Math.min(minX, x); maxX = Math.max(maxX, x);\r\n                minY = Math.min(minY, y); maxY = Math.max(maxY, y);\r\n            }\r\n            const padding = 80;\r\n            const dataW = maxX - minX + padding * 2;\r\n            const dataH = maxY - minY + padding * 2;\r\n            const fitScale = Math.min(canvas.width / dataW, canvas.height / dataH, 1.0);\r\n            const centerX = (canvas.width - dataW * fitScale) / 2 - minX * fitScale + padding * fitScale;\r\n            const centerY = (canvas.height - dataH * fitScale) / 2 - minY * fitScale + padding * fitScale;\r\n\r\n            scale = fitScale;\r\n            offsetX = centerX;\r\n            offsetY = centerY;\r\n            initialScale = fitScale;\r\n            initialOffsetX = centerX;\r\n            initialOffsetY = centerY;\r\n        }\r\n\r\n        function redrawNetwork() {\r\n            const ctx = canvas.getContext('2d');\r\n            ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n            ctx.fillStyle = '#1a1a1a';\r\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n            weights.forEach(w => {\r\n                const p1 = positions[w.src];\r\n                const p2 = positions[w.dst];\r\n                if (!p1 || !p2) return;\r\n                const x1 = p1[0] * scale + offsetX;\r\n                const y1 = p1[1] * scale + offsetY;\r\n                const x2 = p2[0] * scale + offsetX;\r\n                const y2 = p2[1] * scale + offsetY;\r\n\r\n                ctx.beginPath();\r\n                ctx.moveTo(x1, y1);\r\n                ctx.lineTo(x2, y2);\r\n                const intensity = Math.min(Math.abs(w.weight), 1);\r\n                ctx.strokeStyle = w.weight > 0 \r\n                    ? `rgba(0, ${Math.floor(255 * intensity)}, 0, 0.75)` \r\n                    : `rgba(${Math.floor(255 * intensity)}, 0, 0, 0.75)`;\r\n                ctx.lineWidth = Math.max(2, 4 * intensity);\r\n                ctx.stroke();\r\n            });\r\n\r\n            if (showWeightLabels) {\r\n                ctx.font = 'bold 13px Arial';\r\n                ctx.textAlign = 'center';\r\n                ctx.textBaseline = 'middle';\r\n                weights.forEach(w => {\r\n                    const p1 = positions[w.src];\r\n                    const p2 = positions[w.dst];\r\n                    if (!p1 || !p2) return;\r\n                    const x1 = p1[0] * scale + offsetX;\r\n                    const y1 = p1[1] * scale + offsetY;\r\n                    const x2 = p2[0] * scale + offsetX;\r\n                    const y2 = p2[1] * scale + offsetY;\r\n                    const mx = (x1 + x2) / 2;\r\n                    const my = (y1 + y2) / 2;\r\n                    ctx.fillStyle = '#ffffff';\r\n                    ctx.fillText(w.weight.toFixed(2), mx, my);\r\n                });\r\n            }\r\n\r\n            for (const name in positions) {\r\n                const [rx, ry] = positions[name];\r\n                const x = rx * scale + offsetX;\r\n                const y = ry * scale + offsetY;\r\n                const val = states[name] || 0;\r\n                let fill = '#999';\r\n                if (typeof val === 'number') {\r\n                    const norm = Math.max(0, Math.min(1, val / 100));\r\n                    fill = norm > 0.7 ? '#4CAF50' : norm > 0.4 ? '#FFC107' : '#F44336';\r\n                }\r\n                ctx.beginPath();\r\n                ctx.arc(x, y, 16, 0, Math.PI * 2);\r\n                ctx.fillStyle = fill;\r\n                ctx.fill();\r\n                ctx.strokeStyle = '#111';\r\n                ctx.lineWidth = 3;\r\n                ctx.stroke();\r\n\r\n                ctx.fillStyle = '#fff';\r\n                ctx.font = 'bold 13px Arial';\r\n                ctx.textAlign = 'center';\r\n                const disp = name.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());\r\n                ctx.fillText(disp, x, y + 38);\r\n            }\r\n        }\r\n\r\n        let lastWheelTime = 0;\r\n        canvas.addEventListener('wheel', e => {\r\n            e.preventDefault();\r\n            const now = Date.now();\r\n            if (now - lastWheelTime < 16) return;\r\n            lastWheelTime = now;\r\n\r\n            const rect = canvas.getBoundingClientRect();\r\n            const mouseX = e.clientX - rect.left;\r\n            const mouseY = e.clientY - rect.top;\r\n\r\n            const zoomFactor = e.deltaY < 0 ? 1.15 : 0.87;\r\n\r\n            const worldX = (mouseX - offsetX) / scale;\r\n            const worldY = (mouseY - offsetY) / scale;\r\n\r\n            scale *= zoomFactor;\r\n            scale = Math.max(0.2, Math.min(4.0, scale));\r\n\r\n            offsetX = mouseX - worldX * scale;\r\n            offsetY = mouseY - worldY * scale;\r\n\r\n            redrawNetwork();\r\n        });\r\n\r\n        canvas.addEventListener('mousedown', e => {\r\n            if (e.button === 0) {\r\n                isDragging = true;\r\n                lastMouseX = e.clientX;\r\n                lastMouseY = e.clientY;\r\n                canvas.style.cursor = 'grabbing';\r\n            }\r\n        });\r\n\r\n        window.addEventListener('mouseup', () => {\r\n            isDragging = false;\r\n            if (canvas) canvas.style.cursor = 'grab';\r\n        });\r\n\r\n        canvas.addEventListener('mousemove', e => {\r\n            if (!isDragging) {\r\n                const rect = canvas.getBoundingClientRect();\r\n                const mx = e.clientX - rect.left;\r\n                const my = e.clientY - rect.top;\r\n                let found = null;\r\n                for (const name in positions) {\r\n                    const nx = positions[name][0] * scale + offsetX;\r\n                    const ny = positions[name][1] * scale + offsetY;\r\n                    if (Math.hypot(nx - mx, ny - my) < 22) {\r\n                        found = {name, val: states[name] || 0, x: nx, y: ny};\r\n                        break;\r\n                    }\r\n                }\r\n                if (found) {\r\n                    tooltip.style.display = 'block';\r\n                    tooltip.style.left = `${found.x + 25}px`;\r\n                    tooltip.style.top = `${found.y - 10}px`;\r\n                    tooltip.innerHTML = `<strong>${found.name}</strong><br>Value: ${found.val.toFixed(1)}`;\r\n                } else {\r\n                    tooltip.style.display = 'none';\r\n                }\r\n                return;\r\n            }\r\n\r\n            const dx = e.clientX - lastMouseX;\r\n            const dy = e.clientY - lastMouseY;\r\n            offsetX += dx;\r\n            offsetY += dy;\r\n            lastMouseX = e.clientX;\r\n            lastMouseY = e.clientY;\r\n            redrawNetwork();\r\n        });\r\n\r\n        setTimeout(() => {\r\n            calculateInitialView();\r\n            redrawNetwork();\r\n\r\n            tooltip = document.createElement('div');\r\n            tooltip.className = 'tooltip';\r\n            container.appendChild(tooltip);\r\n\r\n            canvas.onmouseleave = () => tooltip.style.display = 'none';\r\n        }, 50);\r\n\r\n        const totalNeurons = Object.keys(positions).length;\r\n        const totalConn = weights.length;\r\n        const avgWeight = weights.length ? (weights.reduce((a,w)=>a+Math.abs(w.weight),0)/weights.length).toFixed(2) : 0;\r\n        const positive = weights.filter(w => w.weight > 0).length;\r\n        const strongest = weights.reduce((max,w) => Math.abs(w.weight) > Math.abs(max.weight) ? w : max, {weight:0});\r\n\r\n        summaryDiv.innerHTML = `\r\n            <div class=\"summary-item\"><strong>Total Neurons</strong><br>${totalNeurons}</div>\r\n            <div class=\"summary-item\"><strong>Connections</strong><br>${totalConn} (avg weight ${avgWeight})</div>\r\n            <div class=\"summary-item\"><strong>Positive Weights</strong><br>${positive} / ${totalConn}</div>\r\n            <div class=\"summary-item\"><strong>Strongest Link</strong><br>${strongest.src} → ${strongest.dst} (${strongest.weight.toFixed(2)})</div>\r\n        `;\r\n\r\n        return container;\r\n    }\r\n\r\n    function renderMemoryDashboard() {\r\n        const container = document.createElement('div');\r\n        container.className = 'memory-dashboard';\r\n\r\n        const shortTermMem = saveData.ShortTerm || [];\r\n        const longTermMem = saveData.LongTerm || [];\r\n        const allMemForCats = [...shortTermMem, ...longTermMem];\r\n\r\n        const summaryRow = document.createElement('div');\r\n        summaryRow.innerHTML = `\r\n            <span class=\"summary-card\">Short-term: <strong>${shortTermMem.length}</strong></span>\r\n            <span class=\"summary-card\">Long-term: <strong>${longTermMem.length}</strong></span>\r\n            <span class=\"summary-card\">Avg importance: <strong>${allMemForCats.length ? (allMemForCats.reduce((s,m)=>s+(m.importance||0),0)/allMemForCats.length).toFixed(1) : 0}</strong></span>\r\n        `;\r\n        container.appendChild(summaryRow);\r\n\r\n        const filterRow = document.createElement('div');\r\n        filterRow.className = 'filter-row';\r\n        \r\n        const searchInput = document.createElement('input');\r\n        searchInput.type = 'text';\r\n        searchInput.placeholder = 'Search key or value...';\r\n        searchInput.style.width = '280px';\r\n        filterRow.appendChild(searchInput);\r\n\r\n        const catSelect = document.createElement('select');\r\n        const categories = [...new Set(allMemForCats.map(m => m.category || 'UNK'))];\r\n        catSelect.innerHTML = `<option value=\"\">All categories</option>` + categories.map(c => `<option value=\"${c}\">${c}</option>`).join('');\r\n        filterRow.appendChild(catSelect);\r\n\r\n        container.appendChild(filterRow);\r\n\r\n        const columnsContainer = document.createElement('div');\r\n        columnsContainer.className = 'memory-columns';\r\n        container.appendChild(columnsContainer);\r\n\r\n        const shortColumn = document.createElement('div');\r\n        shortColumn.className = 'memory-column';\r\n        const shortHeader = document.createElement('div');\r\n        shortHeader.className = 'stat-group-title';\r\n        shortHeader.textContent = `📌 Short Term Memories (${shortTermMem.length})`;\r\n        shortColumn.appendChild(shortHeader);\r\n        const shortListDiv = document.createElement('div');\r\n        shortListDiv.className = 'memory-list';\r\n        shortColumn.appendChild(shortListDiv);\r\n        columnsContainer.appendChild(shortColumn);\r\n\r\n        const longColumn = document.createElement('div');\r\n        longColumn.className = 'memory-column';\r\n        const longHeader = document.createElement('div');\r\n        longHeader.className = 'stat-group-title';\r\n        longHeader.textContent = `📌 Long Term Memories (${longTermMem.length})`;\r\n        longColumn.appendChild(longHeader);\r\n        const longListDiv = document.createElement('div');\r\n        longListDiv.className = 'memory-list';\r\n        longColumn.appendChild(longListDiv);\r\n        columnsContainer.appendChild(longColumn);\r\n\r\n        function renderMemoryList(targetDiv, memArray, filterText = '', catFilter = '') {\r\n            targetDiv.innerHTML = '';\r\n            const filtered = memArray.filter(m => {\r\n                const matchText = (m.key || '') + (m.value || '') + (m.category || '');\r\n                return (!filterText || matchText.toLowerCase().includes(filterText.toLowerCase())) &&\r\n                       (!catFilter || (m.category || 'UNK') === catFilter);\r\n            });\r\n\r\n            if (!filtered.length) {\r\n                targetDiv.innerHTML = '<p style=\"color:#666; text-align:center; padding:20px;\">No memories match filter</p>';\r\n                return;\r\n            }\r\n\r\n            filtered.forEach(mem => {\r\n                const card = document.createElement('div');\r\n                card.className = 'memory-card';\r\n                const header = document.createElement('div');\r\n                header.style.display = 'flex';\r\n                header.style.justifyContent = 'space-between';\r\n                header.style.marginBottom = '5px';\r\n\r\n                const left = document.createElement('div');\r\n                const tag = document.createElement('span');\r\n                tag.className = `tag ${mem.category || 'UNK'}`;\r\n                tag.textContent = mem.category || 'UNK';\r\n                left.appendChild(tag);\r\n                \r\n                const title = document.createElement('span');\r\n                title.style.fontWeight = 'bold';\r\n                title.style.color = '#7fbadc';\r\n                title.textContent = (mem.key || 'Unknown').replace(/_/g, ' ');\r\n                left.appendChild(title);\r\n                header.appendChild(left);\r\n\r\n                if (mem.importance) {\r\n                    const imp = document.createElement('span');\r\n                    imp.style.color = '#FFD700';\r\n                    imp.textContent = `★ ${mem.importance}`;\r\n                    header.appendChild(imp);\r\n                }\r\n                card.appendChild(header);\r\n\r\n                const val = document.createElement('div');\r\n                val.textContent = mem.value;\r\n                val.style.marginBottom = '6px';\r\n                card.appendChild(val);\r\n\r\n                const meta = document.createElement('div');\r\n                meta.style.fontSize = '0.8em';\r\n                meta.style.color = '#666';\r\n                const date = mem.timestamp > 1000000000 ? new Date(mem.timestamp * 1000).toLocaleString() : 'Unknown';\r\n                meta.textContent = `🕐 ${date}`;\r\n                card.appendChild(meta);\r\n\r\n                targetDiv.appendChild(card);\r\n            });\r\n        }\r\n\r\n        function updateLists() {\r\n            const ft = searchInput.value;\r\n            const cf = catSelect.value;\r\n            renderMemoryList(shortListDiv, shortTermMem, ft, cf);\r\n            renderMemoryList(longListDiv, longTermMem, ft, cf);\r\n        }\r\n\r\n        searchInput.oninput = updateLists;\r\n        catSelect.onchange = updateLists;\r\n        updateLists();\r\n\r\n        return container;\r\n    }\r\n\r\n    function renderEnhancedAchievements() {\r\n        const container = document.createElement('div');\r\n        const data = saveData.achievements || {};\r\n        const unlocked = data.unlocked || {};\r\n        const progress = data.progress || {};\r\n\r\n        const unlockedDiv = document.createElement('div');\r\n        const uTitle = document.createElement('h3');\r\n        uTitle.textContent = `Unlocked (${Object.keys(unlocked).length})`;\r\n        unlockedDiv.appendChild(uTitle);\r\n\r\n        Object.keys(unlocked).forEach(k => {\r\n            const item = document.createElement('div');\r\n            item.style.padding = '8px';\r\n            item.style.margin = '4px 0';\r\n            item.style.background = '#1e3a2f';\r\n            item.textContent = `🏆 ${k.replace(/_/g, ' ').toUpperCase()}`;\r\n            unlockedDiv.appendChild(item);\r\n        });\r\n        container.appendChild(unlockedDiv);\r\n\r\n        const progTitle = document.createElement('h3');\r\n        progTitle.textContent = 'Progress';\r\n        progTitle.style.marginTop = '25px';\r\n        container.appendChild(progTitle);\r\n\r\n        Object.entries(progress).forEach(([k, v]) => {\r\n            const row = document.createElement('div');\r\n            row.style.marginBottom = '12px';\r\n            \r\n            const label = document.createElement('div');\r\n            label.textContent = k.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\r\n            row.appendChild(label);\r\n\r\n            const progContainer = document.createElement('div');\r\n            progContainer.className = 'progress-container';\r\n            \r\n            const bar = document.createElement('div');\r\n            bar.className = 'progress-bar';\r\n            const percent = Math.min(100, typeof v === 'number' ? v * 5 : 50);\r\n            bar.style.width = `${percent}%`;\r\n            progContainer.appendChild(bar);\r\n            \r\n            row.appendChild(progContainer);\r\n            row.appendChild(createKVRow('Progress', `${v} ${percent > 95 ? '✅' : ''}`));\r\n            container.appendChild(row);\r\n        });\r\n\r\n        return container;\r\n    }\r\n\r\n    function renderSaveMetadata() {\r\n        const container = document.createElement('div');\r\n        container.className = 'metadata-grid';\r\n\r\n        const uuid = saveData.uuid_txt || saveData.game_state?.squid?.uuid || '—';\r\n        container.appendChild(createKVRow('UUID', uuid));\r\n        container.appendChild(createKVRow('Loaded JSON files', Object.keys(saveData).filter(k => k !== 'uuid_txt').length));\r\n        container.appendChild(createKVRow('Total Neurons', (saveData.brain_state?.neuron_positions ? Object.keys(saveData.brain_state.neuron_positions).length : 0)));\r\n        container.appendChild(createKVRow('File version (inferred)', saveData.game_state?.version || '2.6+'));\r\n        const stats = saveData.statistics || {};\r\n        container.appendChild(createKVRow('Total playtime', formatAge(stats.total_playtime_seconds || stats.total_age_seconds || 0)));\r\n\r\n        return container;\r\n    }\r\n\r\n    function renderJsonView(key, data) {\r\n        return renderJsonTree(data);\r\n    }\r\n\r\n    function renderJsonTree(data) {\r\n        const container = document.createElement('div');\r\n        container.className = 'json-tree';\r\n        \r\n        function createNode(key, value) {\r\n            const li = document.createElement('li');\r\n            if (typeof value === 'object' && value !== null) {\r\n                const isArray = Array.isArray(value);\r\n                const span = document.createElement('span');\r\n                if (Object.keys(value).length > 0) {\r\n                    span.className = 'caret caret-right';\r\n                    span.onclick = function() {\r\n                        this.classList.toggle('caret-down');\r\n                        this.classList.toggle('caret-right');\r\n                        this.parentElement.querySelector('.nested').classList.toggle('active');\r\n                    };\r\n                } else {\r\n                    span.innerHTML = '&nbsp;';\r\n                }\r\n                li.appendChild(span);\r\n                \r\n                const keySpan = document.createElement('span');\r\n                keySpan.className = 'key';\r\n                keySpan.textContent = key ? `${key}: ` : '';\r\n                li.appendChild(keySpan);\r\n\r\n                const typeSpan = document.createElement('span');\r\n                typeSpan.style.color = '#888';\r\n                typeSpan.textContent = isArray ? `[${value.length}]` : `{${Object.keys(value).length}}`;\r\n                li.appendChild(typeSpan);\r\n\r\n                if (Object.keys(value).length > 0) {\r\n                    const ul = document.createElement('ul');\r\n                    ul.className = 'nested';\r\n                    for (const k in value) ul.appendChild(createNode(k, value[k]));\r\n                    li.appendChild(ul);\r\n                }\r\n            } else {\r\n                const span = document.createElement('span');\r\n                span.innerHTML = '&nbsp;';\r\n                li.appendChild(span);\r\n\r\n                const keySpan = document.createElement('span');\r\n                keySpan.className = 'key';\r\n                keySpan.textContent = key ? `${key}: ` : '';\r\n                li.appendChild(keySpan);\r\n\r\n                const valSpan = document.createElement('span');\r\n                if (typeof value === 'string') {\r\n                    valSpan.className = 'string';\r\n                    valSpan.textContent = `\"${value}\"`;\r\n                } else if (typeof value === 'number') {\r\n                    valSpan.className = 'number';\r\n                    valSpan.textContent = value;\r\n                } else if (typeof value === 'boolean') {\r\n                    valSpan.className = 'boolean';\r\n                    valSpan.textContent = value;\r\n                } else {\r\n                    valSpan.className = 'null';\r\n                    valSpan.textContent = 'null';\r\n                }\r\n                li.appendChild(valSpan);\r\n            }\r\n            return li;\r\n        }\r\n\r\n        const ul = document.createElement('ul');\r\n        ul.style.paddingLeft = '0';\r\n        if (Array.isArray(data)) {\r\n            data.forEach((item, i) => ul.appendChild(createNode(i, item)));\r\n        } else {\r\n            for (const key in data) ul.appendChild(createNode(key, data[key]));\r\n        }\r\n        container.appendChild(ul);\r\n        return container;\r\n    }\r\n\r\n    window.switchTab = switchTab;\r\n</script>\r\n</body>\r\n</html>"
  },
  {
    "path": "extras/SquidBreeder.html",
    "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<title>Squid Breeder</title>\r\n\r\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js\"></script>\r\n\r\n<style>\r\nbody {\r\n    font-family: Arial;\r\n    background: #0f1220;\r\n    color: #e0e6ff;\r\n    padding: 20px;\r\n}\r\n\r\n.container {\r\n    display: flex;\r\n    gap: 20px;\r\n}\r\n\r\n.panel {\r\n    flex: 1;\r\n    background: #1a1f3a;\r\n    padding: 15px;\r\n    border-radius: 10px;\r\n    display: flex;\r\n    flex-direction: column;\r\n}\r\n\r\ntextarea {\r\n    width: 100%;\r\n    min-height: 150px;\r\n    resize: both;\r\n    background: #0b0e1a;\r\n    color: #cfd8ff;\r\n    border: none;\r\n    padding: 10px;\r\n}\r\n\r\n.canvas-wrapper {\r\n    display: flex;\r\n    justify-content: center;\r\n    margin-top: 10px;\r\n}\r\n\r\ncanvas {\r\n    width: 400px;\r\n    height: 300px;\r\n    background: #0b0e1a;\r\n}\r\n\r\n.controls {\r\n    font-size: 13px;\r\n    margin-top: 10px;\r\n}\r\n\r\nlabel { margin-right: 10px; }\r\n</style>\r\n</head>\r\n\r\n<body>\r\n\r\n<h1>🧬 Squid Breeder</h1>\r\n\r\n<div class=\"container\">\r\n<div class=\"panel\">\r\n<h3>Squid A</h3>\r\n<input type=\"file\" onchange=\"loadFile(event,'A')\">\r\n<textarea id=\"inputA\"></textarea>\r\n<div class=\"canvas-wrapper\"><canvas id=\"canvasA\" width=\"400\" height=\"300\"></canvas></div>\r\n</div>\r\n\r\n<div class=\"panel\">\r\n<h3>Squid B</h3>\r\n<input type=\"file\" onchange=\"loadFile(event,'B')\">\r\n<textarea id=\"inputB\"></textarea>\r\n<div class=\"canvas-wrapper\"><canvas id=\"canvasB\" width=\"400\" height=\"300\"></canvas></div>\r\n</div>\r\n</div>\r\n\r\n<button onclick=\"breed()\" style=\"background: #ff69b4; color: white; padding: 20px 40px; font-size: 26px; border: none; border-radius: 10px; cursor: pointer; display: block; margin: 20px auto;\">BREED</button>\r\n\r\n<button id=\"saveBtn\" onclick=\"saveJSON()\" style=\"background: #3399ff; color: white; padding: 15px 30px; font-size: 20px; border: none; border-radius: 10px; cursor: pointer; display: none; margin: -10px auto 20px auto;\">💾 Save Child Brain</button>\r\n\r\n<div class=\"panel\">\r\n<h3>Offspring</h3>\r\n\r\n<div class=\"controls\">\r\n<b>Inherited From:</b><br>\r\n<label><input type=\"checkbox\" id=\"fA\" checked>Parent A</label>\r\n<label><input type=\"checkbox\" id=\"fB\" checked>Parent B</label>\r\n<label><input type=\"checkbox\" id=\"fBoth\" checked>Both Parents</label>\r\n\r\n<br><br><b>Neuron Type:</b><br>\r\n<label><input type=\"checkbox\" id=\"fCore\" checked>Core</label>\r\n<label><input type=\"checkbox\" id=\"fCustom\" checked>Custom</label>\r\n\r\n<br><br><b>View Mode:</b><br>\r\n<label><input type=\"checkbox\" id=\"heatmap\">Weight Δ Heatmap</label>\r\n\r\n<br><br><b>Mutation:</b><br>\r\nRate <input type=\"range\" id=\"mutRate\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.1\">\r\nStrength <input type=\"range\" id=\"mutStrength\" min=\"0\" max=\"2\" step=\"0.1\" value=\"0.3\">\r\n</div>\r\n\r\n<textarea id=\"output\"></textarea>\r\n<div class=\"canvas-wrapper\">\r\n<canvas id=\"canvasOffspring\" width=\"400\" height=\"300\"></canvas>\r\n</div>\r\n\r\n<div id=\"probe\"></div>\r\n</div>\r\n\r\n<script>\r\n\r\n// ================= LOAD =================\r\n\r\nasync function loadFile(e,t){\r\n    const f=e.target.files[0];\r\n    if(!f) return;\r\n\r\n    if(f.name.endsWith(\".zip\")){\r\n        const zip=await JSZip.loadAsync(f);\r\n        for(const k of Object.keys(zip.files)){\r\n            if(k.endsWith(\".json\")){\r\n                const txt=await zip.files[k].async(\"string\");\r\n                input(t).value=txt;\r\n                renderNetwork(t);\r\n                return;\r\n            }\r\n        }\r\n    } else {\r\n        input(t).value=await f.text();\r\n        renderNetwork(t);\r\n    }\r\n}\r\n\r\nconst input = t => document.getElementById(\"input\"+t);\r\n\r\n// ================= BREED =================\r\n\r\nfunction breed(){\r\n    const A=JSON.parse(inputA.value);\r\n    const B=JSON.parse(inputB.value);\r\n\r\n    const child=structuredClone(A);\r\n    child._lineage={neurons:{},weights:{}};\r\n\r\n    // neurons\r\n    for(let n in B.neurons){\r\n        if(!child.neurons[n]){\r\n            child.neurons[n]=B.neurons[n];\r\n            child._lineage.neurons[n]=\"B\";\r\n        } else child._lineage.neurons[n]=\"both\";\r\n    }\r\n    for(let n in A.neurons){\r\n        if(!child._lineage.neurons[n]) child._lineage.neurons[n]=\"A\";\r\n    }\r\n\r\n    // weights\r\n    child.weights={};\r\n    const keys=new Set([...Object.keys(A.weights),...Object.keys(B.weights)]);\r\n    keys.forEach(k=>{\r\n        const inA=k in A.weights;\r\n        const inB=k in B.weights;\r\n\r\n        const wa=A.weights[k]??rand();\r\n        const wb=B.weights[k]??rand();\r\n\r\n        child.weights[k]=(wa+wb)/2;\r\n\r\n        child._lineage.weights[k]=inA&&inB?\"both\":(inA?\"A\":\"B\");\r\n    });\r\n\r\n    mutate(child);\r\n\r\n    output.value=JSON.stringify(child,null,2);\r\n    renderOffspring(child);\r\n\r\n    window._offspring=child;\r\n    window._parentA=A;\r\n    window._parentB=B;\r\n\r\n    // Reveal save button\r\n    document.getElementById(\"saveBtn\").style.display = \"block\";\r\n}\r\n\r\n// ================= SAVE =================\r\n\r\nfunction saveJSON() {\r\n    const data = document.getElementById(\"output\").value;\r\n    if (!data) return;\r\n\r\n    const blob = new Blob([data], { type: \"application/json\" });\r\n    const url = URL.createObjectURL(blob);\r\n    const link = document.createElement(\"a\");\r\n    \r\n    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\r\n    link.href = url;\r\n    link.download = `squid_offspring_${timestamp}.json`;\r\n    \r\n    document.body.appendChild(link);\r\n    link.click();\r\n    \r\n    document.body.removeChild(link);\r\n    URL.revokeObjectURL(url);\r\n}\r\n\r\n// ================= MUTATION =================\r\n\r\nfunction mutate(net){\r\n    const rate=+mutRate.value;\r\n    const strength=+mutStrength.value;\r\n\r\n    for(let k in net.weights){\r\n        if(Math.random()<rate){\r\n            net.weights[k]+=rand()*strength;\r\n        }\r\n    }\r\n\r\n    // structural mutation (rare)\r\n    if(Math.random()<rate){\r\n        const nodes=Object.keys(net.neurons);\r\n        const a=nodes[Math.floor(Math.random()*nodes.length)];\r\n        const b=nodes[Math.floor(Math.random()*nodes.length)];\r\n        const key=a+\"|\"+b;\r\n        net.weights[key]=rand();\r\n        net._lineage.weights[key]=\"novel\";\r\n    }\r\n}\r\n\r\nconst rand=()=>Math.random()*2-1;\r\n\r\n// ================= RENDER =================\r\n\r\nfunction renderNetwork(t){\r\n    try{ render(JSON.parse(input(t).value),\"canvas\"+t); }catch{}\r\n}\r\n\r\nfunction renderOffspring(d){ render(d,\"canvasOffspring\",true); }\r\n\r\nfunction render(data,id,isOffspring=false){\r\n    const c=document.getElementById(id);\r\n    const ctx=c.getContext(\"2d\");\r\n    ctx.clearRect(0,0,c.width,c.height);\r\n\r\n    if(!data.neurons) return;\r\n\r\n    const scale=fit(data.neurons);\r\n\r\n    const heat=heatmap.checked;\r\n\r\n    // connections\r\n    for(let k in data.weights||{}){\r\n        const [a,b]=k.split(\"|\");\r\n        if(!data.neurons[a]||!data.neurons[b]) continue;\r\n\r\n        const origin=data._lineage?.weights?.[k]||\"both\";\r\n        if(!pass(origin)) continue;\r\n\r\n        const p1=scale(data.neurons[a].position);\r\n        const p2=scale(data.neurons[b].position);\r\n\r\n        ctx.beginPath();\r\n        ctx.moveTo(...p1);\r\n        ctx.lineTo(...p2);\r\n\r\n        ctx.strokeStyle = heat ? heatColor(k) : color(origin);\r\n        ctx.globalAlpha = origin===\"novel\"?1:0.7;\r\n        ctx.stroke();\r\n    }\r\n\r\n    // neurons\r\n    for(let n in data.neurons){\r\n        const neuron=data.neurons[n];\r\n        const origin=data._lineage?.neurons?.[n]||\"both\";\r\n\r\n        if(!pass(origin)) continue;\r\n        if(neuron.is_core && !fCore.checked) continue;\r\n        if(neuron.is_custom && !fCustom.checked) continue;\r\n\r\n        const [x,y]=scale(neuron.position);\r\n\r\n        ctx.beginPath();\r\n        ctx.arc(x,y,8,0,Math.PI*2);\r\n        ctx.fillStyle=color(origin);\r\n        ctx.fill();\r\n    }\r\n\r\n    // click probe\r\n    c.onclick=e=>probeClick(e,data,scale);\r\n}\r\n\r\n// ================= HELPERS =================\r\n\r\nfunction pass(o){\r\n    return (o===\"A\"&&fA.checked)||(o===\"B\"&&fB.checked)||(o===\"both\"&&fBoth.checked)||(o===\"novel\");\r\n}\r\n\r\nfunction color(o){\r\n    if(o===\"A\") return \"#00ff88\";\r\n    if(o===\"B\") return \"#3399ff\";\r\n    if(o===\"novel\") return \"#ffcc00\";\r\n    return \"#ffffff\";\r\n}\r\n\r\nfunction heatColor(k){\r\n    const a=window._parentA?.weights?.[k]??0;\r\n    const b=window._parentB?.weights?.[k]??0;\r\n    const d=a-b;\r\n\r\n    const v=Math.min(1,Math.abs(d));\r\n    if(d>0) return `rgba(0,255,0,${v})`;\r\n    return `rgba(255,0,0,${v})`;\r\n}\r\n\r\n// fit network into canvas\r\nfunction fit(neurons){\r\n    const xs=Object.values(neurons).map(n=>n.position[0]);\r\n    const ys=Object.values(neurons).map(n=>n.position[1]);\r\n\r\n    const minX=Math.min(...xs), maxX=Math.max(...xs);\r\n    const minY=Math.min(...ys), maxY=Math.max(...ys);\r\n\r\n    const w=maxX-minX, h=maxY-minY;\r\n\r\n    return ([x,y])=>{\r\n        return [\r\n            ((x-minX)/w)*350+25,\r\n            ((y-minY)/h)*250+25\r\n        ];\r\n    };\r\n}\r\n\r\n// ================= PROBE =================\r\n\r\nfunction probeClick(e,data,scale){\r\n    const rect=e.target.getBoundingClientRect();\r\n    const x=e.clientX-rect.left;\r\n    const y=e.clientY-rect.top;\r\n\r\n    for(let n in data.neurons){\r\n        const [nx,ny]=scale(data.neurons[n].position);\r\n        if(Math.hypot(nx-x,ny-y)<10){\r\n            const val=forward(data,n);\r\n            probe.innerText=`Neuron: ${n} → ${val.toFixed(3)}`;\r\n            return;\r\n        }\r\n    }\r\n}\r\n\r\nfunction forward(net,target){\r\n    let val=net.state?.[target]||0;\r\n\r\n    for(let k in net.weights){\r\n        const [a,b]=k.split(\"|\");\r\n        if(b===target){\r\n            val += (net.state?.[a]||0)*net.weights[k];\r\n        }\r\n    }\r\n\r\n    return Math.tanh(val);\r\n}\r\n\r\n// ================= UI =================\r\n\r\n[\"fA\",\"fB\",\"fBoth\",\"fCore\",\"fCustom\",\"heatmap\"].forEach(id=>{\r\n    document.getElementById(id).onchange=()=>{\r\n        if(window._offspring) renderOffspring(window._offspring);\r\n    };\r\n});\r\n\r\n</script>\r\n\r\n</body>\r\n</html>"
  },
  {
    "path": "extras/StepTrainer.html",
    "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Step Trainer</title>\r\n    <script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\r\n    <style>\r\n        * { box-sizing: border-box; }\r\n        body {\r\n            background: #0f172a;\r\n            color: #e2e8f0;\r\n            font-family: 'Inter', 'Segoe UI', sans-serif;\r\n            margin: 0;\r\n            padding: 20px;\r\n        }\r\n        .container { max-width: 1600px; margin: 0 auto; }\r\n        h1 {\r\n            font-size: 1.8rem;\r\n            background: linear-gradient(135deg, #f43f5e, #f97316);\r\n            -webkit-background-clip: text;\r\n            background-clip: text;\r\n            color: transparent;\r\n            margin: 0 0 10px 0;\r\n        }\r\n        .sub {\r\n            color: #94a3b8;\r\n            font-size: 0.9rem;\r\n            margin-bottom: 20px;\r\n        }\r\n        .flex-row { display: flex; gap: 20px; flex-wrap: wrap; }\r\n        .card {\r\n            background: #1e293b;\r\n            border-radius: 16px;\r\n            padding: 16px;\r\n            border: 1px solid #334155;\r\n            box-shadow: 0 8px 20px rgba(0,0,0,0.3);\r\n        }\r\n        .brain-card { flex: 2; min-width: 520px; }\r\n        .controls-card { flex: 1; min-width: 280px; max-height: 85vh; overflow-y: auto; }\r\n        .dashboard-card { flex: 1.5; min-width: 380px; }\r\n        canvas#brainCanvas {\r\n            background: #0f172a;\r\n            border-radius: 12px;\r\n            width: 100%;\r\n            height: auto;\r\n            cursor: crosshair;\r\n            border: 1px solid #334155;\r\n        }\r\n        .input-group {\r\n            margin-bottom: 12px;\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 8px;\r\n            flex-wrap: wrap;\r\n        }\r\n        .input-group label { width: 130px; font-weight: 600; font-size: 0.85rem; }\r\n        input[type=\"range\"] { flex: 1; min-width: 120px; background: #0f172a; }\r\n        button {\r\n            background: #f43f5e;\r\n            border: none;\r\n            color: white;\r\n            padding: 6px 14px;\r\n            border-radius: 8px;\r\n            font-weight: 600;\r\n            cursor: pointer;\r\n            transition: 0.2s;\r\n            margin-right: 8px;\r\n        }\r\n        button:hover { background: #e11d48; transform: scale(0.98); }\r\n        button.secondary { background: #334155; }\r\n        button.secondary:hover { background: #475569; }\r\n        .force-buttons {\r\n            display: flex;\r\n            gap: 10px;\r\n            justify-content: center;\r\n            margin-top: 12px;\r\n            flex-wrap: wrap;\r\n        }\r\n        .force-buttons button {\r\n            flex: 1;\r\n            min-width: 110px;\r\n            background: #2d3748;\r\n            border: 1px solid #475569;\r\n        }\r\n        .force-buttons button:hover { background: #3b4a5e; }\r\n        .log-panel {\r\n            background: #0f172a;\r\n            border-radius: 12px;\r\n            padding: 8px;\r\n            max-height: 180px;\r\n            overflow-y: auto;\r\n            font-family: 'JetBrains Mono', monospace;\r\n            font-size: 0.8rem;\r\n            margin-top: 12px;\r\n        }\r\n        .log-entry {\r\n            border-left: 3px solid #f43f5e;\r\n            padding: 4px 8px;\r\n            margin: 4px 0;\r\n            background: #1e293b;\r\n            border-radius: 6px;\r\n        }\r\n        .stat-grid {\r\n            display: flex;\r\n            gap: 15px;\r\n            margin: 12px 0;\r\n            flex-wrap: wrap;\r\n        }\r\n        .stat-card {\r\n            background: #0f172a;\r\n            padding: 8px 12px;\r\n            border-radius: 12px;\r\n            text-align: center;\r\n            flex: 1;\r\n        }\r\n        .stat-card h4 { margin: 0; font-size: 0.75rem; color: #94a3b8; }\r\n        .stat-value { font-size: 1.6rem; font-weight: bold; color: #f97316; }\r\n        .badge {\r\n            background: #334155;\r\n            border-radius: 20px;\r\n            padding: 2px 8px;\r\n            font-size: 0.7rem;\r\n            display: inline-block;\r\n        }\r\n        hr { border-color: #334155; margin: 12px 0; }\r\n        .flex-between { display: flex; justify-content: space-between; align-items: center; }\r\n        .archetype-item {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 8px;\r\n            margin: 5px 0;\r\n        }\r\n        .archetype-fill {\r\n            background: #f43f5e;\r\n            height: 12px;\r\n            border-radius: 6px;\r\n        }\r\n        .note {\r\n            font-size: 0.7rem;\r\n            color: #94a3b8;\r\n            margin-top: 8px;\r\n            text-align: center;\r\n        }\r\n    </style>\r\n</head>\r\n<body>\r\n<div class=\"container\">\r\n    <div class=\"flex-between\">\r\n        <div>\r\n            <h1>🧠 Step Trainer</h1>\r\n            <div class=\"sub\">Hebbian plasticity | Spike‑timing emulation | Autonomous neuron growth</div>\r\n        </div>\r\n        <div>\r\n            <input type=\"file\" id=\"brainFileInput\" accept=\".json\" style=\"display:none\" />\r\n            <button id=\"loadBtn\">📂 Load Brain JSON</button>\r\n            <button id=\"exportBtn\" class=\"secondary\">💾 Export Brain</button>\r\n            <button id=\"resetBtn\" class=\"secondary\">⟳ Reset State</button>\r\n        </div>\r\n    </div>\r\n    <div class=\"flex-row\">\r\n        <!-- Left: Network Canvas + Simulation -->\r\n        <div class=\"card brain-card\">\r\n            <div class=\"flex-between\">\r\n                <h3>🧬 Active Network</h3>\r\n                <span id=\"tickCounter\" style=\"background:#0f172a; padding:4px 12px; border-radius:20px;\">Ticks: 0</span>\r\n            </div>\r\n            <canvas id=\"brainCanvas\" width=\"800\" height=\"500\" style=\"width:100%; height:auto; background:#0f172a;\"></canvas>\r\n            <div style=\"margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;\">\r\n                <button id=\"stepBtn\">▶ Step</button>\r\n                <button id=\"runBtn\">⏵ Run (auto)</button>\r\n                <button id=\"stopBtn\" class=\"secondary\">⏸ Stop</button>\r\n            </div>\r\n            <div class=\"force-buttons\">\r\n                <button id=\"forceNoveltyBtn\" style=\"background:#f59e0b;\">✨ Force Novelty</button>\r\n                <button id=\"forceStressBtn\" style=\"background:#ef4444;\">😰 Force Stress</button>\r\n                <button id=\"forceRewardBtn\" style=\"background:#10b981;\">🎁 Force Reward</button>\r\n            </div>\r\n            <div class=\"log-panel\" id=\"outputLog\">\r\n                <div class=\"log-entry\">⚡ Neurogenesis monitor ready. Output bindings will appear here.</div>\r\n            </div>\r\n        </div>\r\n\r\n        <!-- Middle: Inputs + Neurogenesis stats -->\r\n        <div class=\"card controls-card\">\r\n            <h3>🎮 Manual Inputs</h3>\r\n            <div id=\"inputControlsContainer\">Load a brain to see sliders</div>\r\n            <hr>\r\n            <h3>🧬 Neurogenesis Triggers</h3>\r\n            <div id=\"neuroStats\" class=\"stat-grid\" style=\"display:block\">\r\n                <div class=\"stat-card\"><h4>Novelty counter</h4><div class=\"stat-value\" id=\"noveltyCounter\">0.0</div><div class=\"badge\">threshold 3.0</div></div>\r\n                <div class=\"stat-card\"><h4>Stress counter</h4><div class=\"stat-value\" id=\"stressCounter\">0.0</div><div class=\"badge\">threshold 2.0</div></div>\r\n                <div class=\"stat-card\"><h4>Reward counter</h4><div class=\"stat-value\" id=\"rewardCounter\">0.0</div><div class=\"badge\">threshold 2.5</div></div>\r\n            </div>\r\n            <div id=\"newNeuronsList\" style=\"font-size:0.8rem; margin-top:8px; background:#0f172a; border-radius:8px; padding:6px;\">\r\n                <strong>🧪 New neurons:</strong> <span id=\"newNeuronNames\">—</span>\r\n            </div>\r\n            <hr>\r\n            <div id=\"outputBindingsInfo\" style=\"font-size:0.8rem; color:#94a3b8;\">No output bindings</div>\r\n            <div class=\"note\">💡 Neurogenesis: counters increase when core emotions exceed thresholds. New neurons are placed randomly across the canvas, avoiding overlap.</div>\r\n        </div>\r\n\r\n        <!-- Right: Analytics Dashboard -->\r\n        <div class=\"card dashboard-card\">\r\n            <h3>📈 Behaviour Analytics</h3>\r\n            <div class=\"stat-grid\">\r\n                <div class=\"stat-card\"><h4>Dominant Archetype</h4><div class=\"stat-value\" id=\"dominantArch\">—</div></div>\r\n                <div class=\"stat-card\"><h4>Neurons (total)</h4><div class=\"stat-value\" id=\"totalNeurons\">0</div></div>\r\n                <div class=\"stat-card\"><h4>Active Outputs</h4><div class=\"stat-value\" id=\"activeOutputCount\">0</div></div>\r\n            </div>\r\n            <div class=\"chart-container\">\r\n                <canvas id=\"activationsChart\" width=\"400\" height=\"180\" style=\"width:100%; height:180px;\"></canvas>\r\n            </div>\r\n            <div id=\"archetypeContainer\" style=\"margin-top: 12px; background:#0f172a; border-radius:12px; padding:8px;\">\r\n                <strong>📊 Archetype distribution (last 50 steps)</strong>\r\n                <div id=\"archetypeList\"></div>\r\n            </div>\r\n        </div>\r\n    </div>\r\n</div>\r\n\r\n<script>\r\n    // ---------- Global state ----------\r\n    let brainData = null;\r\n    let neurons = {};\r\n    let weights = {};\r\n    let state = {};\r\n    let outputBindings = [];\r\n    let inputNeuronNames = [];\r\n    let neuronList = [];\r\n    let positions = {};\r\n    let noveltyCounter = 0, stressCounter = 0, rewardCounter = 0;\r\n    let lastNeurogenesisTick = 0;\r\n    const NEURO_COOLDOWN = 10;\r\n    const NOVELTY_THRESH = 3.0, STRESS_THRESH = 2.0, REWARD_THRESH = 2.5;\r\n    let newNeuronsCreated = [];\r\n    let running = false, intervalId = null, ticks = 0;\r\n    let history = [];\r\n    const MAX_HISTORY = 150;\r\n    let trackedNeurons = ['hunger','happiness','anxiety','curiosity','satisfaction','cleanliness','sleepiness'];\r\n    const DECAY = 0.96, CONTRIB_SCALE = 0.12;\r\n    let canvas, ctx, canvasWidth = 800, canvasHeight = 500;\r\n    let lineChart = null;\r\n    const MIN_DIST = 70, X_MIN = 70, X_MAX = 950, Y_MIN = 70, Y_MAX = 690;\r\n\r\n    // ---- Helper functions ----\r\n    function hashCode(str) {\r\n        let h = 0;\r\n        for (let i = 0; i < str.length; i++) h = ((h << 5) - h) + str.charCodeAt(i);\r\n        return h;\r\n    }\r\n\r\n    function getRandomPosition() {\r\n        for (let attempt = 0; attempt < 100; attempt++) {\r\n            let x = X_MIN + Math.random() * (X_MAX - X_MIN);\r\n            let y = Y_MIN + Math.random() * (Y_MAX - Y_MIN);\r\n            let ok = true;\r\n            for (let n of neuronList) {\r\n                let p = positions[n];\r\n                if (!p) continue;\r\n                let dx = p.x - x, dy = p.y - y;\r\n                if (Math.hypot(dx, dy) < MIN_DIST) { ok = false; break; }\r\n            }\r\n            if (ok) return {x, y};\r\n        }\r\n        return {x: X_MIN + Math.random() * (X_MAX - X_MIN), y: Y_MIN + Math.random() * (Y_MAX - Y_MIN)};\r\n    }\r\n\r\n    // ---- Core simulation functions ----\r\n    function updateCountersFromState() {\r\n        let curiosity = state['curiosity'] || 50;\r\n        let seeFood = state['can_see_food'] || 0;\r\n        let noveltyIncr = (curiosity > 70 ? 0.8 : 0) + (seeFood > 80 ? 0.6 : 0);\r\n        noveltyCounter = Math.min(10, noveltyCounter + noveltyIncr * 0.2);\r\n        let anxiety = state['anxiety'] || 50;\r\n        let hunger = state['hunger'] || 50;\r\n        let stressIncr = (anxiety > 70 ? 1.0 : 0) + (hunger > 75 ? 0.6 : 0);\r\n        if (state['is_sick'] === true || state['is_sick'] > 50) stressIncr += 0.8;\r\n        stressCounter = Math.min(10, stressCounter + stressIncr * 0.2);\r\n        let satisfaction = state['satisfaction'] || 50;\r\n        let happiness = state['happiness'] || 50;\r\n        let rewardIncr = (satisfaction > 75 ? 0.9 : 0) + (happiness > 75 ? 0.7 : 0);\r\n        if (state['is_eating'] === true || state['is_eating'] > 50) rewardIncr += 0.5;\r\n        rewardCounter = Math.min(10, rewardCounter + rewardIncr * 0.2);\r\n        noveltyCounter *= 0.98; stressCounter *= 0.98; rewardCounter *= 0.98;\r\n    }\r\n\r\n    function createNeuron(triggerType) {\r\n        if (ticks - lastNeurogenesisTick < NEURO_COOLDOWN) return null;\r\n        if (neuronList.length > 80) return null;\r\n        let baseName = triggerType === 'novelty' ? 'novelty' : (triggerType === 'stress' ? 'stress' : 'reward');\r\n        let idx = 1;\r\n        let newName = `${baseName}_${idx}`;\r\n        while (neurons[newName]) { idx++; newName = `${baseName}_${idx}`; }\r\n        let pos = getRandomPosition();\r\n        positions[newName] = pos;\r\n        let isBinary = false;\r\n        let neuronType = 'hidden';\r\n        neurons[newName] = { type: neuronType, is_binary: isBinary, position: pos };\r\n        state[newName] = 50;\r\n        neuronList.push(newName);\r\n        let topNeurons = trackedNeurons.filter(n => state[n] !== undefined).sort((a,b)=>state[b]-state[a]).slice(0,3);\r\n        for (let target of topNeurons) {\r\n            let weight = (triggerType === 'reward' ? 0.6 : (triggerType === 'stress' ? -0.5 : 0.5));\r\n            weight += (Math.random() - 0.5)*0.3;\r\n            weight = Math.min(1.0, Math.max(-1.0, weight));\r\n            let key = `${newName}|${target}`;\r\n            weights[key] = weight;\r\n            let revKey = `${target}|${newName}`;\r\n            weights[revKey] = weight * 0.4;\r\n        }\r\n        if (triggerType === 'stress' && neurons['anxiety']) {\r\n            weights[`${newName}|anxiety`] = -0.7;\r\n            weights[`anxiety|${newName}`] = -0.3;\r\n        }\r\n        if (triggerType === 'reward' && neurons['satisfaction']) {\r\n            weights[`${newName}|satisfaction`] = 0.8;\r\n            weights[`satisfaction|${newName}`] = 0.3;\r\n        }\r\n        if (triggerType === 'novelty' && neurons['curiosity']) {\r\n            weights[`${newName}|curiosity`] = 0.6;\r\n            weights[`curiosity|${newName}`] = 0.2;\r\n        }\r\n        newNeuronsCreated.unshift(newName);\r\n        if (newNeuronsCreated.length > 12) newNeuronsCreated.pop();\r\n        logMessage(`🧬 Neurogenesis: new ${triggerType} neuron \"${newName}\" created at (${pos.x.toFixed(0)},${pos.y.toFixed(0)}). Connections added.`);\r\n        lastNeurogenesisTick = ticks;\r\n        return newName;\r\n    }\r\n\r\n    function checkNeurogenesis() {\r\n        updateCountersFromState();\r\n        let created = false;\r\n        if (noveltyCounter >= NOVELTY_THRESH && createNeuron('novelty')) { noveltyCounter = 0; created = true; }\r\n        if (stressCounter >= STRESS_THRESH && createNeuron('stress')) { stressCounter = 0; created = true; }\r\n        if (rewardCounter >= REWARD_THRESH && createNeuron('reward')) { rewardCounter = 0; created = true; }\r\n        if (created) updateUI();\r\n    }\r\n\r\n    function propagate() {\r\n        let newState = {...state};\r\n        for (let target of neuronList) {\r\n            if (inputNeuronNames.includes(target)) continue;\r\n            let cur = state[target];\r\n            let decayed = cur * DECAY + (1 - DECAY) * 50;\r\n            newState[target] = decayed;\r\n        }\r\n        for (let target of neuronList) {\r\n            if (inputNeuronNames.includes(target)) continue;\r\n            let sum = 0;\r\n            for (let src of neuronList) {\r\n                let w = weights[`${src}|${target}`];\r\n                if (w !== undefined) {\r\n                    let srcVal = state[src];\r\n                    if (typeof srcVal === 'boolean') srcVal = srcVal ? 100 : 0;\r\n                    sum += srcVal * w;\r\n                }\r\n            }\r\n            let delta = sum * CONTRIB_SCALE;\r\n            let newVal = newState[target] + delta;\r\n            newVal = Math.min(100, Math.max(0, newVal));\r\n            if (neurons[target] && neurons[target].is_binary) newVal = newVal > 50 ? 100 : 0;\r\n            newState[target] = newVal;\r\n        }\r\n        state = newState;\r\n    }\r\n\r\n    function applyInputsFromUI() {\r\n        for (let name of inputNeuronNames) {\r\n            let el = document.getElementById(`input_${name}`);\r\n            if (el) {\r\n                if (neurons[name] && neurons[name].is_binary) {\r\n                    state[name] = el.checked ? 100 : 0;\r\n                } else {\r\n                    let val = parseFloat(el.value);\r\n                    if (isNaN(val)) val = 50;\r\n                    state[name] = val;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    function checkOutputBindings() {\r\n        let now = Date.now() / 1000;\r\n        for (let b of outputBindings) {\r\n            let activation = state[b.neuron];\r\n            if (activation === undefined) continue;\r\n            let triggered = false;\r\n            let last = b.lastState !== undefined ? b.lastState : false;\r\n            if (b.mode === 'rising') triggered = (!last && activation >= b.threshold);\r\n            else if (b.mode === 'falling') triggered = (last && activation <= b.threshold);\r\n            else if (b.mode === 'above') triggered = (activation >= b.threshold);\r\n            else if (b.mode === 'below') triggered = (activation <= b.threshold);\r\n            else if (b.mode === 'change') triggered = (Math.abs(activation - last) > 10);\r\n            if (triggered && (now - (b.lastFire||0) >= (b.cooldown||0))) {\r\n                b.lastFire = now;\r\n                logOutput(b);\r\n            }\r\n            b.lastState = activation >= b.threshold;\r\n        }\r\n    }\r\n\r\n    function logOutput(binding) {\r\n        let logDiv = document.getElementById('outputLog');\r\n        let entry = document.createElement('div');\r\n        entry.className = 'log-entry';\r\n        let hook = binding.hook.replace('neuron_output_', '');\r\n        entry.innerHTML = `⚡ ${binding.neuron} → ${hook} (act: ${state[binding.neuron].toFixed(1)})`;\r\n        logDiv.prepend(entry);\r\n        if (logDiv.children.length > 40) logDiv.removeChild(logDiv.lastChild);\r\n    }\r\n\r\n    function logMessage(msg) {\r\n        let logDiv = document.getElementById('outputLog');\r\n        let entry = document.createElement('div');\r\n        entry.className = 'log-entry';\r\n        entry.innerHTML = `🧠 ${msg}`;\r\n        logDiv.prepend(entry);\r\n        if (logDiv.children.length > 40) logDiv.removeChild(logDiv.lastChild);\r\n    }\r\n\r\n    function step() {\r\n        applyInputsFromUI();\r\n        propagate();\r\n        ticks++;\r\n        checkNeurogenesis();\r\n        checkOutputBindings();\r\n        updateUI();\r\n    }\r\n\r\n    // ---- UI update functions ----\r\n    function drawNetwork() {\r\n        if (!ctx) return;\r\n        ctx.clearRect(0, 0, canvasWidth, canvasHeight);\r\n        for (let [key, w] of Object.entries(weights)) {\r\n            let [s, t] = key.split('|');\r\n            if (!positions[s] || !positions[t]) continue;\r\n            let p1 = positions[s], p2 = positions[t];\r\n            let sx = (p1.x / 1024) * canvasWidth, sy = (p1.y / 768) * canvasHeight;\r\n            let tx = (p2.x / 1024) * canvasWidth, ty = (p2.y / 768) * canvasHeight;\r\n            ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(tx, ty);\r\n            let intensity = Math.min(1, Math.abs(w));\r\n            ctx.strokeStyle = w >= 0 ? `rgba(80,200,100,${0.3+intensity*0.6})` : `rgba(220,80,80,${0.3+intensity*0.6})`;\r\n            ctx.lineWidth = 1 + intensity * 3;\r\n            ctx.stroke();\r\n        }\r\n        for (let name of neuronList) {\r\n            let pos = positions[name];\r\n            if (!pos) continue;\r\n            let x = (pos.x / 1024) * canvasWidth, y = (pos.y / 768) * canvasHeight;\r\n            let act = state[name] ?? 50;\r\n            let isBin = neurons[name]?.is_binary;\r\n            let radius = 14;\r\n            let hue = isBin ? (act > 50 ? 120 : 0) : (act < 30 ? 0 : (act < 70 ? 60 : 120));\r\n            ctx.beginPath(); ctx.arc(x, y, radius, 0, 2*Math.PI);\r\n            ctx.fillStyle = `hsl(${hue}, 70%, 55%)`; ctx.fill();\r\n            ctx.strokeStyle = '#fff'; ctx.stroke();\r\n            ctx.fillStyle = '#e2e8f0'; ctx.font = '10px monospace';\r\n            ctx.fillText(name.replace(/_/g, ' ').substring(0,12), x-20, y-10);\r\n            ctx.fillStyle = 'white'; ctx.font = 'bold 11px monospace';\r\n            let txt = isBin ? (act > 50 ? 'ON' : 'OFF') : act.toFixed(0);\r\n            ctx.fillText(txt, x-8, y+4);\r\n        }\r\n    }\r\n\r\n    function recordHistory() {\r\n        let entry = { timestamp: ticks, values: {} };\r\n        for (let n of trackedNeurons) if (state[n] !== undefined) entry.values[n] = state[n];\r\n        history.push(entry);\r\n        if (history.length > MAX_HISTORY) history.shift();\r\n    }\r\n\r\n    function updateChart() {\r\n        if (!lineChart) {\r\n            let ctxC = document.getElementById('activationsChart').getContext('2d');\r\n            lineChart = new Chart(ctxC, {\r\n                type: 'line',\r\n                data: { labels: [], datasets: [] },\r\n                options: { responsive: true, maintainAspectRatio: true, scales: { y: { min: 0, max: 100 } } }\r\n            });\r\n        }\r\n        let labels = history.map(h => h.timestamp);\r\n        let datasets = [];\r\n        for (let n of trackedNeurons) {\r\n            let data = history.map(h => h.values[n] ?? 50);\r\n            datasets.push({\r\n                label: n,\r\n                data: data,\r\n                borderColor: `hsl(${Math.abs(hashCode(n) % 360)}, 70%, 60%)`,\r\n                fill: false,\r\n                tension: 0.2,\r\n                pointRadius: 1\r\n            });\r\n        }\r\n        lineChart.data.labels = labels;\r\n        lineChart.data.datasets = datasets;\r\n        lineChart.update();\r\n    }\r\n\r\n    function getArchetype(vals) {\r\n        let best = null, bestV = -1;\r\n        for (let n of trackedNeurons) {\r\n            let v = vals[n];\r\n            if (v !== undefined && v > bestV) { bestV = v; best = n; }\r\n        }\r\n        return best || 'unknown';\r\n    }\r\n\r\n    function updateArchetypeStats() {\r\n        if (history.length === 0) return;\r\n        let recent = history.slice(-50);\r\n        let counts = {};\r\n        for (let h of recent) {\r\n            let a = getArchetype(h.values);\r\n            counts[a] = (counts[a] || 0) + 1;\r\n        }\r\n        let total = recent.length;\r\n        let dom = Object.entries(counts).sort((a,b)=>b[1]-a[1])[0];\r\n        document.getElementById('dominantArch').innerHTML = dom ? `${dom[0]} (${Math.round(dom[1]/total*100)}%)` : '—';\r\n        let listDiv = document.getElementById('archetypeList');\r\n        listDiv.innerHTML = '';\r\n        for (let [arch, cnt] of Object.entries(counts)) {\r\n            let pct = (cnt/total*100).toFixed(0);\r\n            let div = document.createElement('div'); div.className = 'archetype-item';\r\n            div.innerHTML = `<span style=\"width:100px\">${arch}</span><div style=\"flex:1; background:#2d3748; border-radius:8px;\"><div class=\"archetype-fill\" style=\"width:${pct}%;\"></div></div><span>${pct}%</span>`;\r\n            listDiv.appendChild(div);\r\n        }\r\n    }\r\n\r\n    function updateUI() {\r\n        drawNetwork();\r\n        updateInputSliders();\r\n        updateOutputBindingsDisplay();\r\n        document.getElementById('noveltyCounter').innerHTML = noveltyCounter.toFixed(2);\r\n        document.getElementById('stressCounter').innerHTML = stressCounter.toFixed(2);\r\n        document.getElementById('rewardCounter').innerHTML = rewardCounter.toFixed(2);\r\n        document.getElementById('totalNeurons').innerHTML = neuronList.length;\r\n        document.getElementById('newNeuronNames').innerHTML = newNeuronsCreated.length ? newNeuronsCreated.join(', ') : '—';\r\n        let now = Date.now()/1000;\r\n        let active = outputBindings.filter(b => (now - (b.lastFire||0)) < 1).length;\r\n        document.getElementById('activeOutputCount').innerHTML = active;\r\n        document.getElementById('tickCounter').innerText = `Ticks: ${ticks}`;\r\n        recordHistory();\r\n        updateChart();\r\n        updateArchetypeStats();\r\n    }\r\n\r\n    function updateInputSliders() {\r\n        for (let name of inputNeuronNames) {\r\n            let el = document.getElementById(`input_${name}`);\r\n            if (el) {\r\n                let val = state[name];\r\n                if (neurons[name] && neurons[name].is_binary) {\r\n                    el.checked = val > 50;\r\n                } else {\r\n                    el.value = val;\r\n                    let span = el.nextElementSibling;\r\n                    if (span && span.tagName === 'SPAN') span.innerText = val.toFixed(0);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    function updateOutputBindingsDisplay() {\r\n        let info = document.getElementById('outputBindingsInfo');\r\n        if (!outputBindings.length) {\r\n            info.innerHTML = '📌 No output bindings.';\r\n            return;\r\n        }\r\n        let html = '<strong>🔌 Output bindings</strong><ul style=\"margin:5px 0 0 15px;\">';\r\n        for (let b of outputBindings) html += `<li>${b.neuron} → ${b.hook.replace('neuron_output_','')} (thresh=${b.threshold})</li>`;\r\n        html += '</ul>';\r\n        info.innerHTML = html;\r\n    }\r\n\r\n    function buildInputControls() {\r\n        let container = document.getElementById('inputControlsContainer');\r\n        container.innerHTML = '';\r\n        for (let name of inputNeuronNames) {\r\n            let val = state[name] ?? 50;\r\n            let isBinary = neurons[name]?.is_binary;\r\n            let div = document.createElement('div'); div.className = 'input-group';\r\n            let label = document.createElement('label'); label.innerText = name.replace(/_/g,' ');\r\n            div.appendChild(label);\r\n            if (isBinary) {\r\n                let cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = val > 50;\r\n                cb.id = `input_${name}`;\r\n                cb.addEventListener('change', () => { if(!running) step(); else applyInputsFromUI(); });\r\n                div.appendChild(cb);\r\n            } else {\r\n                let slider = document.createElement('input'); slider.type = 'range'; slider.min = 0; slider.max = 100; slider.step = 1;\r\n                slider.value = val; slider.id = `input_${name}`;\r\n                let span = document.createElement('span'); span.innerText = val.toFixed(0);\r\n                slider.addEventListener('input', (e) => { span.innerText = e.target.value; if(!running) step(); else applyInputsFromUI(); });\r\n                div.appendChild(slider); div.appendChild(span);\r\n            }\r\n            container.appendChild(div);\r\n        }\r\n    }\r\n\r\n    // ---- Brain loading and reset ----\r\n    function parseBrain(data) {\r\n        neurons = {};\r\n        weights = {};\r\n        state = {};\r\n        outputBindings = [];\r\n        positions = {};\r\n        const neuronsObj = data.neurons || {};\r\n        for (let [name, info] of Object.entries(neuronsObj)) {\r\n            let ntype = info.type || info.neuron_type || 'hidden';\r\n            let isBinary = info.is_binary === true;\r\n            let pos = info.position;\r\n            if (pos && Array.isArray(pos) && pos.length >= 2) positions[name] = {x: pos[0], y: pos[1]};\r\n            else if (data.neuron_positions && data.neuron_positions[name]) {\r\n                let p = data.neuron_positions[name];\r\n                positions[name] = {x: p[0], y: p[1]};\r\n            } else positions[name] = {x: Math.random()*600+100, y: Math.random()*400+50};\r\n            neurons[name] = { type: ntype, is_binary: isBinary, position: positions[name] };\r\n        }\r\n        let conns = data.connections || data.weights || {};\r\n        if (Array.isArray(conns)) {\r\n            for (let c of conns) if(c.source && c.target) weights[`${c.source}|${c.target}`] = c.weight ?? 0.5;\r\n        } else {\r\n            for (let [key, w] of Object.entries(conns)) {\r\n                let parts = key.includes('->') ? key.split('->') : key.split('|');\r\n                if(parts.length === 2) weights[`${parts[0]}|${parts[1]}`] = parseFloat(w);\r\n            }\r\n        }\r\n        let initState = data.state || {};\r\n        for (let name of Object.keys(neurons)) {\r\n            if (initState[name] !== undefined) state[name] = parseFloat(initState[name]);\r\n            else if (neurons[name].is_binary) state[name] = 0;\r\n            else state[name] = 50;\r\n        }\r\n        outputBindings = (data.output_bindings || []).map(b => ({\r\n            neuron: b.neuron_name, hook: b.output_hook, threshold: b.threshold,\r\n            mode: b.trigger_mode, cooldown: b.cooldown, params: b.hook_params || {},\r\n            lastFire: 0, lastState: false\r\n        }));\r\n        let coreSet = new Set(['hunger','happiness','cleanliness','sleepiness','satisfaction','anxiety','curiosity']);\r\n        inputNeuronNames = [];\r\n        for (let name of Object.keys(neurons)) {\r\n            if (neurons[name].type === 'sensor' || coreSet.has(name) || name === 'can_see_food') inputNeuronNames.push(name);\r\n        }\r\n        neuronList = Object.keys(neurons);\r\n        trackedNeurons = trackedNeurons.filter(n => neurons[n]);\r\n        if (trackedNeurons.length === 0) trackedNeurons = neuronList.slice(0,5);\r\n    }\r\n\r\n    function resetSimulation() {\r\n        if (brainData && brainData.state) {\r\n            for (let n in brainData.state) if (state[n] !== undefined) state[n] = brainData.state[n];\r\n        }\r\n        for (let n in neurons) if (state[n] === undefined) state[n] = neurons[n].is_binary ? 0 : 50;\r\n        noveltyCounter = 0; stressCounter = 0; rewardCounter = 0;\r\n        ticks = 0; history = []; lastNeurogenesisTick = 0;\r\n        for (let b of outputBindings) { b.lastFire = 0; b.lastState = false; }\r\n        updateUI();\r\n        logMessage(\"Reset to initial state.\");\r\n    }\r\n\r\n    function exportBrain() {\r\n        if (!brainData && neuronList.length === 0) return;\r\n        let exportData = {\r\n            version: \"2.0\",\r\n            format: \"dosidicus\",\r\n            metadata: { name: \"Exported Brain\", author: \"Playground\", version: \"1.0\", created: new Date().toISOString() },\r\n            neurons: {},\r\n            connections: {},\r\n            state: {},\r\n            output_bindings: outputBindings,\r\n            neuron_positions: {},\r\n            weights: {},\r\n            required_complete: true\r\n        };\r\n        for (let name of neuronList) {\r\n            let pos = positions[name];\r\n            let ntype = neurons[name]?.type || 'hidden';\r\n            let isBin = neurons[name]?.is_binary || false;\r\n            exportData.neurons[name] = { position: [pos.x, pos.y], type: ntype, is_binary: isBin, is_core: false, is_sensor: (inputNeuronNames.includes(name)) };\r\n            exportData.neuron_positions[name] = [pos.x, pos.y];\r\n            exportData.state[name] = state[name];\r\n        }\r\n        for (let [key, w] of Object.entries(weights)) {\r\n            let [s,t] = key.split('|');\r\n            exportData.connections[`${s}->${t}`] = w;\r\n            exportData.weights[`${s}|${t}`] = w;\r\n        }\r\n        let blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});\r\n        let url = URL.createObjectURL(blob);\r\n        let a = document.createElement('a');\r\n        a.href = url;\r\n        a.download = `brain_export_${Date.now()}.json`;\r\n        a.click();\r\n        URL.revokeObjectURL(url);\r\n        logMessage(\"📁 Brain exported successfully\");\r\n    }\r\n\r\n    function forceNeurogenesis(type) {\r\n        if (createNeuron(type)) updateUI();\r\n        else logMessage(`Could not create ${type} neuron (cooldown or limit)`);\r\n    }\r\n\r\n    function loadBrainFromFile(file) {\r\n        let reader = new FileReader();\r\n        reader.onload = (e) => {\r\n            try {\r\n                let data = JSON.parse(e.target.result);\r\n                brainData = data;\r\n                parseBrain(data);\r\n                resetSimulation();\r\n                buildInputControls();\r\n                updateUI();\r\n                logMessage(`Loaded brain: ${neuronList.length} neurons, ${Object.keys(weights).length} connections`);\r\n            } catch (err) {\r\n                alert(\"Invalid JSON: \" + err.message);\r\n            }\r\n        };\r\n        reader.readAsText(file);\r\n    }\r\n\r\n    // ---- Initialization ----\r\n    canvas = document.getElementById('brainCanvas');\r\n    ctx = canvas.getContext('2d');\r\n    canvas.width = canvasWidth;\r\n    canvas.height = canvasHeight;\r\n\r\n    // Event listeners\r\n    document.getElementById('loadBtn').addEventListener('click', () => document.getElementById('brainFileInput').click());\r\n    document.getElementById('brainFileInput').addEventListener('change', (e) => { if (e.target.files.length) loadBrainFromFile(e.target.files[0]); });\r\n    document.getElementById('exportBtn').addEventListener('click', exportBrain);\r\n    document.getElementById('stepBtn').addEventListener('click', () => { if (!running) step(); });\r\n    document.getElementById('runBtn').addEventListener('click', () => {\r\n        if (running) return;\r\n        running = true;\r\n        intervalId = setInterval(() => { if (running) step(); }, 150);\r\n    });\r\n    document.getElementById('stopBtn').addEventListener('click', () => { running = false; if (intervalId) clearInterval(intervalId); intervalId = null; });\r\n    document.getElementById('resetBtn').addEventListener('click', resetSimulation);\r\n    document.getElementById('forceNoveltyBtn').addEventListener('click', () => forceNeurogenesis('novelty'));\r\n    document.getElementById('forceStressBtn').addEventListener('click', () => forceNeurogenesis('stress'));\r\n    document.getElementById('forceRewardBtn').addEventListener('click', () => forceNeurogenesis('reward'));\r\n\r\n    function initEmpty() { drawNetwork(); document.getElementById('inputControlsContainer').innerHTML = '<i>Load a brain JSON to begin</i>'; }\r\n    initEmpty();\r\n</script>\r\n</body>\r\n</html>"
  },
  {
    "path": "extras/brain_2_keras.py",
    "content": "import json\nimport numpy as np\nimport os\nimport argparse\n\n# Try to import keras, handle if not installed\ntry:\n    import keras\n    from keras import ops\nexcept ImportError:\n    print(\"Error: Keras v3 is required. Please install it via 'pip install keras'\")\n    exit(1)\n\nclass HybridActivation(keras.layers.Layer):\n    \"\"\"\n    Applies different activation functions to different slices of the input tensor.\n    This is necessary because the Brain Designer allows per-neuron activation settings.\n    \"\"\"\n    def __init__(self, activation_map, units, **kwargs):\n        \"\"\"\n        activation_map: dict mapping activation name ('relu', 'sigmoid') to list of indices\n        units: total number of neurons\n        \"\"\"\n        super().__init__(**kwargs)\n        self.activation_map = activation_map\n        self.units = units\n        \n        # Create boolean masks for each activation type for vectorized application\n        self.masks = {}\n        for act_name, indices in activation_map.items():\n            mask = np.zeros(units, dtype=\"float32\")\n            mask[indices] = 1.0\n            self.masks[act_name] = mask\n\n    def call(self, inputs):\n        output = ops.zeros_like(inputs)\n        \n        for act_name, mask in self.masks.items():\n            # Convert mask to tensor\n            mask_tensor = ops.convert_to_tensor(mask)\n            \n            # Apply standard keras activations\n            if act_name == 'relu':\n                activated = keras.activations.relu(inputs)\n            elif act_name == 'sigmoid':\n                activated = keras.activations.sigmoid(inputs)\n            elif act_name == 'tanh':\n                activated = keras.activations.tanh(inputs)\n            elif act_name == 'linear':\n                activated = inputs\n            else:\n                # Default to linear if unknown\n                activated = inputs\n            \n            # Add masked contribution to output\n            output = output + (activated * mask_tensor)\n            \n        return output\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\n            \"activation_map\": self.activation_map,\n            \"units\": self.units\n        })\n        return config\n\nclass BrainCell(keras.layers.Layer):\n    \"\"\"\n    A Custom RNN Cell that mimics the exact topology of the JSON brain design.\n    \"\"\"\n    def __init__(self, input_units, state_units, \n                 input_weights_init, recurrent_weights_init, \n                 activation_map, output_indices, **kwargs):\n        super().__init__(**kwargs)\n        self.input_units = input_units\n        self.state_units = state_units\n        self.state_size = state_units\n        self.output_size = len(output_indices) # We only output specific neurons\n        self.output_indices = output_indices\n        \n        # Initializers (passed as numpy arrays from the converter)\n        self.w_in_init = input_weights_init\n        self.w_rec_init = recurrent_weights_init\n        self.activation_map = activation_map\n\n    def build(self, input_shape):\n        # Input Kernel (Input Neurons -> State Neurons)\n        self.kernel = self.add_weight(\n            shape=(self.input_units, self.state_units),\n            initializer=keras.initializers.Constant(self.w_in_init),\n            name=\"input_kernel\",\n            trainable=True # Set to False to freeze the design structure exactly\n        )\n        \n        # Recurrent Kernel (State Neurons -> State Neurons)\n        self.recurrent_kernel = self.add_weight(\n            shape=(self.state_units, self.state_units),\n            initializer=keras.initializers.Constant(self.w_rec_init),\n            name=\"recurrent_kernel\",\n            trainable=True\n        )\n        \n        self.bias = self.add_weight(\n            shape=(self.state_units,),\n            initializer=\"zeros\",\n            name=\"bias\"\n        )\n        \n        self.hybrid_activation = HybridActivation(self.activation_map, self.state_units)\n        self.built = True\n\n    def call(self, inputs, states):\n        prev_output = states[0]\n        \n        # Standard RNN math: h' = Act(Wx + Uh + b)\n        # inputs shape: (batch, input_units)\n        # prev_output shape: (batch, state_units)\n        \n        h_in = ops.matmul(inputs, self.kernel)\n        h_rec = ops.matmul(prev_output, self.recurrent_kernel)\n        \n        total_input = h_in + h_rec + self.bias\n        \n        # Apply per-neuron activations\n        output = self.hybrid_activation(total_input)\n        \n        return output, [output]\n\n    def get_config(self):\n        config = super().get_config()\n        # Note: We don't serialize large numpy arrays in config typically, \n        # but for reconstruction within this tool it's handled via the loader.\n        # For saving/loading the Keras model strictly, rely on model.save() \n        # which handles weight serialization automatically.\n        config.update({\n            \"input_units\": self.input_units,\n            \"state_units\": self.state_units,\n            \"output_indices\": self.output_indices,\n            \"activation_map\": self.activation_map,\n            # We cannot serialize numpy arrays directly in config for JSON safety\n            # If rebuilding from config, standard initializers would be used\n            # unless we implement custom logic.\n        })\n        return config\n\ndef load_brain_json(filepath):\n    \"\"\"Parses the Brain Designer JSON file.\"\"\"\n    with open(filepath, 'r') as f:\n        data = json.load(f)\n    return data\n\ndef parse_design(data):\n    \"\"\"\n    Analyzes the JSON data to build matrices.\n    Returns: input_ids, state_ids, output_ids, W_in, W_rec, act_map\n    \"\"\"\n    \n    # 1. Identify Neurons\n    neurons = data.get('neurons', {})\n    if isinstance(neurons, list): # Handle array format\n        # Convert list to dict map\n        temp = {}\n        for n in neurons:\n            temp[n['name']] = n\n        neurons = temp\n    \n    # Sort into Input (Sensors) vs State (Everything else)\n    # Note: In BrainDesigner logic, sensors are just neurons with 0 inputs typically, \n    # but strictly speaking, they are the interface to the outside world.\n    \n    input_names = []\n    state_names = []\n    \n    # Keras needs fixed indices\n    name_to_idx = {}\n    \n    # Logic to distinguish inputs:\n    # Look for 'sensor' type or specific names in standard inputs\n    known_inputs = [\n        'external_stimulus', 'plant_proximity', 'threat_level', \n        'pursuing_food', 'is_sick', 'is_fleeing', 'is_eating', \n        'is_sleeping', 'is_startled', 'can_see_food'\n    ]\n    \n    for name, props in neurons.items():\n        n_type = props.get('type', props.get('neuron_type', 'hidden')).lower()\n        \n        # Determine if it's an input source or a stateful neuron\n        if n_type == 'sensor' or name in known_inputs:\n            input_names.append(name)\n        else:\n            state_names.append(name)\n            \n    # Sort for determinism\n    input_names.sort()\n    state_names.sort()\n    \n    input_map = {name: i for i, name in enumerate(input_names)}\n    state_map = {name: i for i, name in enumerate(state_names)}\n    \n    print(f\"Parsed {len(input_names)} inputs and {len(state_names)} state neurons.\")\n    \n    # 2. Build Matrices\n    n_in = len(input_names)\n    n_state = len(state_names)\n    \n    W_in = np.zeros((n_in, n_state), dtype=\"float32\")\n    W_rec = np.zeros((n_state, n_state), dtype=\"float32\")\n    \n    # Helper to find where a name belongs\n    def get_loc(name):\n        if name in state_map: return ('state', state_map[name])\n        if name in input_map: return ('input', input_map[name])\n        return (None, -1)\n\n    # Parse Connections\n    connections = data.get('connections', [])\n    \n    # Normalize connection format (list of dicts vs dict of strings)\n    conn_list = []\n    if isinstance(connections, dict):\n        for k, w in connections.items():\n            if '->' in k: s, t = k.split('->')\n            elif '|' in k: s, t = k.split('|')\n            else: continue\n            conn_list.append((s, t, w))\n    else:\n        for c in connections:\n            conn_list.append((c['source'], c['target'], c['weight']))\n            \n    for source, target, weight in conn_list:\n        # We only care about connections going TO a state neuron.\n        # Connections TO an input neuron are ignored (inputs are forced).\n        \n        target_type, t_idx = get_loc(target)\n        if target_type != 'state':\n            continue\n            \n        source_type, s_idx = get_loc(source)\n        \n        if source_type == 'input':\n            W_in[s_idx, t_idx] = weight\n        elif source_type == 'state':\n            W_rec[s_idx, t_idx] = weight\n            \n    # 3. Activation Mapping\n    # Group state indices by activation function\n    act_map = {\n        'relu': [], 'sigmoid': [], 'tanh': [], 'linear': []\n    }\n    \n    for name in state_names:\n        idx = state_map[name]\n        props = neurons[name]\n        \n        # Default logic based on type if explicit activation not found\n        act = props.get('activation')\n        if not act:\n            n_type = props.get('type', props.get('neuron_type', 'hidden')).lower()\n            if n_type == 'output': act = 'sigmoid'\n            elif n_type == 'core': act = 'relu'\n            elif props.get('is_binary'): act = 'step' # approximate step with sigmoid*100 or tanh\n            else: act = 'relu'\n            \n        if act == 'step': act = 'sigmoid' # Keras doesn't have hard step usually, mapping to sigmoid\n        \n        if act not in act_map:\n            act_map[act] = []\n        act_map[act].append(idx)\n\n    # 4. Output Indices\n    # By default, treat 'output' type or 'core' type as model outputs\n    output_indices = []\n    for name in state_names:\n        n_type = neurons[name].get('type', neurons[name].get('neuron_type', '')).lower()\n        if n_type in ['output', 'core']:\n            output_indices.append(state_map[name])\n            \n    return n_in, n_state, W_in, W_rec, act_map, output_indices, input_names, state_names\n\ndef convert_to_keras_model(json_path, output_path):\n    print(f\"Loading {json_path}...\")\n    data = load_brain_json(json_path)\n    \n    # Parse topology\n    n_in, n_state, W_in, W_rec, act_map, out_idx, in_names, st_names = parse_design(data)\n    \n    # Create the custom cell\n    # Note: We must pass weights as list to `Constant` initializer or load them after.\n    # Here we pass them to the constructor to initialize the layers.\n    cell = BrainCell(\n        input_units=n_in,\n        state_units=n_state,\n        input_weights_init=W_in,\n        recurrent_weights_init=W_rec,\n        activation_map=act_map,\n        output_indices=out_idx\n    )\n    \n    # Create the RNN Layer\n    # return_sequences=True gives output at every time step\n    rnn_layer = keras.layers.RNN(cell, return_sequences=True, name=\"brain_rnn\")\n    \n    # Define Input\n    # Shape: (None, input_features) -> Timesteps is flexible\n    inputs = keras.Input(shape=(None, n_in), name=\"sensor_inputs\")\n    \n    # Get RNN outputs (this returns the full state vector at every step)\n    whole_state_sequence = rnn_layer(inputs)\n    \n    # If we want to filter only specific \"Output\" neurons:\n    # We use a Lambda layer or slicing. Keras slicing layer is cleaner.\n    if out_idx:\n        # Sort indices to ensure deterministic output order\n        out_idx.sort() \n        def filter_outputs(x):\n            return x[:, :, out_idx] # Batch, Time, Indices\n        \n        # Note: Lambda layers can be tricky for saving/loading without custom objects.\n        # But `ops.take` or slicing is fine.\n        final_output = keras.layers.Lambda(\n            lambda x: ops.take(x, indices=out_idx, axis=-1),\n            name=\"filter_outputs\"\n        )(whole_state_sequence)\n    else:\n        final_output = whole_state_sequence\n\n    model = keras.Model(inputs=inputs, outputs=final_output, name=\"DosidicusBrain\")\n    \n    # Compile just to finalize\n    model.compile(loss=\"mse\", optimizer=\"adam\")\n    \n    model.summary()\n    \n    # Save\n    print(f\"Saving Keras model to {output_path}...\")\n    model.save(output_path)\n    print(\"Done.\")\n    \n    # Print input mapping for the user\n    print(\"\\n--- Input Mapping (Index: Name) ---\")\n    for i, name in enumerate(in_names):\n        print(f\"{i}: {name}\")\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Convert Dosidicus Brain JSON to Keras v3 Model\")\n    parser.add_argument(\"input\", help=\"Path to input JSON file\")\n    parser.add_argument(\"output\", help=\"Path to output .keras file\")\n    \n    args = parser.parse_args()\n    \n    if not os.path.exists(args.input):\n        print(f\"Error: Input file '{args.input}' not found.\")\n    else:\n        convert_to_keras_model(args.input, args.output)\n"
  },
  {
    "path": "headless/HeadlessLauncher.jsx",
    "content": "import { useState, useEffect } from 'react';\n\nconst scenarios = {\n  none: { name: '⚙️ Custom', desc: 'Use manual settings without predefined events', ticks: null },\n  balanced: { name: '⚖️ Balanced', desc: 'Standard training with normal event rates', ticks: 10000 },\n  stress_test: { name: '😰 Stress Test', desc: 'High anxiety to develop resilience neurons', ticks: 5000 },\n  reward_rich: { name: '🎁 Reward Rich', desc: 'Frequent positive outcomes for reward pathways', ticks: 8000 },\n  novelty_exploration: { name: '🔍 Novelty', desc: 'High curiosity with new object encounters', ticks: 6000 },\n  endurance: { name: '🏃 Endurance', desc: 'Long-duration training session', ticks: 50000 },\n};\n\nexport default function HeadlessLauncher() {\n  const [brainFile, setBrainFile] = useState('');\n  const [outputFile, setOutputFile] = useState('');\n  const [ticks, setTicks] = useState(10000);\n  const [progress, setProgress] = useState(500);\n  const [learningRate, setLearningRate] = useState('');\n  const [maxNeurons, setMaxNeurons] = useState('');\n  const [neurogenesis, setNeurogenesis] = useState(true);\n  const [quietMode, setQuietMode] = useState(false);\n  const [scenario, setScenario] = useState('none');\n  const [copied, setCopied] = useState('');\n\n  const selectScenario = (key) => {\n    setScenario(key);\n    if (scenarios[key].ticks) {\n      setTicks(scenarios[key].ticks);\n    }\n  };\n\n  const getCommand = () => {\n    let cmd = 'python headless_trainer.py';\n    if (brainFile) cmd += ` --brain \"${brainFile}\"`;\n    if (scenario !== 'none') cmd += ` --scenario ${scenario}`;\n    cmd += ` --ticks ${ticks}`;\n    if (outputFile) cmd += ` --output \"${outputFile}\"`;\n    if (progress !== 500) cmd += ` --progress ${progress}`;\n    if (learningRate) cmd += ` --learning-rate ${learningRate}`;\n    if (maxNeurons) cmd += ` --max-neurons ${maxNeurons}`;\n    if (!neurogenesis) cmd += ` --neurogenesis False`;\n    if (quietMode) cmd += ` --quiet`;\n    return cmd;\n  };\n\n  const copyToClipboard = (text, label) => {\n    navigator.clipboard.writeText(text);\n    setCopied(label);\n    setTimeout(() => setCopied(''), 2000);\n  };\n\n  const estTime = (ticks / 30000).toFixed(1);\n  const estNeuronsMin = Math.floor(ticks / 2000);\n  const estNeuronsMax = Math.min(Math.floor(ticks / 500), 100);\n  const estHebbian = Math.floor(ticks / 30);\n\n  return (\n    <div className=\"min-h-screen bg-slate-900 text-gray-100 p-4\">\n      <div className=\"max-w-3xl mx-auto\">\n        {/* Header */}\n        <div className=\"text-center mb-6 p-4 bg-slate-800 rounded-xl border border-slate-700\">\n          <h1 className=\"text-2xl font-bold bg-gradient-to-r from-rose-400 to-pink-300 bg-clip-text text-transparent\">\n            🦑 Dosidicus-2 Headless Trainer\n          </h1>\n          <p className=\"text-gray-400 text-sm mt-1\">Configure and launch accelerated brain training</p>\n        </div>\n\n        {/* Brain Config */}\n        <div className=\"bg-slate-800 rounded-lg p-4 mb-4 border border-slate-700\">\n          <h2 className=\"text-rose-400 font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"w-1 h-5 bg-rose-400 rounded\" />\n            Brain Configuration\n          </h2>\n          <div className=\"grid grid-cols-2 gap-3\">\n            <div>\n              <label className=\"text-xs text-gray-400\">Brain File (JSON)</label>\n              <input\n                type=\"text\"\n                value={brainFile}\n                onChange={(e) => setBrainFile(e.target.value)}\n                placeholder=\"path/to/brain.json\"\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n            <div>\n              <label className=\"text-xs text-gray-400\">Output File</label>\n              <input\n                type=\"text\"\n                value={outputFile}\n                onChange={(e) => setOutputFile(e.target.value)}\n                placeholder=\"trained_brain.json\"\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n          </div>\n        </div>\n\n        {/* Training Params */}\n        <div className=\"bg-slate-800 rounded-lg p-4 mb-4 border border-slate-700\">\n          <h2 className=\"text-rose-400 font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"w-1 h-5 bg-rose-400 rounded\" />\n            Training Parameters\n          </h2>\n          <div className=\"grid grid-cols-2 gap-3 mb-4\">\n            <div>\n              <label className=\"text-xs text-gray-400\">Training Ticks</label>\n              <input\n                type=\"number\"\n                value={ticks}\n                onChange={(e) => setTicks(parseInt(e.target.value) || 1000)}\n                min={100}\n                step={1000}\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n            <div>\n              <label className=\"text-xs text-gray-400\">Progress Interval</label>\n              <input\n                type=\"number\"\n                value={progress}\n                onChange={(e) => setProgress(parseInt(e.target.value) || 0)}\n                min={0}\n                step={100}\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n          </div>\n          \n          <div className=\"grid grid-cols-3 gap-2\">\n            <div className=\"bg-slate-900 rounded p-3 text-center\">\n              <div className=\"text-xl font-bold text-rose-400\">{estTime}s</div>\n              <div className=\"text-xs text-gray-500\">Est. Time</div>\n            </div>\n            <div className=\"bg-slate-900 rounded p-3 text-center\">\n              <div className=\"text-xl font-bold text-rose-400\">~{estNeuronsMin}-{estNeuronsMax}</div>\n              <div className=\"text-xs text-gray-500\">New Neurons</div>\n            </div>\n            <div className=\"bg-slate-900 rounded p-3 text-center\">\n              <div className=\"text-xl font-bold text-rose-400\">~{estHebbian}</div>\n              <div className=\"text-xs text-gray-500\">Hebbian Updates</div>\n            </div>\n          </div>\n        </div>\n\n        {/* Scenarios */}\n        <div className=\"bg-slate-800 rounded-lg p-4 mb-4 border border-slate-700\">\n          <h2 className=\"text-rose-400 font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"w-1 h-5 bg-rose-400 rounded\" />\n            Training Scenario\n          </h2>\n          <div className=\"grid grid-cols-3 gap-2\">\n            {Object.entries(scenarios).map(([key, info]) => (\n              <button\n                key={key}\n                onClick={() => selectScenario(key)}\n                className={`p-2 rounded border text-left transition-all text-sm ${\n                  scenario === key\n                    ? 'border-emerald-400 bg-emerald-400/10'\n                    : 'border-slate-600 bg-slate-900 hover:border-rose-400'\n                }`}\n              >\n                <div className=\"font-medium\">{info.name}</div>\n                <div className=\"text-xs text-gray-500 mt-1\">{info.desc}</div>\n                {info.ticks && (\n                  <div className=\"text-xs text-amber-400 mt-1\">{info.ticks.toLocaleString()} ticks</div>\n                )}\n              </button>\n            ))}\n          </div>\n        </div>\n\n        {/* Advanced */}\n        <div className=\"bg-slate-800 rounded-lg p-4 mb-4 border border-slate-700\">\n          <h2 className=\"text-rose-400 font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"w-1 h-5 bg-rose-400 rounded\" />\n            Advanced Options\n          </h2>\n          <div className=\"grid grid-cols-2 gap-3 mb-3\">\n            <div>\n              <label className=\"text-xs text-gray-400\">Learning Rate</label>\n              <input\n                type=\"number\"\n                value={learningRate}\n                onChange={(e) => setLearningRate(e.target.value)}\n                placeholder=\"0.1\"\n                step={0.01}\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n            <div>\n              <label className=\"text-xs text-gray-400\">Max Neurons</label>\n              <input\n                type=\"number\"\n                value={maxNeurons}\n                onChange={(e) => setMaxNeurons(e.target.value)}\n                placeholder=\"100\"\n                className=\"w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-sm focus:border-rose-400 focus:outline-none\"\n              />\n            </div>\n          </div>\n          <div className=\"flex gap-4\">\n            <label className=\"flex items-center gap-2 cursor-pointer\">\n              <input\n                type=\"checkbox\"\n                checked={neurogenesis}\n                onChange={(e) => setNeurogenesis(e.target.checked)}\n                className=\"w-4 h-4 accent-rose-400\"\n              />\n              <span className=\"text-sm\">Enable Neurogenesis</span>\n            </label>\n            <label className=\"flex items-center gap-2 cursor-pointer\">\n              <input\n                type=\"checkbox\"\n                checked={quietMode}\n                onChange={(e) => setQuietMode(e.target.checked)}\n                className=\"w-4 h-4 accent-rose-400\"\n              />\n              <span className=\"text-sm\">Quiet Mode</span>\n            </label>\n          </div>\n        </div>\n\n        {/* Command Output */}\n        <div className=\"bg-slate-800 rounded-lg p-4 border border-slate-700\">\n          <h2 className=\"text-rose-400 font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"w-1 h-5 bg-rose-400 rounded\" />\n            Generated Command\n          </h2>\n          <div className=\"bg-slate-900 rounded p-3 font-mono text-sm break-all border border-slate-700\">\n            <span className=\"text-emerald-400\">python</span>{' '}\n            <span className=\"text-gray-300\">headless_trainer.py</span>\n            {brainFile && (\n              <> <span className=\"text-amber-400\">--brain</span> <span className=\"text-sky-300\">\"{brainFile}\"</span></>\n            )}\n            {scenario !== 'none' && (\n              <> <span className=\"text-amber-400\">--scenario</span> <span className=\"text-sky-300\">{scenario}</span></>\n            )}\n            <> <span className=\"text-amber-400\">--ticks</span> <span className=\"text-sky-300\">{ticks}</span></>\n            {outputFile && (\n              <> <span className=\"text-amber-400\">--output</span> <span className=\"text-sky-300\">\"{outputFile}\"</span></>\n            )}\n            {progress !== 500 && (\n              <> <span className=\"text-amber-400\">--progress</span> <span className=\"text-sky-300\">{progress}</span></>\n            )}\n            {learningRate && (\n              <> <span className=\"text-amber-400\">--learning-rate</span> <span className=\"text-sky-300\">{learningRate}</span></>\n            )}\n            {maxNeurons && (\n              <> <span className=\"text-amber-400\">--max-neurons</span> <span className=\"text-sky-300\">{maxNeurons}</span></>\n            )}\n            {!neurogenesis && (\n              <> <span className=\"text-amber-400\">--neurogenesis</span> <span className=\"text-sky-300\">False</span></>\n            )}\n            {quietMode && (\n              <> <span className=\"text-amber-400\">--quiet</span></>\n            )}\n          </div>\n          \n          <div className=\"flex gap-2 mt-3 flex-wrap\">\n            <button\n              onClick={() => copyToClipboard(getCommand(), 'cmd')}\n              className=\"px-4 py-2 bg-rose-500 hover:bg-rose-400 rounded font-medium text-sm transition-colors\"\n            >\n              {copied === 'cmd' ? '✓ Copied!' : '📋 Copy Command'}\n            </button>\n            <button\n              onClick={() => copyToClipboard(\n                `@echo off\\necho Dosidicus-2 Headless Trainer\\necho.\\n${getCommand()}\\npause`,\n                'bat'\n              )}\n              className=\"px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded font-medium text-sm transition-colors border border-slate-600\"\n            >\n              {copied === 'bat' ? '✓ Copied!' : '📄 Copy .bat'}\n            </button>\n            <button\n              onClick={() => copyToClipboard(\n                `#!/bin/bash\\necho \"Dosidicus-2 Headless Trainer\"\\n${getCommand()}`,\n                'sh'\n              )}\n              className=\"px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded font-medium text-sm transition-colors border border-slate-600\"\n            >\n              {copied === 'sh' ? '✓ Copied!' : '📄 Copy .sh'}\n            </button>\n          </div>\n        </div>\n\n        <div className=\"text-center text-gray-500 text-xs mt-4\">\n          Dosidicus-2 Headless Trainer Launcher\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "headless/README.md",
    "content": "## Headless Brain Trainer\nA standalone training system for Dosidicus neural network brains that runs without GUI overhead, enabling fast accelerated training of custom brain architectures.\n\n\n## Features\n\n- **Headless Operation**: No GUI required, runs purely on CPU\n- **Accelerated Time**: Runs 25,000-35,000+ ticks per second (vs ~1 tick/second in real-time)\n- **Custom Brain Loading**: Load brain architectures from JSON files\n- **Neurogenesis**: Automatic creation of new neurons based on stress/novelty/reward triggers\n- **Hebbian Learning**: Continuous weight updates based on co-activation\n- **Training Scenarios**: Predefined scenarios for different training goals\n- **Export Trained Brains**: Save trained brains back to JSON for use in the main game\n\n## Installation\n\n**No additional dependencies required** beyond Python 3.6+. The trainer is self-contained.\n\n```bash\n# Make executable (optional)\nchmod +x headless_trainer.py\n```\n\n## Quick Start\n\n```bash\n# Train with default brain for 10,000 ticks\npython headless_trainer.py --ticks 10000 --output my_trained_brain.json\n\n# Train a custom brain\npython headless_trainer.py --brain my_custom_brain.json --ticks 20000 --output trained.json\n\n# Use a predefined scenario\npython headless_trainer.py --brain my_brain.json --scenario stress_test --output stress_trained.json\n\n# List available scenarios\npython headless_trainer.py --list-scenarios\n```\n\n## Command Line Options\n\n| Option | Short | Description |\n|--------|-------|-------------|\n| `--brain FILE` | `-b` | Path to brain JSON file to load |\n| `--output FILE` | `-o` | Path to save trained brain (auto-generated if not specified) |\n| `--ticks N` | `-t` | Number of simulation ticks (default: 10000) |\n| `--scenario NAME` | `-s` | Use a predefined training scenario |\n| `--list-scenarios` | | List available training scenarios |\n| `--progress N` | `-p` | Progress report interval (default: 500, 0=quiet) |\n| `--quiet` | `-q` | Minimal output |\n| `--learning-rate F` | | Override Hebbian learning rate |\n| `--neurogenesis BOOL` | | Enable/disable neurogenesis |\n| `--max-neurons N` | | Maximum neurons allowed |\n\n## Training Scenarios\n\n### `balanced`\nStandard balanced training with normal event rates. Good for general-purpose brain development.\n\n### `stress_test`\nHigh anxiety/stress conditions to develop resilience neurons. Includes:\n- Reduced food availability\n- Increased startle events\n- Scripted high-anxiety events\n\n### `reward_rich`\nFrequent positive outcomes to develop reward pathways. Includes:\n- High food spawn rate\n- Reduced negative events\n- Scripted feeding events\n\n### `novelty_exploration`\nHigh curiosity environment with new objects. Good for developing exploration behaviors.\n\n### `endurance`\nLong-duration (50,000 ticks) training with varied conditions.\n\n## Brain JSON Format\n\nBrains are stored as JSON files with the following structure:\n\n```json\n{\n  \"metadata\": {\n    \"name\": \"My Brain\",\n    \"description\": \"Description of the brain\"\n  },\n  \"positions\": {\n    \"hunger\": {\"x\": 127, \"y\": 81, \"is_custom\": false},\n    \"custom_neuron\": {\"x\": 450, \"y\": 250, \"is_custom\": true}\n  },\n  \"weights\": {\n    \"hunger,satisfaction\": -0.3,\n    \"happiness,satisfaction\": 0.4\n  },\n  \"neuron_shapes\": {\n    \"custom_neuron\": \"pentagon\"\n  },\n  \"output_bindings\": []\n}\n```\n\n### Core Neurons (cannot be removed)\n- `hunger`, `happiness`, `cleanliness`, `sleepiness`\n- `satisfaction`, `anxiety`, `curiosity`\n\n### Input Sensors\n- `can_see_food`, `plant_proximity`, `external_stimulus`\n- `is_eating`, `is_sleeping`, `is_sick`, `is_fleeing`, `is_startled`, `pursuing_food`\n\n### Custom Neurons\nAny neuron not in the core/sensor lists is treated as a custom neuron and will participate in Hebbian learning with boosted rates.\n\n## Example Workflow\n\n### 1. Create a custom brain in the Brain Designer\nUse the [Brain Designer](https://github.com/ViciousSquid/Dosidicus/wiki/Brain-Designer) to create your architecture, then export it as JSON.\n\n### 2. Train the brain headlessly\n```bash\n# Quick training\npython headless_trainer.py -b my_design.json -t 50000 -o trained_v1.json\n\n# Or with a scenario\npython headless_trainer.py -b my_design.json -s stress_test -o stress_trained.json\n```\n\n### 3. Load the trained brain back into Dosidicus\nUse the \"Load Brain\" button in the [Network tab](https://github.com/ViciousSquid/Dosidicus/wiki/Network-Tab) to load your trained brain.\n\n## Training Tips\n\n1. **Start with balanced training** to establish baseline connections\n2. **Use stress_test** if you want resilience neurons for anxiety handling\n3. **Use reward_rich** if you want strong satisfaction/happiness pathways\n4. **Long training (50k+ ticks)** allows more sophisticated weight patterns to develop\n5. **Multiple training rounds** with different scenarios can create well-rounded brains\n\n## Performance\n\nOn modern hardware, expect:\n- ~25,000-35,000 ticks/second\n- A 50,000 tick training session completes in ~2 seconds\n- A 1,000,000 tick session completes in ~30 seconds\n\n\n## Integration with Dosidicus-2\n\nThe trained brain JSON files are fully compatible with:\n- Brain Designer (import/export)\n- Main game's \"Load Brain\" feature\n- Save game custom brain storage\n\n## License\n\nPart of Dosidicus-2 project. [GPL-2.0 license](https://github.com/ViciousSquid/Dosidicus/blob/v2.6.1.0__b1218_LatestVersion/LICENSE)\n"
  },
  {
    "path": "headless/headless_launcher.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Dosidicus-2 Headless Trainer Launcher</title>\n    <style>\n        :root {\n            --bg-dark: #1a1a2e;\n            --bg-card: #16213e;\n            --bg-input: #0f0f23;\n            --accent: #e94560;\n            --accent-hover: #ff6b6b;\n            --text: #eee;\n            --text-dim: #888;\n            --success: #4ecca3;\n            --warning: #ffc107;\n            --border: #333;\n        }\n\n        * {\n            box-sizing: border-box;\n            margin: 0;\n            padding: 0;\n        }\n\n        body {\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n            background: var(--bg-dark);\n            color: var(--text);\n            min-height: 100vh;\n            padding: 20px;\n        }\n\n        .container {\n            max-width: 900px;\n            margin: 0 auto;\n        }\n\n        header {\n            text-align: center;\n            margin-bottom: 30px;\n            padding: 20px;\n            background: linear-gradient(135deg, var(--bg-card), #1a1a3e);\n            border-radius: 12px;\n            border: 1px solid var(--border);\n        }\n\n        header h1 {\n            font-size: 1.8em;\n            margin-bottom: 8px;\n            background: linear-gradient(90deg, var(--accent), #ff9a9e);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n        }\n\n        header p {\n            color: var(--text-dim);\n            font-size: 0.95em;\n        }\n\n        .card {\n            background: var(--bg-card);\n            border-radius: 10px;\n            padding: 20px;\n            margin-bottom: 20px;\n            border: 1px solid var(--border);\n        }\n\n        .card h2 {\n            font-size: 1.1em;\n            margin-bottom: 15px;\n            color: var(--accent);\n            display: flex;\n            align-items: center;\n            gap: 8px;\n        }\n\n        .card h2::before {\n            content: '';\n            display: inline-block;\n            width: 4px;\n            height: 20px;\n            background: var(--accent);\n            border-radius: 2px;\n        }\n\n        .form-row {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 15px;\n            margin-bottom: 15px;\n        }\n\n        .form-group {\n            display: flex;\n            flex-direction: column;\n            gap: 6px;\n        }\n\n        .form-group.full-width {\n            grid-column: 1 / -1;\n        }\n\n        label {\n            font-size: 0.85em;\n            color: var(--text-dim);\n            font-weight: 500;\n        }\n\n        input[type=\"text\"],\n        input[type=\"number\"],\n        select {\n            background: var(--bg-input);\n            border: 1px solid var(--border);\n            border-radius: 6px;\n            padding: 10px 12px;\n            color: var(--text);\n            font-size: 0.95em;\n            transition: border-color 0.2s, box-shadow 0.2s;\n        }\n\n        input:focus,\n        select:focus {\n            outline: none;\n            border-color: var(--accent);\n            box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2);\n        }\n\n        input[type=\"file\"] {\n            background: var(--bg-input);\n            border: 2px dashed var(--border);\n            border-radius: 6px;\n            padding: 15px;\n            color: var(--text);\n            cursor: pointer;\n            transition: border-color 0.2s;\n        }\n\n        input[type=\"file\"]:hover {\n            border-color: var(--accent);\n        }\n\n        .checkbox-group {\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            padding: 10px 0;\n        }\n\n        input[type=\"checkbox\"] {\n            width: 18px;\n            height: 18px;\n            accent-color: var(--accent);\n            cursor: pointer;\n        }\n\n        .checkbox-group label {\n            color: var(--text);\n            cursor: pointer;\n        }\n\n        .scenario-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n            gap: 10px;\n        }\n\n        .scenario-card {\n            background: var(--bg-input);\n            border: 2px solid var(--border);\n            border-radius: 8px;\n            padding: 12px;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n\n        .scenario-card:hover {\n            border-color: var(--accent);\n            transform: translateY(-2px);\n        }\n\n        .scenario-card.selected {\n            border-color: var(--success);\n            background: rgba(78, 204, 163, 0.1);\n        }\n\n        .scenario-card h3 {\n            font-size: 0.95em;\n            margin-bottom: 4px;\n            color: var(--text);\n        }\n\n        .scenario-card p {\n            font-size: 0.8em;\n            color: var(--text-dim);\n            line-height: 1.4;\n        }\n\n        .scenario-card .duration {\n            font-size: 0.75em;\n            color: var(--warning);\n            margin-top: 6px;\n        }\n\n        .command-output {\n            background: var(--bg-input);\n            border: 1px solid var(--border);\n            border-radius: 8px;\n            padding: 15px;\n            font-family: 'Consolas', 'Monaco', monospace;\n            font-size: 0.9em;\n            word-break: break-all;\n            line-height: 1.6;\n            position: relative;\n        }\n\n        .command-output .cmd-prefix {\n            color: var(--success);\n        }\n\n        .command-output .cmd-arg {\n            color: var(--warning);\n        }\n\n        .command-output .cmd-value {\n            color: #87ceeb;\n        }\n\n        .btn-row {\n            display: flex;\n            gap: 10px;\n            margin-top: 15px;\n            flex-wrap: wrap;\n        }\n\n        .btn {\n            padding: 12px 24px;\n            border: none;\n            border-radius: 6px;\n            font-size: 0.95em;\n            font-weight: 600;\n            cursor: pointer;\n            transition: all 0.2s;\n            display: flex;\n            align-items: center;\n            gap: 8px;\n        }\n\n        .btn-primary {\n            background: var(--accent);\n            color: white;\n        }\n\n        .btn-primary:hover {\n            background: var(--accent-hover);\n            transform: translateY(-1px);\n        }\n\n        .btn-secondary {\n            background: var(--bg-input);\n            color: var(--text);\n            border: 1px solid var(--border);\n        }\n\n        .btn-secondary:hover {\n            border-color: var(--accent);\n            background: rgba(233, 69, 96, 0.1);\n        }\n\n        .btn-success {\n            background: var(--success);\n            color: #1a1a2e;\n        }\n\n        .btn-success:hover {\n            filter: brightness(1.1);\n        }\n\n        .status {\n            padding: 10px 15px;\n            border-radius: 6px;\n            margin-top: 15px;\n            font-size: 0.9em;\n            display: none;\n        }\n\n        .status.show {\n            display: block;\n        }\n\n        .status.success {\n            background: rgba(78, 204, 163, 0.2);\n            border: 1px solid var(--success);\n            color: var(--success);\n        }\n\n        .status.error {\n            background: rgba(233, 69, 96, 0.2);\n            border: 1px solid var(--accent);\n            color: var(--accent);\n        }\n\n        .help-text {\n            font-size: 0.8em;\n            color: var(--text-dim);\n            margin-top: 4px;\n        }\n\n        .input-with-button {\n            display: flex;\n            gap: 8px;\n        }\n\n        .input-with-button input[type=\"text\"] {\n            flex: 1;\n        }\n\n        .btn-browse {\n            background: var(--accent);\n            color: white;\n            border: none;\n            border-radius: 6px;\n            padding: 10px 16px;\n            font-size: 0.9em;\n            font-weight: 600;\n            cursor: pointer;\n            white-space: nowrap;\n            transition: all 0.2s;\n        }\n\n        .btn-browse:hover {\n            background: var(--accent-hover);\n        }\n\n        .brain-preview {\n            background: var(--bg-input);\n            border: 1px solid var(--success);\n            border-radius: 8px;\n            padding: 12px;\n            margin-top: 15px;\n        }\n\n        .preview-header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin-bottom: 10px;\n        }\n\n        .preview-title {\n            font-weight: 600;\n            color: var(--success);\n        }\n\n        .btn-clear {\n            background: transparent;\n            border: 1px solid var(--text-dim);\n            color: var(--text-dim);\n            border-radius: 4px;\n            padding: 4px 10px;\n            font-size: 0.8em;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n\n        .btn-clear:hover {\n            border-color: var(--accent);\n            color: var(--accent);\n        }\n\n        .preview-stats {\n            display: flex;\n            gap: 15px;\n            margin-bottom: 8px;\n        }\n\n        .preview-stat {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            background: rgba(78, 204, 163, 0.1);\n            padding: 8px 15px;\n            border-radius: 6px;\n        }\n\n        .preview-stat .stat-value {\n            font-size: 1.2em;\n            font-weight: bold;\n            color: var(--success);\n        }\n\n        .preview-stat .stat-label {\n            font-size: 0.75em;\n            color: var(--text-dim);\n        }\n\n        .preview-desc {\n            font-size: 0.85em;\n            color: var(--text-dim);\n            font-style: italic;\n        }\n\n        .drop-zone {\n            border: 2px dashed var(--border);\n            border-radius: 8px;\n            padding: 20px;\n            text-align: center;\n            margin-top: 10px;\n            transition: all 0.2s;\n            cursor: pointer;\n        }\n\n        .drop-zone:hover,\n        .drop-zone.drag-over {\n            border-color: var(--accent);\n            background: rgba(233, 69, 96, 0.1);\n        }\n\n        .drop-zone.drag-over {\n            transform: scale(1.01);\n        }\n\n        .drop-zone-text {\n            color: var(--text-dim);\n            font-size: 0.9em;\n        }\n\n        .drop-zone-text strong {\n            color: var(--accent);\n        }\n\n        .stats-preview {\n            display: grid;\n            grid-template-columns: repeat(3, 1fr);\n            gap: 10px;\n            margin-top: 15px;\n        }\n\n        .stat-box {\n            background: var(--bg-input);\n            padding: 12px;\n            border-radius: 6px;\n            text-align: center;\n        }\n\n        .stat-box .value {\n            font-size: 1.4em;\n            font-weight: bold;\n            color: var(--accent);\n        }\n\n        .stat-box .label {\n            font-size: 0.75em;\n            color: var(--text-dim);\n            margin-top: 4px;\n        }\n\n        .divider {\n            height: 1px;\n            background: var(--border);\n            margin: 20px 0;\n        }\n\n        @media (max-width: 600px) {\n            .form-row {\n                grid-template-columns: 1fr;\n            }\n            \n            .scenario-grid {\n                grid-template-columns: 1fr;\n            }\n\n            .stats-preview {\n                grid-template-columns: 1fr;\n            }\n        }\n\n        .icon {\n            font-size: 1.2em;\n        }\n\n        footer {\n            text-align: center;\n            padding: 20px;\n            color: var(--text-dim);\n            font-size: 0.85em;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <header>\n            <h1>🦑 Dosidicus-2 Headless Trainer</h1>\n            <p>Configure and launch accelerated brain training sessions</p>\n        </header>\n\n        <!-- Brain File -->\n        <div class=\"card\">\n            <h2>Brain Configuration</h2>\n            <div class=\"form-row\">\n                <div class=\"form-group\">\n                    <label for=\"brainFile\">Brain File (JSON)</label>\n                    <div class=\"input-with-button\">\n                        <input type=\"text\" id=\"brainFile\" placeholder=\"path/to/brain.json\">\n                        <button type=\"button\" class=\"btn-browse\" onclick=\"document.getElementById('brainFileInput').click()\">\n                            📁 Browse\n                        </button>\n                        <input type=\"file\" id=\"brainFileInput\" accept=\".json\" style=\"display: none;\" onchange=\"handleBrainFileSelect(this)\">\n                    </div>\n                    <span class=\"help-text\">Leave empty to use default brain architecture</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"outputFile\">Output File</label>\n                    <input type=\"text\" id=\"outputFile\" placeholder=\"trained_brain.json\">\n                    <span class=\"help-text\">Auto-generated if not specified</span>\n                </div>\n            </div>\n            \n            <!-- Brain Preview Panel (shown when file is loaded) -->\n            <div id=\"brainPreview\" class=\"brain-preview\" style=\"display: none;\">\n                <div class=\"preview-header\">\n                    <span class=\"preview-title\">🧠 <span id=\"brainName\">Brain Name</span></span>\n                    <button class=\"btn-clear\" onclick=\"clearBrainFile()\">✕ Clear</button>\n                </div>\n                <div class=\"preview-stats\">\n                    <div class=\"preview-stat\">\n                        <span class=\"stat-value\" id=\"previewNeurons\">0</span>\n                        <span class=\"stat-label\">Neurons</span>\n                    </div>\n                    <div class=\"preview-stat\">\n                        <span class=\"stat-value\" id=\"previewConnections\">0</span>\n                        <span class=\"stat-label\">Connections</span>\n                    </div>\n                    <div class=\"preview-stat\">\n                        <span class=\"stat-value\" id=\"previewCustom\">0</span>\n                        <span class=\"stat-label\">Custom</span>\n                    </div>\n                </div>\n                <div class=\"preview-desc\" id=\"previewDesc\"></div>\n            </div>\n            \n            <!-- Drop Zone -->\n            <div class=\"drop-zone\" id=\"dropZone\" onclick=\"document.getElementById('brainFileInput').click()\">\n                <div class=\"drop-zone-text\">\n                    <strong>📂 Drop brain.json here</strong> or click to browse\n                </div>\n            </div>\n        </div>\n\n        <!-- Training Parameters -->\n        <div class=\"card\">\n            <h2>Training Parameters</h2>\n            <div class=\"form-row\">\n                <div class=\"form-group\">\n                    <label for=\"ticks\">Training Ticks</label>\n                    <input type=\"number\" id=\"ticks\" value=\"10000\" min=\"100\" max=\"10000000\" step=\"1000\">\n                    <span class=\"help-text\">Number of simulation steps (≈30k ticks/sec)</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"progress\">Progress Interval</label>\n                    <input type=\"number\" id=\"progress\" value=\"500\" min=\"0\" max=\"10000\" step=\"100\">\n                    <span class=\"help-text\">Report frequency (0 = quiet mode)</span>\n                </div>\n            </div>\n\n            <div class=\"stats-preview\">\n                <div class=\"stat-box\">\n                    <div class=\"value\" id=\"estTime\">0.3s</div>\n                    <div class=\"label\">Estimated Time</div>\n                </div>\n                <div class=\"stat-box\">\n                    <div class=\"value\" id=\"estNeurons\">~5-20</div>\n                    <div class=\"label\">New Neurons</div>\n                </div>\n                <div class=\"stat-box\">\n                    <div class=\"value\" id=\"estHebbian\">~334</div>\n                    <div class=\"label\">Hebbian Updates</div>\n                </div>\n            </div>\n        </div>\n\n        <!-- Scenarios -->\n        <div class=\"card\">\n            <h2>Training Scenario</h2>\n            <p style=\"color: var(--text-dim); font-size: 0.9em; margin-bottom: 15px;\">\n                Select a predefined scenario or use custom settings\n            </p>\n            <div class=\"scenario-grid\">\n                <div class=\"scenario-card selected\" data-scenario=\"none\" onclick=\"selectScenario(this, 'none')\">\n                    <h3>⚙️ Custom</h3>\n                    <p>Use manual settings without predefined events</p>\n                </div>\n                <div class=\"scenario-card\" data-scenario=\"balanced\" onclick=\"selectScenario(this, 'balanced')\">\n                    <h3>⚖️ Balanced</h3>\n                    <p>Standard training with normal event rates</p>\n                    <div class=\"duration\">10,000 ticks</div>\n                </div>\n                <div class=\"scenario-card\" data-scenario=\"stress_test\" onclick=\"selectScenario(this, 'stress_test')\">\n                    <h3>😰 Stress Test</h3>\n                    <p>High anxiety to develop resilience neurons</p>\n                    <div class=\"duration\">5,000 ticks</div>\n                </div>\n                <div class=\"scenario-card\" data-scenario=\"reward_rich\" onclick=\"selectScenario(this, 'reward_rich')\">\n                    <h3>🎁 Reward Rich</h3>\n                    <p>Frequent positive outcomes for reward pathways</p>\n                    <div class=\"duration\">8,000 ticks</div>\n                </div>\n                <div class=\"scenario-card\" data-scenario=\"novelty_exploration\" onclick=\"selectScenario(this, 'novelty_exploration')\">\n                    <h3>🔍 Novelty</h3>\n                    <p>High curiosity with new object encounters</p>\n                    <div class=\"duration\">6,000 ticks</div>\n                </div>\n                <div class=\"scenario-card\" data-scenario=\"endurance\" onclick=\"selectScenario(this, 'endurance')\">\n                    <h3>🏃 Endurance</h3>\n                    <p>Long-duration training session</p>\n                    <div class=\"duration\">50,000 ticks</div>\n                </div>\n            </div>\n        </div>\n\n        <!-- Advanced Options -->\n        <div class=\"card\">\n            <h2>Advanced Options</h2>\n            <div class=\"form-row\">\n                <div class=\"form-group\">\n                    <label for=\"learningRate\">Learning Rate</label>\n                    <input type=\"number\" id=\"learningRate\" placeholder=\"0.1\" step=\"0.01\" min=\"0.01\" max=\"1.0\">\n                    <span class=\"help-text\">Hebbian learning rate (default: 0.1)</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"maxNeurons\">Max Neurons</label>\n                    <input type=\"number\" id=\"maxNeurons\" placeholder=\"100\" min=\"10\" max=\"500\">\n                    <span class=\"help-text\">Maximum neurons allowed (default: 100)</span>\n                </div>\n            </div>\n            <div class=\"checkbox-group\">\n                <input type=\"checkbox\" id=\"neurogenesis\" checked>\n                <label for=\"neurogenesis\">Enable Neurogenesis</label>\n            </div>\n            <div class=\"checkbox-group\">\n                <input type=\"checkbox\" id=\"quietMode\">\n                <label for=\"quietMode\">Quiet Mode (minimal output)</label>\n            </div>\n        </div>\n\n        <!-- Command Output -->\n        <div class=\"card\">\n            <h2>Generated Command</h2>\n            <div class=\"command-output\" id=\"commandOutput\">\n                <span class=\"cmd-prefix\">python</span> headless_trainer.py <span class=\"cmd-arg\">--ticks</span> <span class=\"cmd-value\">10000</span>\n            </div>\n            <div class=\"btn-row\">\n                <button class=\"btn btn-primary\" onclick=\"copyCommand()\">\n                    📋 Copy Command\n                </button>\n                <button class=\"btn btn-secondary\" onclick=\"copyAsBatch()\">\n                    📄 Copy as .bat\n                </button>\n                <button class=\"btn btn-secondary\" onclick=\"copyAsShell()\">\n                    📄 Copy as .sh\n                </button>\n                <button class=\"btn btn-success\" onclick=\"downloadBatch()\">\n                    💾 Download Launcher\n                </button>\n            </div>\n            <div class=\"status\" id=\"status\"></div>\n        </div>\n\n        <!-- Quick Reference -->\n        <div class=\"card\">\n            <h2>Quick Reference</h2>\n            <div style=\"font-size: 0.9em; color: var(--text-dim); line-height: 1.8;\">\n                <p><strong>Performance:</strong> ~25,000-35,000 ticks/second on modern hardware</p>\n                <p><strong>Neurogenesis:</strong> Creates stress/novelty/reward neurons based on squid state</p>\n                <p><strong>Hebbian Learning:</strong> Updates connection weights every 30 ticks</p>\n                <p><strong>Output:</strong> Compatible with Brain Designer and main game \"Load Brain\" feature</p>\n            </div>\n        </div>\n\n        <footer>\n            Dosidicus Headless Trainer Launcher • Part of the Dosidicus-2 Project : <a href=\"https://github.com/ViciousSquid/Dosidicus\"\n   target=\"_blank\"\n   style=\"color: orange;\">\n  https://github.com/ViciousSquid/Dosidicus\n</a> \n        </footer>\n    </div>\n\n    <script>\n        let selectedScenario = 'none';\n        let loadedBrainData = null;\n\n        const scenarioTicks = {\n            'none': null,\n            'balanced': 10000,\n            'stress_test': 5000,\n            'reward_rich': 8000,\n            'novelty_exploration': 6000,\n            'endurance': 50000\n        };\n\n        // Handle brain file selection\n        function handleBrainFileSelect(input) {\n            const file = input.files[0];\n            if (!file) return;\n\n            // Update the text input with filename\n            document.getElementById('brainFile').value = file.name;\n\n            // Read and parse the file\n            const reader = new FileReader();\n            reader.onload = function(e) {\n                try {\n                    const brainData = JSON.parse(e.target.result);\n                    loadedBrainData = brainData;\n                    showBrainPreview(brainData, file.name);\n                    document.getElementById('dropZone').style.display = 'none';\n                    \n                    // Auto-suggest output filename\n                    const outputField = document.getElementById('outputFile');\n                    if (!outputField.value) {\n                        const baseName = file.name.replace('.json', '');\n                        outputField.value = `${baseName}_trained.json`;\n                    }\n                    \n                    updateCommand();\n                    showStatus('Brain file loaded successfully!', 'success');\n                } catch (err) {\n                    showStatus('Error parsing brain file: ' + err.message, 'error');\n                    loadedBrainData = null;\n                }\n            };\n            reader.readAsText(file);\n        }\n\n        function showBrainPreview(brainData, filename) {\n            const preview = document.getElementById('brainPreview');\n            \n            // Get brain name\n            let brainName = filename.replace('.json', '');\n            if (brainData.metadata && brainData.metadata.name) {\n                brainName = brainData.metadata.name;\n            }\n            document.getElementById('brainName').textContent = brainName;\n\n            // Count neurons\n            const positions = brainData.positions || brainData.neurons || {};\n            const neuronCount = Object.keys(positions).length;\n            document.getElementById('previewNeurons').textContent = neuronCount;\n\n            // Count connections\n            const weights = brainData.weights || brainData.connections || {};\n            let connectionCount = 0;\n            if (Array.isArray(weights)) {\n                connectionCount = weights.length;\n            } else if (typeof weights === 'object') {\n                connectionCount = Object.keys(weights).length;\n            }\n            document.getElementById('previewConnections').textContent = connectionCount;\n\n            // Count custom neurons\n            let customCount = 0;\n            for (const [name, data] of Object.entries(positions)) {\n                if (typeof data === 'object' && data.is_custom) {\n                    customCount++;\n                } else if (!isCoreSensor(name)) {\n                    customCount++;\n                }\n            }\n            document.getElementById('previewCustom').textContent = customCount;\n\n            // Description\n            let desc = '';\n            if (brainData.metadata && brainData.metadata.description) {\n                desc = brainData.metadata.description;\n            }\n            document.getElementById('previewDesc').textContent = desc;\n\n            preview.style.display = 'block';\n            document.getElementById('dropZone').style.display = 'none';\n        }\n\n        function isCoreSensor(name) {\n            const core = [\n                'hunger', 'happiness', 'cleanliness', 'sleepiness',\n                'satisfaction', 'anxiety', 'curiosity',\n                'can_see_food', 'is_eating', 'is_sleeping', 'is_sick',\n                'pursuing_food', 'is_fleeing', 'is_startled', \n                'external_stimulus', 'plant_proximity'\n            ];\n            return core.includes(name);\n        }\n\n        function clearBrainFile() {\n            document.getElementById('brainFile').value = '';\n            document.getElementById('brainFileInput').value = '';\n            document.getElementById('brainPreview').style.display = 'none';\n            document.getElementById('dropZone').style.display = 'block';\n            loadedBrainData = null;\n            updateCommand();\n        }\n\n        // Drag and drop handlers\n        function setupDragDrop() {\n            const dropZone = document.getElementById('dropZone');\n            \n            ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {\n                dropZone.addEventListener(eventName, preventDefaults, false);\n                document.body.addEventListener(eventName, preventDefaults, false);\n            });\n\n            ['dragenter', 'dragover'].forEach(eventName => {\n                dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'), false);\n            });\n\n            ['dragleave', 'drop'].forEach(eventName => {\n                dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'), false);\n            });\n\n            dropZone.addEventListener('drop', handleDrop, false);\n        }\n\n        function preventDefaults(e) {\n            e.preventDefault();\n            e.stopPropagation();\n        }\n\n        function handleDrop(e) {\n            const dt = e.dataTransfer;\n            const files = dt.files;\n            \n            if (files.length > 0) {\n                const file = files[0];\n                if (file.name.endsWith('.json')) {\n                    processDroppedFile(file);\n                } else {\n                    showStatus('Please drop a .json file', 'error');\n                }\n            }\n        }\n\n        function processDroppedFile(file) {\n            document.getElementById('brainFile').value = file.name;\n            \n            const reader = new FileReader();\n            reader.onload = function(e) {\n                try {\n                    const brainData = JSON.parse(e.target.result);\n                    loadedBrainData = brainData;\n                    showBrainPreview(brainData, file.name);\n                    document.getElementById('dropZone').style.display = 'none';\n                    \n                    const outputField = document.getElementById('outputFile');\n                    if (!outputField.value) {\n                        const baseName = file.name.replace('.json', '');\n                        outputField.value = `${baseName}_trained.json`;\n                    }\n                    \n                    updateCommand();\n                    showStatus('Brain file loaded successfully!', 'success');\n                } catch (err) {\n                    showStatus('Error parsing brain file: ' + err.message, 'error');\n                    loadedBrainData = null;\n                }\n            };\n            reader.readAsText(file);\n        }\n\n        // Initialize\n        document.addEventListener('DOMContentLoaded', () => {\n            updateCommand();\n            setupDragDrop();\n            \n            // Add event listeners to all inputs\n            document.querySelectorAll('input, select').forEach(el => {\n                el.addEventListener('input', updateCommand);\n                el.addEventListener('change', updateCommand);\n            });\n        });\n\n        function selectScenario(element, scenario) {\n            // Remove selected class from all\n            document.querySelectorAll('.scenario-card').forEach(card => {\n                card.classList.remove('selected');\n            });\n            \n            // Add selected to clicked\n            element.classList.add('selected');\n            selectedScenario = scenario;\n            \n            // Update ticks if scenario has default\n            if (scenarioTicks[scenario]) {\n                document.getElementById('ticks').value = scenarioTicks[scenario];\n            }\n            \n            updateCommand();\n        }\n\n        function updateCommand() {\n            const brainFile = document.getElementById('brainFile').value.trim();\n            const outputFile = document.getElementById('outputFile').value.trim();\n            const ticks = document.getElementById('ticks').value;\n            const progress = document.getElementById('progress').value;\n            const learningRate = document.getElementById('learningRate').value;\n            const maxNeurons = document.getElementById('maxNeurons').value;\n            const neurogenesis = document.getElementById('neurogenesis').checked;\n            const quietMode = document.getElementById('quietMode').checked;\n\n            let cmd = '<span class=\"cmd-prefix\">python</span> headless_trainer.py';\n\n            if (brainFile) {\n                cmd += ` <span class=\"cmd-arg\">--brain</span> <span class=\"cmd-value\">\"${brainFile}\"</span>`;\n            }\n\n            if (selectedScenario !== 'none') {\n                cmd += ` <span class=\"cmd-arg\">--scenario</span> <span class=\"cmd-value\">${selectedScenario}</span>`;\n            }\n\n            cmd += ` <span class=\"cmd-arg\">--ticks</span> <span class=\"cmd-value\">${ticks}</span>`;\n\n            if (outputFile) {\n                cmd += ` <span class=\"cmd-arg\">--output</span> <span class=\"cmd-value\">\"${outputFile}\"</span>`;\n            }\n\n            if (progress && progress !== '500') {\n                cmd += ` <span class=\"cmd-arg\">--progress</span> <span class=\"cmd-value\">${progress}</span>`;\n            }\n\n            if (learningRate) {\n                cmd += ` <span class=\"cmd-arg\">--learning-rate</span> <span class=\"cmd-value\">${learningRate}</span>`;\n            }\n\n            if (maxNeurons) {\n                cmd += ` <span class=\"cmd-arg\">--max-neurons</span> <span class=\"cmd-value\">${maxNeurons}</span>`;\n            }\n\n            if (!neurogenesis) {\n                cmd += ` <span class=\"cmd-arg\">--neurogenesis</span> <span class=\"cmd-value\">False</span>`;\n            }\n\n            if (quietMode) {\n                cmd += ` <span class=\"cmd-arg\">--quiet</span>`;\n            }\n\n            document.getElementById('commandOutput').innerHTML = cmd;\n            \n            // Update estimates\n            updateEstimates(parseInt(ticks));\n        }\n\n        function updateEstimates(ticks) {\n            const estSeconds = (ticks / 30000).toFixed(1);\n            const estNeuronsMin = Math.floor(ticks / 2000);\n            const estNeuronsMax = Math.floor(ticks / 500);\n            const estHebbian = Math.floor(ticks / 30);\n\n            document.getElementById('estTime').textContent = estSeconds + 's';\n            document.getElementById('estNeurons').textContent = `~${estNeuronsMin}-${Math.min(estNeuronsMax, 100)}`;\n            document.getElementById('estHebbian').textContent = `~${estHebbian}`;\n        }\n\n        function getPlainCommand() {\n            const brainFile = document.getElementById('brainFile').value.trim();\n            const outputFile = document.getElementById('outputFile').value.trim();\n            const ticks = document.getElementById('ticks').value;\n            const progress = document.getElementById('progress').value;\n            const learningRate = document.getElementById('learningRate').value;\n            const maxNeurons = document.getElementById('maxNeurons').value;\n            const neurogenesis = document.getElementById('neurogenesis').checked;\n            const quietMode = document.getElementById('quietMode').checked;\n\n            let cmd = 'python headless_trainer.py';\n\n            if (brainFile) cmd += ` --brain \"${brainFile}\"`;\n            if (selectedScenario !== 'none') cmd += ` --scenario ${selectedScenario}`;\n            cmd += ` --ticks ${ticks}`;\n            if (outputFile) cmd += ` --output \"${outputFile}\"`;\n            if (progress && progress !== '500') cmd += ` --progress ${progress}`;\n            if (learningRate) cmd += ` --learning-rate ${learningRate}`;\n            if (maxNeurons) cmd += ` --max-neurons ${maxNeurons}`;\n            if (!neurogenesis) cmd += ` --neurogenesis False`;\n            if (quietMode) cmd += ` --quiet`;\n\n            return cmd;\n        }\n\n        function copyCommand() {\n            const cmd = getPlainCommand();\n            navigator.clipboard.writeText(cmd).then(() => {\n                showStatus('Command copied to clipboard!', 'success');\n            }).catch(() => {\n                showStatus('Failed to copy command', 'error');\n            });\n        }\n\n        function copyAsBatch() {\n            const cmd = getPlainCommand();\n            const batch = `@echo off\necho Dosidicus-2 Headless Brain Trainer\necho ===================================\necho.\n${cmd}\necho.\necho Training complete!\npause\n`;\n            navigator.clipboard.writeText(batch).then(() => {\n                showStatus('Batch script copied to clipboard!', 'success');\n            });\n        }\n\n        function copyAsShell() {\n            const cmd = getPlainCommand();\n            const shell = `#!/bin/bash\necho \"Dosidicus-2 Headless Brain Trainer\"\necho \"===================================\"\necho\n${cmd}\necho\necho \"Training complete!\"\n`;\n            navigator.clipboard.writeText(shell).then(() => {\n                showStatus('Shell script copied to clipboard!', 'success');\n            });\n        }\n\n        function downloadBatch() {\n            const cmd = getPlainCommand();\n            const batch = `@echo off\ntitle Dosidicus-2 Headless Trainer\necho.\necho   ____            _     _ _                     ____  \necho  ^|  _ \\\\  ___  ___(_) __^| (_) ___ _   _ ___    ^|___ \\\\ \necho  ^| ^| ^| ^|/ _ \\\\/ __^| ^|/ _\\` ^| ^|/ __^| ^| ^| / __^|     __) ^|\necho  ^| ^|_^| ^| (_) \\\\__ \\\\ ^| (_^| ^| ^| (__^| ^|_^| \\\\__ \\\\    / __/ \necho  ^|____/ \\\\___/^|___/_^|\\\\__,_^|_^|\\\\___|\\\\__,_^|___/   ^|_____^|\necho.\necho  Headless Brain Trainer\necho  =======================\necho.\necho  Starting training session...\necho.\n${cmd}\necho.\necho  ========================================\necho  Training complete! Press any key to exit.\npause > nul\n`;\n            const blob = new Blob([batch], { type: 'text/plain' });\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = 'train_brain.bat';\n            a.click();\n            URL.revokeObjectURL(url);\n            showStatus('Launcher downloaded as train_brain.bat', 'success');\n        }\n\n        function showStatus(message, type) {\n            const status = document.getElementById('status');\n            status.textContent = message;\n            status.className = `status show ${type}`;\n            setTimeout(() => {\n                status.classList.remove('show');\n            }, 3000);\n        }\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "headless/headless_trainer.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nDosidicus-2 Headless Brain Trainer\n\nAllows training custom neural network brains without GUI overhead.\nSupports accelerated time, custom brain loading, and state persistence.\n\nUsage:\n    python headless_trainer.py --brain custom_brain.json --ticks 10000 --output trained_brain.json\n    python headless_trainer.py --brain custom_brain.json --duration 3600 --speed 100\n    python headless_trainer.py --list-scenarios\n    python headless_trainer.py --brain my_brain.json --scenario stress_test\n\nAuthor: Headless training system for Dosidicus-2\n\"\"\"\n\nimport argparse\nimport json\nimport math\nimport os\nimport random\nimport sys\nimport time\nfrom collections import deque\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Set, Tuple, Any\nfrom heapq import nlargest\n\n\n# ============================================================================\n# CONSTANTS (from brain_worker.py and brain_constants.py)\n# ============================================================================\n\nPURE_INPUTS = {\n    \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\",\n    \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\",\n    \"plant_proximity\"\n}\n\nCORE_NEURONS = {\n    \"hunger\", \"happiness\", \"cleanliness\", \"sleepiness\",\n    \"satisfaction\", \"anxiety\", \"curiosity\"\n}\n\nINPUT_SENSORS = {\n    \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\",\n    \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\",\n    \"plant_proximity\"\n}\n\n\nclass Personality(Enum):\n    \"\"\"Squid personality types\"\"\"\n    TIMID = \"timid\"\n    ADVENTUROUS = \"adventurous\"\n    GREEDY = \"greedy\"\n    STUBBORN = \"stubborn\"\n    ENERGETIC = \"energetic\"\n\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\n@dataclass\nclass TrainingConfig:\n    \"\"\"Configuration for headless training\"\"\"\n    # Learning parameters\n    learning_rate: float = 0.1\n    weight_decay: float = 0.01\n    max_hebbian_pairs: int = 2\n    hebbian_interval: int = 30  # ticks between hebbian updates\n    \n    # Neurogenesis parameters\n    neurogenesis_enabled: bool = True\n    neurogenesis_cooldown: int = 60  # ticks\n    max_neurons: int = 100\n    novelty_threshold: float = 3.0\n    stress_threshold: float = 1.2\n    reward_threshold: float = 3.5\n    \n    # Simulation parameters\n    decay_rate: float = 0.95\n    noise_range: float = 0.5\n    \n    # Scenarios\n    food_spawn_chance: float = 0.02\n    poop_spawn_chance: float = 0.01\n    startle_chance: float = 0.005\n    \n    @classmethod\n    def from_dict(cls, data: Dict) -> 'TrainingConfig':\n        return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})\n\n\n@dataclass\nclass TrainingScenario:\n    \"\"\"Predefined training scenarios\"\"\"\n    name: str\n    description: str\n    duration_ticks: int\n    config_overrides: Dict = field(default_factory=dict)\n    events: List[Dict] = field(default_factory=list)\n\n\nTRAINING_SCENARIOS = {\n    \"balanced\": TrainingScenario(\n        name=\"Balanced Training\",\n        description=\"Standard balanced training with normal event rates\",\n        duration_ticks=10000,\n        config_overrides={},\n        events=[]\n    ),\n    \"stress_test\": TrainingScenario(\n        name=\"Stress Test\",\n        description=\"High anxiety/stress conditions to develop resilience neurons\",\n        duration_ticks=5000,\n        config_overrides={\n            \"startle_chance\": 0.02,\n            \"food_spawn_chance\": 0.005,\n        },\n        events=[\n            {\"tick\": 100, \"type\": \"set_state\", \"state\": {\"anxiety\": 80}},\n            {\"tick\": 500, \"type\": \"startle\"},\n            {\"tick\": 1000, \"type\": \"set_state\", \"state\": {\"hunger\": 90}},\n        ]\n    ),\n    \"reward_rich\": TrainingScenario(\n        name=\"Reward-Rich Environment\",\n        description=\"Frequent positive outcomes to develop reward pathways\",\n        duration_ticks=8000,\n        config_overrides={\n            \"food_spawn_chance\": 0.08,\n            \"poop_spawn_chance\": 0.001,\n        },\n        events=[\n            {\"tick\": 200, \"type\": \"feed\"},\n            {\"tick\": 600, \"type\": \"feed\"},\n            {\"tick\": 1200, \"type\": \"feed\"},\n        ]\n    ),\n    \"novelty_exploration\": TrainingScenario(\n        name=\"Novelty Exploration\",\n        description=\"High curiosity environment with new objects\",\n        duration_ticks=6000,\n        config_overrides={\n            \"startle_chance\": 0.001,\n        },\n        events=[\n            {\"tick\": 100, \"type\": \"set_state\", \"state\": {\"curiosity\": 85}},\n            {\"tick\": 300, \"type\": \"new_object\"},\n            {\"tick\": 800, \"type\": \"new_object\"},\n            {\"tick\": 1500, \"type\": \"new_object\"},\n        ]\n    ),\n    \"endurance\": TrainingScenario(\n        name=\"Endurance Training\",\n        description=\"Long-duration training with varied conditions\",\n        duration_ticks=50000,\n        config_overrides={},\n        events=[]\n    ),\n}\n\n\n# ============================================================================\n# HEADLESS SQUID (minimal state tracking)\n# ============================================================================\n\nclass HeadlessSquid:\n    \"\"\"Minimal squid implementation for headless training\"\"\"\n    \n    def __init__(self, personality: Personality = None):\n        self.personality = personality or random.choice(list(Personality))\n        \n        # Core stats (0-100)\n        self.hunger = 50.0\n        self.happiness = 50.0\n        self.cleanliness = 80.0\n        self.sleepiness = 20.0\n        self.health = 100.0\n        \n        # Goal neurons\n        self.satisfaction = 50.0\n        self.anxiety = 10.0\n        self.curiosity = 55.0\n        \n        # Boolean states\n        self.is_sick = False\n        self.is_sleeping = False\n        self.is_eating = False\n        self.is_fleeing = False\n        self.is_startled = False\n        self.pursuing_food = False\n        \n        # Position (normalized 0-1)\n        self.x = 0.5\n        self.y = 0.5\n        self.direction = \"right\"\n        \n        # Status\n        self.status = \"roaming\"\n        \n        # Tracking\n        self.food_visible = False\n        self.plant_nearby = False\n        self.time_since_ate = 0\n        self.time_since_slept = 0\n        \n    def get_state_dict(self) -> Dict[str, Any]:\n        \"\"\"Get current state as dictionary for brain updates\"\"\"\n        return {\n            \"hunger\": self.hunger,\n            \"happiness\": self.happiness,\n            \"cleanliness\": self.cleanliness,\n            \"sleepiness\": self.sleepiness,\n            \"satisfaction\": self.satisfaction,\n            \"anxiety\": self.anxiety,\n            \"curiosity\": self.curiosity,\n            \"is_sick\": self.is_sick,\n            \"is_sleeping\": self.is_sleeping,\n            \"is_eating\": self.is_eating,\n            \"pursuing_food\": self.pursuing_food,\n            \"is_fleeing\": self.is_fleeing,\n            \"is_startled\": self.is_startled,\n            \"can_see_food\": 100.0 if self.food_visible else 0.0,\n            \"plant_proximity\": 100.0 if self.plant_nearby else 0.0,\n            \"external_stimulus\": 0.0,\n            \"direction\": self.direction,\n            \"position\": (self.x, self.y),\n            \"personality\": self.personality.value,\n            \"status\": self.status,\n        }\n    \n    def update(self, dt: float = 1.0):\n        \"\"\"Update squid state for one tick\"\"\"\n        # Natural stat changes\n        self.hunger = min(100, self.hunger + 0.1 * dt)\n        self.sleepiness = min(100, self.sleepiness + 0.05 * dt)\n        self.cleanliness = max(0, self.cleanliness - 0.02 * dt)\n        \n        # Update time trackers\n        self.time_since_ate += dt\n        if not self.is_sleeping:\n            self.time_since_slept += dt\n        \n        # Personality effects\n        if self.personality == Personality.GREEDY:\n            self.hunger = min(100, self.hunger + 0.05 * dt)\n        elif self.personality == Personality.ENERGETIC:\n            self.sleepiness = max(0, self.sleepiness - 0.02 * dt)\n        elif self.personality == Personality.TIMID:\n            if self.plant_nearby:\n                self.anxiety = max(0, self.anxiety - 0.1 * dt)\n        \n        # Anxiety from needs\n        if self.hunger > 70:\n            self.anxiety = min(100, self.anxiety + 0.1 * dt)\n        if self.cleanliness < 30:\n            self.anxiety = min(100, self.anxiety + 0.05 * dt)\n            \n        # Satisfaction from good states\n        if self.hunger < 30 and self.cleanliness > 70:\n            self.satisfaction = min(100, self.satisfaction + 0.1 * dt)\n        else:\n            self.satisfaction = max(0, self.satisfaction - 0.02 * dt)\n            \n        # Happiness dynamics\n        if self.anxiety > 50:\n            self.happiness = max(0, self.happiness - 0.1 * dt)\n        elif self.satisfaction > 60:\n            self.happiness = min(100, self.happiness + 0.05 * dt)\n            \n        # Curiosity wanders\n        self.curiosity += random.uniform(-0.5, 0.5) * dt\n        self.curiosity = max(0, min(100, self.curiosity))\n        \n        # Clear temporary states\n        if self.is_startled:\n            if random.random() < 0.1:\n                self.is_startled = False\n                self.is_fleeing = False\n                \n        if self.is_eating:\n            if random.random() < 0.2:\n                self.is_eating = False\n                self.pursuing_food = False\n                \n        # Sleep behavior\n        if self.sleepiness > 90 and not self.is_sleeping:\n            self.is_sleeping = True\n            self.status = \"sleeping\"\n        elif self.is_sleeping and self.sleepiness < 20:\n            self.is_sleeping = False\n            self.time_since_slept = 0\n            self.status = \"roaming\"\n            \n        # Movement simulation\n        if not self.is_sleeping and not self.is_eating:\n            self.x += random.uniform(-0.02, 0.02)\n            self.y += random.uniform(-0.02, 0.02)\n            self.x = max(0, min(1, self.x))\n            self.y = max(0, min(1, self.y))\n            \n    def feed(self):\n        \"\"\"Feed the squid\"\"\"\n        self.hunger = max(0, self.hunger - 30)\n        self.happiness = min(100, self.happiness + 10)\n        self.satisfaction = min(100, self.satisfaction + 15)\n        self.is_eating = True\n        self.time_since_ate = 0\n        self.status = \"eating\"\n        \n    def startle(self):\n        \"\"\"Startle the squid\"\"\"\n        self.is_startled = True\n        self.is_fleeing = True\n        self.anxiety = min(100, self.anxiety + 20)\n        self.happiness = max(0, self.happiness - 10)\n        self.status = \"fleeing!\"\n        \n    def clean(self):\n        \"\"\"Clean the environment\"\"\"\n        self.cleanliness = min(100, self.cleanliness + 40)\n        self.anxiety = max(0, self.anxiety - 5)\n\n\n# ============================================================================\n# HEADLESS BRAIN\n# ============================================================================\n\nclass HeadlessBrain:\n    \"\"\"\n    Neural network brain without GUI dependencies.\n    Handles state updates, Hebbian learning, and neurogenesis.\n    \"\"\"\n    \n    def __init__(self, config: TrainingConfig = None):\n        self.config = config or TrainingConfig()\n        \n        # Neural state\n        self.state: Dict[str, float] = {}\n        self.weights: Dict[Tuple[str, str], float] = {}\n        self.positions: Dict[str, Tuple[float, float]] = {}\n        self.neuron_shapes: Dict[str, str] = {}\n        \n        # Custom neurons tracking\n        self.custom_neurons: Set[str] = set()\n        self.new_neurons: Set[str] = set()\n        self.connector_neurons: Set[str] = set()\n        \n        # Learning tracking\n        self.last_hebbian_pairs: List[Tuple[str, str]] = []\n        self.weight_history: List[Dict] = []\n        \n        # Neurogenesis tracking\n        self.neurogenesis_data = {\n            'new_neurons': [],\n            'last_neuron_time': 0,\n            'neurons_created': 0,\n            'functional_neurons': {},\n        }\n        self.last_neurogenesis_tick = 0\n        self.stress_neuron_count = 0\n        self.novelty_neuron_count = 0\n        self.reward_neuron_count = 0\n        \n        # Output bindings (for behaviors)\n        self.output_bindings: List[Dict] = []\n        \n        # Initialize default state\n        self._initialize_default_state()\n        \n    def _initialize_default_state(self):\n        \"\"\"Initialize with default neuron structure\"\"\"\n        default_positions = {\n            \"can_see_food\": (50, 200),\n            \"hunger\": (127, 81),\n            \"happiness\": (361, 81),\n            \"cleanliness\": (627, 81),\n            \"sleepiness\": (840, 81),\n            \"satisfaction\": (271, 380),\n            \"anxiety\": (491, 389),\n            \"curiosity\": (701, 386),\n        }\n        \n        for name, pos in default_positions.items():\n            self.positions[name] = pos\n            if name in CORE_NEURONS:\n                self.state[name] = 50.0\n            elif name in INPUT_SENSORS:\n                self.state[name] = 0.0\n            else:\n                self.state[name] = 50.0\n                \n        # Default connections\n        default_connections = [\n            (\"hunger\", \"satisfaction\", -0.3),\n            (\"happiness\", \"satisfaction\", 0.4),\n            (\"anxiety\", \"satisfaction\", -0.35),\n            (\"cleanliness\", \"happiness\", 0.2),\n            (\"sleepiness\", \"happiness\", -0.15),\n            (\"curiosity\", \"happiness\", 0.25),\n        ]\n        \n        for src, dst, weight in default_connections:\n            self.weights[(src, dst)] = weight\n            \n    def load_brain(self, brain_data: Dict) -> bool:\n        \"\"\"Load a brain from dictionary (JSON structure)\"\"\"\n        try:\n            # Clear existing\n            self.state.clear()\n            self.weights.clear()\n            self.positions.clear()\n            self.custom_neurons.clear()\n            \n            # Load positions/neurons — accept both 'positions' (headless export)\n            # and 'neurons' (Designer / game format).\n            positions_raw = brain_data.get('positions', brain_data.get('neurons', {}))\n            for name, pos in positions_raw.items():\n                if isinstance(pos, dict):\n                    # Designer format: {'position': [x, y], 'is_custom': bool}\n                    # Headless export format: {'x': float, 'y': float, 'is_custom': bool}\n                    if 'position' in pos:\n                        p = pos['position']\n                        self.positions[name] = (float(p[0]), float(p[1]))\n                    elif 'x' in pos and 'y' in pos:\n                        self.positions[name] = (float(pos['x']), float(pos['y']))\n                    else:\n                        self.positions[name] = (0.0, 0.0)\n                    if pos.get('is_custom', False):\n                        self.custom_neurons.add(name)\n                elif isinstance(pos, (list, tuple)) and len(pos) >= 2:\n                    self.positions[name] = (float(pos[0]), float(pos[1]))\n                else:\n                    # Fallback: unknown shape — park at origin rather than\n                    # storing a raw unsupported value.\n                    self.positions[name] = (0.0, 0.0)\n                    \n                # Initialize state\n                if name in CORE_NEURONS:\n                    self.state[name] = 50.0\n                elif name in INPUT_SENSORS:\n                    self.state[name] = 0.0\n                else:\n                    self.state[name] = 50.0\n                    self.custom_neurons.add(name)\n                    \n            # Load weights/connections\n            # Accept list format (Designer) and dict format (headless export).\n            weights_data = brain_data.get('weights', brain_data.get('connections', {}))\n            if isinstance(weights_data, dict):\n                for key, weight in weights_data.items():\n                    # Handle string keys like \"hunger,satisfaction\" or \"hunger->satisfaction\"\n                    if isinstance(key, str):\n                        if '->' in key:\n                            parts = key.split('->')\n                        else:\n                            parts = key.replace('(', '').replace(')', '').replace(\"'\", \"\").split(',')\n                        if len(parts) >= 2:\n                            src = parts[0].strip()\n                            dst = parts[1].strip()\n                            self.weights[(src, dst)] = float(weight)\n                    else:\n                        self.weights[tuple(key)] = float(weight)\n            elif isinstance(weights_data, list):\n                for conn in weights_data:\n                    if isinstance(conn, dict):\n                        src = conn.get('source', conn.get('from', ''))\n                        dst = conn.get('target', conn.get('to', ''))\n                        w = conn.get('weight', 0.5)\n                        if src and dst:\n                            self.weights[(src, dst)] = float(w)\n                    elif isinstance(conn, (list, tuple)) and len(conn) >= 2:\n                        self.weights[(conn[0], conn[1])] = float(conn[2]) if len(conn) > 2 else 0.5\n                        \n            # Load shapes\n            self.neuron_shapes = brain_data.get('neuron_shapes', {})\n            \n            # Load output bindings\n            self.output_bindings = brain_data.get('output_bindings', [])\n            \n            # Load neurogenesis data if present\n            if 'neurogenesis_data' in brain_data:\n                self.neurogenesis_data.update(brain_data['neurogenesis_data'])\n                \n            print(f\"✓ Loaded brain: {len(self.positions)} neurons, {len(self.weights)} connections\")\n            return True\n            \n        except Exception as e:\n            print(f\"✗ Error loading brain: {e}\")\n            return False\n            \n    def load_brain_file(self, filepath: str) -> bool:\n        \"\"\"Load brain from JSON file\"\"\"\n        try:\n            with open(filepath, 'r') as f:\n                brain_data = json.load(f)\n            return self.load_brain(brain_data)\n        except Exception as e:\n            print(f\"✗ Error loading brain file: {e}\")\n            return False\n            \n    def export_brain(self) -> Dict:\n        \"\"\"\n        Export brain in the Designer/game format understood by\n        custom_brain_loader.BrainLoader._parse() (Format 2: neurons dict +\n        connections list).  This ensures trained brains can be loaded\n        straight back into the game without any manual conversion.\n        \"\"\"\n        # neurons dict — position as list, is_custom flag\n        neurons = {}\n        for name, pos in self.positions.items():\n            neurons[name] = {\n                'position': [pos[0], pos[1]],\n                'is_custom': name in self.custom_neurons,\n            }\n\n        # connections as a list of {source, target, weight} dicts\n        # (BrainLoader._parse() Format 2 expects this exact structure)\n        connections = []\n        for (src, dst), w in self.weights.items():\n            connections.append({'source': src, 'target': dst, 'weight': float(w)})\n\n        return {\n            'metadata': {\n                'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'),\n                'exported_by': 'headless_trainer',\n                'neuron_count': len(self.positions),\n                'connection_count': len(self.weights),\n                'custom_neuron_count': len(self.custom_neurons),\n            },\n            # ── game-loader-compatible keys ──────────────────────────────────\n            'neurons': neurons,        # _parse() checks for 'neurons' key\n            'connections': connections, # _parse() Format 2: list of dicts\n            # ────────────────────────────────────────────────────────────────\n            'neuron_shapes': dict(self.neuron_shapes),\n            'output_bindings': self.output_bindings,\n            'state': {k: v for k, v in self.state.items()},\n            'neurogenesis_data': {\n                'neurons_created': self.neurogenesis_data.get('neurons_created', 0),\n                'stress_neurons':  self.stress_neuron_count,\n                'novelty_neurons': self.novelty_neuron_count,\n                'reward_neurons':  self.reward_neuron_count,\n            },\n        }\n        \n    def save_brain(self, filepath: str) -> bool:\n        \"\"\"Save brain to JSON file\"\"\"\n        try:\n            brain_data = self.export_brain()\n            with open(filepath, 'w') as f:\n                json.dump(brain_data, f, indent=2)\n            print(f\"✓ Saved brain to {filepath}\")\n            return True\n        except Exception as e:\n            print(f\"✗ Error saving brain: {e}\")\n            return False\n            \n    def update_state(self, squid_state: Dict[str, Any]):\n        \"\"\"Update brain state from squid state\"\"\"\n        for key, value in squid_state.items():\n            if key in self.positions or key in self.state:\n                if isinstance(value, bool):\n                    self.state[key] = 100.0 if value else 0.0\n                elif isinstance(value, (int, float)):\n                    self.state[key] = float(value)\n                    \n    def propagate(self):\n        \"\"\"Propagate activations through connections\"\"\"\n        updates = {}\n        \n        for (src, dst), weight in self.weights.items():\n            if src in self.state and dst in self.state:\n                if dst in PURE_INPUTS:\n                    continue\n                    \n                src_val = self.state[src]\n                effect = src_val * weight * 0.1\n                updates[dst] = updates.get(dst, 0) + effect\n                \n        # Apply updates with decay\n        for neuron, delta in updates.items():\n            if neuron in self.state and neuron not in PURE_INPUTS:\n                old_val = self.state[neuron]\n                new_val = old_val * self.config.decay_rate + delta\n                new_val += random.uniform(-self.config.noise_range, self.config.noise_range)\n                self.state[neuron] = max(-100, min(100, new_val))\n                \n    def perform_hebbian_learning(self) -> List[Tuple[str, str]]:\n        \"\"\"Perform Hebbian learning update\"\"\"\n        # Get learning candidates\n        excluded = set(PURE_INPUTS) | self.connector_neurons\n        candidates = [n for n in self.positions.keys() if n not in excluded]\n        \n        if len(candidates) < 2:\n            return []\n            \n        # Score pairs\n        scored_pairs = []\n        for i, n1 in enumerate(candidates):\n            for n2 in candidates[i+1:]:\n                v1 = self._get_neuron_value(self.state.get(n1, 50))\n                v2 = self._get_neuron_value(self.state.get(n2, 50))\n                score = v1 + v2 + random.uniform(0, 40)\n                \n                # Penalize recent pairs\n                pair_key = tuple(sorted((n1, n2)))\n                if pair_key in self.last_hebbian_pairs:\n                    score -= 500\n                    \n                # Boost custom neurons\n                if n1 in self.custom_neurons or n2 in self.custom_neurons:\n                    score += 15\n                    \n                scored_pairs.append((score, n1, n2, v1, v2))\n                \n        # Select top pairs\n        top_pairs = nlargest(self.config.max_hebbian_pairs, scored_pairs)\n        self.last_hebbian_pairs = [tuple(sorted((n1, n2))) for _, n1, n2, _, _ in top_pairs]\n        \n        updated_pairs = []\n        for _, n1, n2, v1, v2 in top_pairs:\n            pair = (n1, n2)\n            reverse_pair = (n2, n1)\n            \n            # Find existing connection\n            if pair in self.weights:\n                use_pair = pair\n            elif reverse_pair in self.weights:\n                use_pair = reverse_pair\n            else:\n                use_pair = pair\n                self.weights[use_pair] = 0.0\n                \n            old_w = self.weights[use_pair]\n            \n            # Calculate learning rate with boosts\n            lr = self.config.learning_rate\n            if n1 in self.new_neurons or n2 in self.new_neurons:\n                lr *= 2.0\n            if n1 in self.custom_neurons or n2 in self.custom_neurons:\n                lr *= 1.5\n                \n            # Hebbian update\n            delta = lr * (v1 / 100.0) * (v2 / 100.0)\n            new_w = old_w + delta - (old_w * self.config.weight_decay)\n            new_w = max(-1.0, min(1.0, new_w))\n            \n            self.weights[use_pair] = new_w\n            updated_pairs.append(use_pair)\n            \n            # Track history\n            self.weight_history.append({\n                'pair': use_pair,\n                'old': old_w,\n                'new': new_w,\n                'tick': time.time()\n            })\n            \n        return updated_pairs\n        \n    def check_neurogenesis(self, squid_state: Dict, tick: int) -> Optional[str]:\n        \"\"\"Check if neurogenesis should occur\"\"\"\n        if not self.config.neurogenesis_enabled:\n            return None\n            \n        if tick - self.last_neurogenesis_tick < self.config.neurogenesis_cooldown:\n            return None\n            \n        if len(self.positions) >= self.config.max_neurons:\n            return None\n            \n        # Check triggers\n        anxiety = squid_state.get('anxiety', 50)\n        curiosity = squid_state.get('curiosity', 50)\n        happiness = squid_state.get('happiness', 50)\n        satisfaction = squid_state.get('satisfaction', 50)\n        \n        trigger_type = None\n        \n        # Stress trigger\n        if anxiety > 70 or (anxiety > 50 and squid_state.get('is_fleeing', False)):\n            if self.stress_neuron_count < 5:  # Cap stress neurons\n                trigger_type = 'stress'\n                \n        # Novelty trigger\n        elif curiosity > 75:\n            trigger_type = 'novelty'\n            \n        # Reward trigger\n        elif happiness > 80 and satisfaction > 70:\n            trigger_type = 'reward'\n            \n        if trigger_type:\n            neuron_name = self._create_neuron(trigger_type, squid_state)\n            self.last_neurogenesis_tick = tick\n            return neuron_name\n            \n        return None\n        \n    def _create_neuron(self, neuron_type: str, context: Dict) -> str:\n        \"\"\"Create a new neuron\"\"\"\n        # Generate unique name\n        count = self.neurogenesis_data.get('neurons_created', 0) + 1\n        self.neurogenesis_data['neurons_created'] = count\n        \n        neuron_name = f\"{neuron_type}_{count}\"\n        \n        # Find position (spread from existing neurons)\n        existing_positions = list(self.positions.values())\n        if existing_positions:\n            avg_x = sum(p[0] for p in existing_positions) / len(existing_positions)\n            avg_y = sum(p[1] for p in existing_positions) / len(existing_positions)\n            \n            # Add some randomness\n            new_x = avg_x + random.uniform(-100, 100)\n            new_y = avg_y + random.uniform(-100, 100)\n        else:\n            new_x = random.uniform(100, 800)\n            new_y = random.uniform(100, 400)\n            \n        self.positions[neuron_name] = (new_x, new_y)\n        self.state[neuron_name] = 50.0\n        self.custom_neurons.add(neuron_name)\n        self.new_neurons.add(neuron_name)\n        \n        # Set shape based on type\n        shape_map = {'stress': 'hexagon', 'novelty': 'triangle', 'reward': 'circle'}\n        self.neuron_shapes[neuron_name] = shape_map.get(neuron_type, 'circle')\n        \n        # Create connections based on type\n        if neuron_type == 'stress':\n            self.stress_neuron_count += 1\n            self.weights[(neuron_name, 'anxiety')] = -0.3\n            self.weights[('anxiety', neuron_name)] = 0.4\n            \n        elif neuron_type == 'novelty':\n            self.novelty_neuron_count += 1\n            self.weights[(neuron_name, 'curiosity')] = 0.3\n            self.weights[('curiosity', neuron_name)] = 0.3\n            \n        elif neuron_type == 'reward':\n            self.reward_neuron_count += 1\n            self.weights[(neuron_name, 'satisfaction')] = 0.35\n            self.weights[(neuron_name, 'happiness')] = 0.25\n            \n        self.neurogenesis_data['new_neurons'].append(neuron_name)\n        \n        return neuron_name\n        \n    def _get_neuron_value(self, val) -> float:\n        \"\"\"Convert value to float for calculations\"\"\"\n        if isinstance(val, bool):\n            return 100.0 if val else 0.0\n        if isinstance(val, (int, float)):\n            return float(val)\n        return 0.0\n        \n    def get_statistics(self) -> Dict:\n        \"\"\"Get brain statistics\"\"\"\n        return {\n            'total_neurons': len(self.positions),\n            'total_connections': len(self.weights),\n            'custom_neurons': len(self.custom_neurons),\n            'stress_neurons': self.stress_neuron_count,\n            'novelty_neurons': self.novelty_neuron_count,\n            'reward_neurons': self.reward_neuron_count,\n            'weight_updates': len(self.weight_history),\n            'avg_weight': sum(self.weights.values()) / len(self.weights) if self.weights else 0,\n        }\n\n\n# ============================================================================\n# HEADLESS SIMULATION\n# ============================================================================\n\nclass HeadlessSimulation:\n    \"\"\"\n    Headless simulation runner for brain training.\n    Runs the simulation loop without GUI, with accelerated time.\n    \"\"\"\n    \n    def __init__(self, config: TrainingConfig = None):\n        self.config = config or TrainingConfig()\n        self.brain = HeadlessBrain(config)\n        self.squid = HeadlessSquid()\n        \n        # Simulation state\n        self.tick = 0\n        self.running = False\n        \n        # Environment\n        self.food_items: List[Dict] = []\n        self.poop_items: List[Dict] = []\n        \n        # Statistics\n        self.stats = {\n            'ticks_completed': 0,\n            'neurons_created': 0,\n            'hebbian_updates': 0,\n            'food_eaten': 0,\n            'startles': 0,\n            'peak_anxiety': 0,\n            'peak_happiness': 0,\n        }\n        \n        # Event queue (for scenarios)\n        self.event_queue: List[Dict] = []\n        \n        # Progress callback\n        self.progress_callback = None\n        \n    def load_brain(self, filepath: str) -> bool:\n        \"\"\"Load a brain from file\"\"\"\n        return self.brain.load_brain_file(filepath)\n        \n    def load_scenario(self, scenario_name: str) -> bool:\n        \"\"\"Load a predefined training scenario\"\"\"\n        if scenario_name not in TRAINING_SCENARIOS:\n            print(f\"✗ Unknown scenario: {scenario_name}\")\n            print(f\"  Available: {list(TRAINING_SCENARIOS.keys())}\")\n            return False\n            \n        scenario = TRAINING_SCENARIOS[scenario_name]\n        print(f\"✓ Loading scenario: {scenario.name}\")\n        print(f\"  Description: {scenario.description}\")\n        print(f\"  Duration: {scenario.duration_ticks} ticks\")\n        \n        # Apply config overrides\n        for key, value in scenario.config_overrides.items():\n            if hasattr(self.config, key):\n                setattr(self.config, key, value)\n                \n        # Queue events\n        self.event_queue = list(scenario.events)\n        \n        return True\n        \n    def run(self, ticks: int = 1000, progress_interval: int = 100) -> Dict:\n        \"\"\"\n        Run the simulation for specified number of ticks.\n        \n        Args:\n            ticks: Number of simulation ticks to run\n            progress_interval: How often to report progress\n            \n        Returns:\n            Dictionary with final statistics\n        \"\"\"\n        self.running = True\n        start_time = time.time()\n        \n        print(f\"\\n🚀 Starting headless training: {ticks} ticks\")\n        print(f\"   Brain: {len(self.brain.positions)} neurons, {len(self.brain.weights)} connections\")\n        print()\n        \n        for self.tick in range(ticks):\n            if not self.running:\n                break\n                \n            # Process queued events\n            self._process_events()\n            \n            # Simulation step\n            self._simulation_step()\n            \n            # Progress report\n            if progress_interval > 0 and (self.tick + 1) % progress_interval == 0:\n                elapsed = time.time() - start_time\n                tps = (self.tick + 1) / elapsed if elapsed > 0 else 0\n                self._report_progress(tps)\n                \n        # Final statistics\n        elapsed = time.time() - start_time\n        self.stats['ticks_completed'] = self.tick + 1\n        self.stats['elapsed_seconds'] = elapsed\n        self.stats['ticks_per_second'] = (self.tick + 1) / elapsed if elapsed > 0 else 0\n        \n        print(f\"\\n✓ Training complete!\")\n        print(f\"  Ticks: {self.stats['ticks_completed']}\")\n        print(f\"  Time: {elapsed:.2f}s ({self.stats['ticks_per_second']:.1f} ticks/sec)\")\n        print(f\"  Neurons created: {self.stats['neurons_created']}\")\n        print(f\"  Hebbian updates: {self.stats['hebbian_updates']}\")\n        \n        brain_stats = self.brain.get_statistics()\n        print(f\"\\n  Brain Statistics:\")\n        print(f\"    Total neurons: {brain_stats['total_neurons']}\")\n        print(f\"    Connections: {brain_stats['total_connections']}\")\n        print(f\"    Custom neurons: {brain_stats['custom_neurons']}\")\n        \n        return {**self.stats, **brain_stats}\n        \n    def _simulation_step(self):\n        \"\"\"Execute one simulation tick\"\"\"\n        # 1. Update squid state\n        self.squid.update()\n        \n        # 2. Environment events\n        self._update_environment()\n        \n        # 3. Update brain with squid state\n        squid_state = self.squid.get_state_dict()\n        self.brain.update_state(squid_state)\n        \n        # 4. Propagate brain activations\n        self.brain.propagate()\n        \n        # 5. Hebbian learning (periodic)\n        if self.tick % self.config.hebbian_interval == 0:\n            updated = self.brain.perform_hebbian_learning()\n            if updated:\n                self.stats['hebbian_updates'] += len(updated)\n                \n        # 6. Neurogenesis check\n        new_neuron = self.brain.check_neurogenesis(squid_state, self.tick)\n        if new_neuron:\n            self.stats['neurons_created'] += 1\n            print(f\"  🧠 Tick {self.tick}: Created neuron '{new_neuron}'\")\n            \n        # 7. Track statistics\n        self.stats['peak_anxiety'] = max(self.stats['peak_anxiety'], self.squid.anxiety)\n        self.stats['peak_happiness'] = max(self.stats['peak_happiness'], self.squid.happiness)\n        \n    def _update_environment(self):\n        \"\"\"Update environment (spawn/despawn items, random events)\"\"\"\n        # Food spawning\n        if random.random() < self.config.food_spawn_chance:\n            self.food_items.append({'x': random.random(), 'y': random.random()})\n            self.squid.food_visible = True\n            \n        # Food consumption\n        if self.squid.food_visible and random.random() < 0.1:\n            self.squid.feed()\n            self.stats['food_eaten'] += 1\n            if self.food_items:\n                self.food_items.pop(0)\n            self.squid.food_visible = len(self.food_items) > 0\n            \n        # Poop spawning\n        if random.random() < self.config.poop_spawn_chance:\n            self.poop_items.append({'x': random.random(), 'y': random.random()})\n            self.squid.cleanliness = max(0, self.squid.cleanliness - 5)\n            \n        # Random startle\n        if random.random() < self.config.startle_chance:\n            self.squid.startle()\n            self.stats['startles'] += 1\n            \n        # Plant proximity (random for now)\n        self.squid.plant_nearby = random.random() < 0.2\n        \n    def _process_events(self):\n        \"\"\"Process queued scenario events\"\"\"\n        events_to_remove = []\n        \n        for event in self.event_queue:\n            if event.get('tick', 0) == self.tick:\n                self._execute_event(event)\n                events_to_remove.append(event)\n                \n        for event in events_to_remove:\n            self.event_queue.remove(event)\n            \n    def _execute_event(self, event: Dict):\n        \"\"\"Execute a scenario event\"\"\"\n        event_type = event.get('type', '')\n        \n        if event_type == 'set_state':\n            state = event.get('state', {})\n            for key, value in state.items():\n                if hasattr(self.squid, key):\n                    setattr(self.squid, key, value)\n            print(f\"  📋 Tick {self.tick}: Set state {state}\")\n            \n        elif event_type == 'feed':\n            self.squid.feed()\n            self.stats['food_eaten'] += 1\n            print(f\"  🍕 Tick {self.tick}: Fed squid\")\n            \n        elif event_type == 'startle':\n            self.squid.startle()\n            self.stats['startles'] += 1\n            print(f\"  ⚡ Tick {self.tick}: Startled squid\")\n            \n        elif event_type == 'clean':\n            self.squid.clean()\n            print(f\"  🧹 Tick {self.tick}: Cleaned environment\")\n            \n        elif event_type == 'new_object':\n            self.squid.curiosity = min(100, self.squid.curiosity + 20)\n            print(f\"  🆕 Tick {self.tick}: New object encountered\")\n            \n    def _report_progress(self, tps: float):\n        \"\"\"Report training progress\"\"\"\n        stats = self.brain.get_statistics()\n        squid = self.squid\n        \n        progress_pct = (self.tick + 1) / max(1, self.stats.get('target_ticks', self.tick + 1)) * 100\n        \n        print(f\"  [{self.tick+1:6d}] {tps:6.1f} t/s | \"\n              f\"Neurons: {stats['total_neurons']:2d} | \"\n              f\"H:{squid.hunger:5.1f} A:{squid.anxiety:5.1f} S:{squid.satisfaction:5.1f} | \"\n              f\"Created: {self.stats['neurons_created']}\")\n              \n    def stop(self):\n        \"\"\"Stop the simulation\"\"\"\n        self.running = False\n\n\n# ============================================================================\n# COMMAND LINE INTERFACE\n# ============================================================================\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description='Dosidicus-2 Headless Brain Trainer',\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nExamples:\n  Train a brain for 10000 ticks:\n    python headless_trainer.py --brain my_brain.json --ticks 10000 --output trained.json\n    \n  Run a stress test scenario:\n    python headless_trainer.py --brain my_brain.json --scenario stress_test --output stress_trained.json\n    \n  List available scenarios:\n    python headless_trainer.py --list-scenarios\n    \n  Quick training with default brain:\n    python headless_trainer.py --ticks 5000 --output quick_trained.json\n        \"\"\"\n    )\n    \n    parser.add_argument('--brain', '-b', type=str, help='Path to brain JSON file to load')\n    parser.add_argument('--output', '-o', type=str, help='Path to save trained brain')\n    parser.add_argument('--ticks', '-t', type=int, default=10000, help='Number of ticks to train (default: 10000)')\n    parser.add_argument('--scenario', '-s', type=str, help='Training scenario to use')\n    parser.add_argument('--list-scenarios', action='store_true', help='List available training scenarios')\n    parser.add_argument('--progress', '-p', type=int, default=500, help='Progress report interval (default: 500)')\n    parser.add_argument('--quiet', '-q', action='store_true', help='Minimal output')\n    \n    # Config overrides\n    parser.add_argument('--learning-rate', type=float, help='Hebbian learning rate')\n    parser.add_argument('--neurogenesis', type=bool, help='Enable neurogenesis')\n    parser.add_argument('--max-neurons', type=int, help='Maximum neurons allowed')\n    \n    args = parser.parse_args()\n    \n    # List scenarios\n    if args.list_scenarios:\n        print(\"\\nAvailable Training Scenarios:\")\n        print(\"-\" * 60)\n        for name, scenario in TRAINING_SCENARIOS.items():\n            print(f\"\\n  {name}:\")\n            print(f\"    {scenario.description}\")\n            print(f\"    Duration: {scenario.duration_ticks} ticks\")\n            if scenario.config_overrides:\n                print(f\"    Config overrides: {scenario.config_overrides}\")\n        print()\n        return\n        \n    # Build config\n    config = TrainingConfig()\n    if args.learning_rate:\n        config.learning_rate = args.learning_rate\n    if args.neurogenesis is not None:\n        config.neurogenesis_enabled = args.neurogenesis\n    if args.max_neurons:\n        config.max_neurons = args.max_neurons\n        \n    # Create simulation\n    sim = HeadlessSimulation(config)\n    \n    # Load brain\n    if args.brain:\n        if not os.path.exists(args.brain):\n            print(f\"✗ Brain file not found: {args.brain}\")\n            sys.exit(1)\n        if not sim.load_brain(args.brain):\n            sys.exit(1)\n    else:\n        print(\"ℹ Using default brain architecture\")\n        \n    # Load scenario\n    ticks = args.ticks\n    if args.scenario:\n        if not sim.load_scenario(args.scenario):\n            sys.exit(1)\n        # Use scenario duration if longer\n        scenario = TRAINING_SCENARIOS.get(args.scenario)\n        if scenario and scenario.duration_ticks > ticks:\n            ticks = scenario.duration_ticks\n            \n    # Run training\n    progress = 0 if args.quiet else args.progress\n    sim.stats['target_ticks'] = ticks\n    \n    try:\n        stats = sim.run(ticks=ticks, progress_interval=progress)\n    except KeyboardInterrupt:\n        print(\"\\n\\n⚠ Training interrupted by user\")\n        sim.stop()\n        stats = sim.stats\n        \n    # Save output\n    if args.output:\n        sim.brain.save_brain(args.output)\n    else:\n        # Default output name\n        timestamp = time.strftime('%Y%m%d_%H%M%S')\n        output_name = f\"trained_brain_{timestamp}.json\"\n        sim.brain.save_brain(output_name)\n        \n    print(\"\\n✓ Done!\")\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "images/decoration/DecorationsFolder",
    "content": "\n"
  },
  {
    "path": "linux_setup.sh",
    "content": "#!/usr/bin/env bash\n# =============================================================\n#  Dosidicus - Linux Setup & Launch Script\n#  https://github.com/ViciousSquid/Dosidicus\n# =============================================================\n#\n# If you hit a Qt platform error like `could not load the Qt platform plugin \"xcb\", run the following:\n# sudo apt install libxcb-xinerama0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0\n\nset -e\n\nREPO_URL=\"https://github.com/ViciousSquid/Dosidicus.git\"\n# Use the current directory where the script lives\nINSTALL_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nVENV_DIR=\"$INSTALL_DIR/.venv\"\n\n# ── Colours ──────────────────────────────────────────────────\nRED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[1;33m'\nCYAN='\\033[0;36m'; BOLD='\\033[1m'; NC='\\033[0m'\n\ninfo()    { echo -e \"${CYAN}▸ $*${NC}\"; }\nsuccess() { echo -e \"${GREEN}✔ $*${NC}\"; }\nwarn()    { echo -e \"${YELLOW}⚠ $*${NC}\"; }\nerror()   { echo -e \"${RED}✖ $*${NC}\"; exit 1; }\n\necho -e \"${BOLD}\"\necho \"  DOSIDICUS  by ViciousSquid\"\necho -e \"${NC}\"\necho -e \"  ${BOLD}A cognitive sandbox for raising digital squids${NC}\"\necho \"  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\n\n# ── 1. Check Python ───────────────────────────────────────────\ninfo \"Checking Python version...\"\nif ! command -v python3 &>/dev/null; then\n    error \"python3 not found. Install it with: sudo apt install python3\"\nfi\n\nPY_VER=$(python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")')\nPY_MAJOR=$(echo \"$PY_VER\" | cut -d. -f1)\nPY_MINOR=$(echo \"$PY_VER\" | cut -d. -f2)\n\nif [[ \"$PY_MAJOR\" -lt 3 || (\"$PY_MAJOR\" -eq 3 && \"$PY_MINOR\" -lt 9) ]]; then\n    error \"Python 3.9+ required. Found: $PY_VER\"\nfi\nsuccess \"Python $PY_VER found\"\n\n# ── 2. Check / install system deps ───────────────────────────\ninfo \"Checking system dependencies...\"\n\nMISSING_PKGS=()\nfor pkg in git python3-venv python3-dev; do\n    if ! dpkg -s \"$pkg\" &>/dev/null 2>&1; then\n        MISSING_PKGS+=(\"$pkg\")\n    fi\ndone\n\n# PyQt5 often needs these on Linux\nfor pkg in libxcb-xinerama0 libxcb-cursor0 libgl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0; do\n    if ! dpkg -s \"$pkg\" &>/dev/null 2>&1; then\n        MISSING_PKGS+=(\"$pkg\")\n    fi\ndone\n\nif [[ ${#MISSING_PKGS[@]} -gt 0 ]]; then\n    warn \"Missing packages: ${MISSING_PKGS[*]}\"\n    echo -e \"  Installing with sudo...\"\n    sudo apt-get update -qq\n    sudo apt-get install -y -qq \"${MISSING_PKGS[@]}\"\n    success \"System packages installed\"\nelse\n    success \"All system packages present\"\nfi\n\n# ── 3. Clone or update repo ───────────────────────────────────\n# If we are already inside a git repo, just pull.\nif [[ -d \"$INSTALL_DIR/.git\" ]]; then\n    info \"Repo already exists at $INSTALL_DIR — pulling latest...\"\n    git -C \"$INSTALL_DIR\" pull --ff-only\n    success \"Updated to latest\"\nelif [[ ! -f \"$INSTALL_DIR/main.py\" ]]; then\n    info \"Cloning Dosidicus into $INSTALL_DIR...\"\n    git clone \"$REPO_URL\" \"$INSTALL_DIR\"\n    success \"Cloned successfully\"\nfi\n\ncd \"$INSTALL_DIR\"\n\n# ── 4. Create virtualenv ──────────────────────────────────────\nif [[ ! -d \"$VENV_DIR\" ]]; then\n    info \"Creating Python virtual environment...\"\n    python3 -m venv \"$VENV_DIR\"\n    success \"Virtual environment created\"\nelse\n    success \"Virtual environment already exists\"\nfi\n\n# Activate\n# shellcheck disable=SC1091\nsource \"$VENV_DIR/bin/activate\"\n\n# ── 5. Install Python dependencies ───────────────────────────\ninfo \"Installing Python dependencies...\"\npip install --upgrade pip --quiet\n\nif [[ -f \"requirements.txt\" ]]; then\n    pip install -r requirements.txt --quiet\nelse\n    pip install --quiet \"PyQt5>=5.15\" \"numpy>=1.21\"\nfi\n\n# Optional but recommended for PyQt5\npip install --quiet \"pyqt5-sip\" 2>/dev/null || true\n\nsuccess \"Python packages installed\"\n\n# ── 6. Create launcher script ─────────────────────────────────\nLAUNCHER=\"$INSTALL_DIR/run.sh\"\ncat > \"$LAUNCHER\" << LAUNCH\n#!/usr/bin/env bash\nDIR=\"\\$(cd \"\\$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"\\$DIR/.venv/bin/activate\"\n\n# Fix for Qt/XCB issues on some Linux setups\nexport QT_QPA_PLATFORM=\"\\${QT_QPA_PLATFORM:-xcb}\"\n# Crucial: This ensures main.py can find the 'src' folder\nexport PYTHONPATH=\"\\$DIR:\\$PYTHONPATH\"\n\ncd \"\\$DIR\"\nexec python3 main.py \"\\$@\"\nLAUNCH\nchmod +x \"$LAUNCHER\"\nsuccess \"Launcher script created: $LAUNCHER\"\n\n# ── 7. Create desktop shortcut (optional) ────────────────────\nDESKTOP_FILE=\"$HOME/.local/share/applications/dosidicus.desktop\"\nmkdir -p \"$(dirname \"$DESKTOP_FILE\")\"\ncat > \"$DESKTOP_FILE\" << DESKTOP\n[Desktop Entry]\nName=Dosidicus\nComment=A cognitive sandbox for raising digital squids\nExec=$LAUNCHER\nIcon=$INSTALL_DIR/images/icon.png\nTerminal=false\nType=Application\nCategories=Game;Simulation;\nDESKTOP\nsuccess \"Desktop shortcut created\"\n\n# ── 8. Done! ──────────────────────────────────────────────────\necho \"\"\necho -e \"${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho -e \"  Setup complete!\"\necho -e \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho \"\"\necho -e \"  ${BOLD}To launch Dosidicus:${NC}\"\necho -e \"    ${CYAN}$LAUNCHER${NC}\"\necho \"\"\necho -e \"  ${BOLD}Or from any terminal:${NC}\"\necho -e \"    ${CYAN}cd $INSTALL_DIR && source .venv/bin/activate && python3 main.py${NC}\"\necho \"\"\necho -e \"  ${BOLD}Headless/training mode:${NC}\"\necho -e \"    ${CYAN}cd $INSTALL_DIR && source .venv/bin/activate && python3 headless/headless_trainer.py${NC}\"\necho \"\"\n\n# ── 9. Offer to launch now ────────────────────────────────────\nread -rp \"  Launch Dosidicus now? [y/N] \" REPLY\nif [[ \"$REPLY\" =~ ^[Yy]$ ]]; then\n    info \"Launching...\"\n    exec \"$LAUNCHER\"\nfi\n"
  },
  {
    "path": "main.py",
    "content": "# Dosidicus - a digital pet with a neural network   |   2.1.2.0 March 2026\n# main.py Entrypoint \nfrom PyQt5 import QtWidgets, QtCore, QtGui\nimport sys\nimport os\n\n# BOOTSTRAP: Add the current directory and src directory to sys.path\n# This ensures \"from src.ui import Ui\" works even if launched from elsewhere.\nBASE_DIR = os.path.dirname(os.path.abspath(__file__))\nif BASE_DIR not in sys.path:\n    sys.path.insert(0, BASE_DIR)\n\nimport time\nimport sys\nimport json\nimport os\nimport shutil\nimport traceback\nimport multiprocessing\nimport logging\nfrom PyQt5 import QtWidgets, QtCore\nimport random\nimport argparse\nfrom src.ui import Ui\nfrom src.tamagotchi_logic import TamagotchiLogic\nfrom src.squid import Squid, Personality\nfrom src.splash_screen import SplashScreen\nfrom src.save_manager import SaveManager\nfrom src.brain_tool import SquidBrainWindow\nfrom src.display_scaling import DisplayScaling\nfrom src.learning import LearningConfig\nfrom src.plugin_manager import PluginManager\nfrom src.brain_worker import BrainWorker\nfrom src.config_manager import ConfigManager\nfrom src.localisation import Localisation\n\ndef launch_brain_designer_process():\n    \"\"\"Entry point for Brain Designer in a separate process\"\"\"\n    from src.brain_designer_launcher import launch_brain_designer_process as _launch\n    _launch()\n\ndef setup_logging_configuration():\n    \"\"\"Initialize logging configuration\"\"\"\n    os.environ['QT_LOGGING_RULES'] = '*.debug=false;qt.qpa.*=false;qt.style.*=false'\n    os.makedirs('logs', exist_ok=True)\n\n    logging.basicConfig(\n        filename='logs/dosidicus_log.txt',\n        level=logging.ERROR,\n        format='%(asctime)s - %(levelname)s - %(message)s'\n    )\n\ndef perform_cleanup_and_exit():\n    \"\"\"Recursively delete __pycache__ and logs directories.\"\"\"\n    print(\"🧹 Cleaning environment...\")\n    root_dir = os.path.dirname(os.path.abspath(__file__))\n    \n    deleted_count = 0\n    \n    for root, dirs, files in os.walk(root_dir, topdown=True):\n        # Filter and remove specific directories\n        # We iterate over a copy of dirs so we can modify the original list safely\n        for name in list(dirs):\n            if name in ['__pycache__', 'logs']:\n                path = os.path.join(root, name)\n                try:\n                    shutil.rmtree(path)\n                    print(f\"   Deleted: {path}\")\n                    dirs.remove(name)  # Prevent os.walk from trying to enter this dir\n                    deleted_count += 1\n                except Exception as e:\n                    print(f\"   ❌ Failed to delete {path}: {e}\")\n                    \n    print(f\"✨ Cleanup complete. Removed {deleted_count} directories.\")\n\ndef global_exception_handler(exctype, value, tb):\n    \"\"\"Global exception handler to log unhandled exceptions\"\"\"\n    error_message = ''.join(traceback.format_exception(exctype, value, tb))\n    logging.error(\"Unhandled exception:\\n%s\", error_message)\n    QtWidgets.QMessageBox.critical(None, \"Error\", \n                                 \"An unexpected error occurred. Please check dosidicus_log.txt for details.\")\n\n\nclass TeeStream:\n    \"\"\"Duplicate output to both console and file\"\"\"\n    def __init__(self, original_stream, file_stream):\n        self.original_stream = original_stream\n        self.file_stream = file_stream\n\n    def write(self, data):\n        self.original_stream.write(data)\n        self.file_stream.write(data)\n        self.file_stream.flush()\n\n    def flush(self):\n        self.original_stream.flush()\n        self.file_stream.flush()\n\nclass TimedMessageBox(QtWidgets.QDialog):\n    \"\"\"A message box that auto-closes after a timeout with a default choice\"\"\"\n    def __init__(self, parent, title, message, timeout_seconds=5):\n        super().__init__(parent)\n        self.setWindowTitle(title)\n        self.timeout_seconds = timeout_seconds\n        self.remaining_seconds = timeout_seconds\n        self.result_value = QtWidgets.QMessageBox.No  # Default to No\n        self.loc = Localisation.instance()\n        \n        # Setup UI\n        layout = QtWidgets.QVBoxLayout()\n        \n        self.message_label = QtWidgets.QLabel(message)\n        self.message_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(13)}pt;\")\n        layout.addWidget(self.message_label)\n        \n        # Auto-decline message\n        self.timer_label = QtWidgets.QLabel(self.loc.get(\"auto_decline\", seconds=self.remaining_seconds))\n        self.timer_label.setStyleSheet(f\"color: gray; font-size: {DisplayScaling.font_size(11)}pt;\")\n        layout.addWidget(self.timer_label)\n        \n        # Buttons\n        self.button_box = QtWidgets.QDialogButtonBox(\n            QtWidgets.QDialogButtonBox.Yes | QtWidgets.QDialogButtonBox.No\n        )\n        # Localise buttons manually since we use a custom dict system\n        self.button_box.button(QtWidgets.QDialogButtonBox.Yes).setText(self.loc.get(\"yes\"))\n        self.button_box.button(QtWidgets.QDialogButtonBox.No).setText(self.loc.get(\"no\"))\n\n        self.button_box.accepted.connect(self.accept_yes)\n        self.button_box.rejected.connect(self.reject_no)\n        layout.addWidget(self.button_box)\n        \n        self.setLayout(layout)\n        \n        # Setup timer\n        self.timer = QtCore.QTimer(self)\n        self.timer.timeout.connect(self.update_countdown)\n        self.timer.start(1000)  # Update every second\n\n\n        \n    def update_countdown(self):\n        \"\"\"Update the countdown and auto-close when time runs out\"\"\"\n        self.remaining_seconds -= 1\n        self.timer_label.setText(self.loc.get(\"auto_decline\", seconds=self.remaining_seconds))\n        \n        if self.remaining_seconds <= 0:\n            self.timer.stop()\n            self.reject_no()  # Auto-close with No\n    \n    def accept_yes(self):\n        \"\"\"User clicked Yes\"\"\"\n        self.timer.stop()\n        self.result_value = QtWidgets.QMessageBox.Yes\n        self.accept()\n    \n    def reject_no(self):\n        \"\"\"User clicked No or timeout occurred\"\"\"\n        self.timer.stop()\n        self.result_value = QtWidgets.QMessageBox.No\n        self.reject()\n    \n    def get_result(self):\n        \"\"\"Get the result after dialog closes\"\"\"\n        return self.result_value\n    \n\n\nclass MainWindow(QtWidgets.QMainWindow):\n    def __init__(self, specified_personality=None, debug_mode=False, neuro_cooldown=None):\n        super().__init__()\n        \n        # Apply configured language from config.ini\n        config_manager = ConfigManager()\n        language = config_manager.get_language()\n        Localisation.instance().set_language(language)\n        print(f\"📄 Applied language from config: {language}\")\n        \n        # Initialize configuration\n        self.config = LearningConfig()\n        if neuro_cooldown is not None:\n            self.config.neurogenesis['cooldown'] = neuro_cooldown\n        \n        # Add initialization tracking flag\n        self._initialization_complete = False\n        \n        # Set up debugging\n        self.debug_mode = debug_mode\n        if self.debug_mode:\n            self.setup_logging()\n        \n        # Initialize UI first\n        logging.debug(\"Initializing UI\")\n        self.user_interface = Ui(self, debug_mode=self.debug_mode)\n\n        # Initialize SquidBrainWindow with config\n        logging.debug(\"Initializing SquidBrainWindow\")\n        self.brain_window = SquidBrainWindow(None, self.debug_mode, self.config)\n        \n        # Store the original window reference to prevent garbage collection\n        self._brain_window_ref = self.brain_window\n        \n        # Explicitly force creation of all tab contents\n        QtCore.QTimer.singleShot(100, self.preload_brain_window_tabs)\n        \n        # Continue with normal initialization\n        self.brain_window.set_tamagotchi_logic(None)  # Placeholder to ensure initialization\n        self.user_interface.squid_brain_window = self.brain_window\n        \n        # Initialize plugin manager after UI and brain window\n        logging.debug(\"Initializing PluginManager\")\n        self.plugin_manager = PluginManager()\n        print(f\"> Plugin manager initialized: {self.plugin_manager}\")\n        \n        self.specified_personality = specified_personality\n        self.neuro_cooldown = neuro_cooldown\n        self.squid = None\n        \n        # Check for existing save data\n        self.save_manager = SaveManager(\"saves\")\n        \n        # Track whether we want to show tutorial\n        self.show_tutorial = False\n\n        # ===== PERFORMANCE FIX: Single BrainWorker managed by brain_tool =====\n        # Don't create another worker here - SquidBrainWindow creates and shares it\n        # Access via self.brain_window.brain_worker if needed\n        self.brain_worker = None\n        print(\"ℹ️ BrainWorker managed by SquidBrainWindow\")\n        \n        # Initialize the game\n        logging.debug(\"Initializing game\")\n        self.initialize_game()\n        \n        # Now that tamagotchi_logic is created, set it in plugin_manager and brain_window\n        logging.debug(\"Setting tamagotchi_logic references\")\n        self.plugin_manager.tamagotchi_logic = self.tamagotchi_logic\n        self.tamagotchi_logic.plugin_manager = self.plugin_manager\n        self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n        # New in 2.4.5.0 : Create a unique personality starter neuron\n        squid = self.tamagotchi_logic.squid\n        brain_widget = self.brain_window.brain_widget\n        if (squid and squid.personality and\n            brain_widget and hasattr(brain_widget, 'enhanced_neurogenesis')):\n\n            if not squid._has_personality_starter_neuron():\n                neuron = brain_widget.enhanced_neurogenesis.create_personality_starter_neuron(\n                    squid.personality.value,\n                    brain_widget.state\n                )\n                if neuron:\n                    print(f\"🧬 Personality starter neuron created: {neuron}\")\n        \n        # Load and initialize plugins after core components\n        logging.debug(\"Loading plugins\")\n        plugin_results = self.plugin_manager.load_all_plugins()\n        \n        # Setup plugins with tamagotchi_logic reference\n        for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n            instance = plugin_data.get('instance')\n            if instance and hasattr(instance, 'setup') and not plugin_data.get('is_setup', False):\n                try:\n                    instance.setup(self.plugin_manager, self.tamagotchi_logic)\n                    plugin_data['is_setup'] = True\n                except Exception as e:\n                    print(f\"Error setting up plugin {plugin_name}: {e}\")\n        \n        # CRITICAL FIX: Re-load achievement data since plugin instances were replaced\n        # during load_all_plugins(), discarding any data loaded earlier\n        if self.save_manager.save_exists():\n            save_data = self.save_manager.load_game()\n            if save_data and 'achievements' in save_data:\n                self._restore_achievements_data(save_data['achievements'])\n        \n        # Update status bar with plugin information\n        if hasattr(self.user_interface, 'status_bar'):\n            self.user_interface.status_bar.update_plugins_status(self.plugin_manager)\n        \n        # Connect signals\n        self.user_interface.new_game_action.triggered.connect(self.start_new_game)\n        self.user_interface.load_action.triggered.connect(self.load_game)\n        self.user_interface.save_action.triggered.connect(self.save_game)\n        self.user_interface.decorations_action.triggered.connect(self.user_interface.toggle_decoration_window)\n        \n        # Initialize plugin menu - do this AFTER loading plugins\n        self.user_interface.apply_plugin_menu_registrations(self.plugin_manager)\n    \n        # Position window 300 pixels to the left of default position\n        desktop = QtWidgets.QApplication.desktop()\n        screen_rect = desktop.screenGeometry()\n        window_rect = self.geometry()\n        center_x = screen_rect.center().x()\n        window_x = center_x - (window_rect.width() // 2)  # Default centered X position\n        \n        # Move 300 pixels to the left\n        self.move(window_x - 300, self.y())\n        \n        if self.debug_mode:\n            print(f\"DEBUG MODE ENABLED: Console output is being logged to console.txt\")\n\n        self.setup_facts_timer()\n\n    def preload_brain_window_tabs(self):\n        \"\"\"Force creation of all tab contents to prevent crashes during tutorial\"\"\"\n        print(\"Pre-loading brain window tabs...\")\n        if not hasattr(self, 'brain_window') or not self.brain_window:\n            print(\"⚠️  Brain window not initialized, cannot preload\")\n            return\n            \n        try:\n            # Force the window to process events and initialize all tabs\n            if hasattr(self.brain_window, 'tabs'):\n                # Visit each tab to ensure it's loaded\n                tab_count = self.brain_window.tabs.count()\n                \n                # Initialize tabs array to prevent garbage collection\n                if not hasattr(self, '_preloaded_tabs'):\n                    self._preloaded_tabs = []\n                \n                # Remember if window was visible before we started\n                was_visible = self.brain_window.isVisible()\n                #print(f\"📋 Brain window was_visible before preload: {was_visible}\")\n                    \n                # Temporarily show the window off-screen to force loading\n                original_pos = self.brain_window.pos()\n                self.brain_window.move(-10000, -10000)  # Move off-screen\n                self.brain_window.show()\n                \n                # Force each tab to be displayed at least once\n                for i in range(tab_count):\n                    self.brain_window.tabs.setCurrentIndex(i)\n                    QtWidgets.QApplication.processEvents()\n                    \n                    # Get and store references to tab widgets\n                    widget = self.brain_window.tabs.widget(i)\n                    if widget:\n                        self._preloaded_tabs.append(widget)\n                        #print(f\"  ✓ Preloaded tab {i}: {self.brain_window.tabs.tabText(i)}\")\n                \n                # Return to first tab (Network/Brain tab)\n                self.brain_window.tabs.setCurrentIndex(0)\n                QtWidgets.QApplication.processEvents()\n                #print(f\"📋 Reset to first tab: {self.brain_window.tabs.tabText(0)}\")\n                \n                # Restore original position\n                self.brain_window.move(original_pos)\n                \n                # Only hide if it wasn't visible before (don't hide if user is viewing it)\n                if not was_visible:\n                    self.brain_window.hide()\n                    print(\"📋 Brain window hidden after preload (was not visible before)\")\n                else:\n                    print(\"📋 Brain window kept visible after preload (was visible before)\")\n                \n                print(f\"✅ Successfully preloaded {len(self._preloaded_tabs)} tabs\")\n        except Exception as e:\n            print(f\"❌ Error preloading tabs: {e}\")\n            import traceback\n            traceback.print_exc()\n\n    def setup_logging(self):\n        \"\"\"Set up console logging to file\"\"\"\n        if not hasattr(sys, '_original_stdout'):\n            sys._original_stdout = sys.stdout\n            sys._original_stderr = sys.stderr\n            \n        console_log = open('console.txt', 'w', encoding='utf-8')\n        sys.stdout = TeeStream(sys._original_stdout, console_log)\n        sys.stderr = TeeStream(sys._original_stderr, console_log)\n\n    def setup_facts_timer(self):\n        \"\"\"Rare ocean-blue Humboldt squid facts every 5 minutes\"\"\"\n        config_manager = ConfigManager()\n        if not config_manager.get_facts_enabled():\n            return\n        self.fact_timer = QtCore.QTimer(self)\n        self.fact_timer.timeout.connect(self.show_random_squid_fact)\n        self.fact_timer.start(config_manager.get_fact_interval_ms())\n\n    def show_random_squid_fact(self):\n        \"\"\"Show one short Humboldt squid fact – big, bright blue, always visible\"\"\"\n        try:\n            from src.squid_facts import get_random_fact\n            fact = get_random_fact()\n            if not fact:\n                return\n\n            msg = f\"🌊 Humboldt Fact: {fact}\"\n\n            # Preferred: status bar (works everywhere, supports color)\n            if hasattr(self.user_interface, 'status_bar') and self.user_interface.status_bar:\n                colored_msg = f'<span style=\"color:#00BFFF; font-weight:bold; font-size:14px;\">{msg}</span>'\n                self.user_interface.status_bar.showMessage(colored_msg, 8000)  # 8 seconds\n            else:\n                # Fallback: plain message\n                self.user_interface.show_message(msg)\n\n            print(f\"[Facts] DISPLAYED: {fact[:80]}...\")  # helpful console confirmation\n\n        except Exception as e:\n            print(f\"[Facts] Error showing fact: {e}\")\n\n    def initialize_game(self):\n        if hasattr(self.save_manager, 'cleanup_duplicate_saves'):\n            self.save_manager.cleanup_duplicate_saves()\n        \n        if self.save_manager.save_exists() and self.specified_personality is None:\n            print(\"\\x1b[32mExisting save data found and will be loaded\\x1b[0m\")\n            self.squid = Squid(self.user_interface, None, None)\n            self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n\n            self.squid.tamagotchi_logic = self.tamagotchi_logic\n            self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n            self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n            if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n                self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n            self.load_game()\n\n            if hasattr(self.tamagotchi_logic, 'statistics_window'):\n                self.tamagotchi_logic.statistics_window.update_statistics()\n\n                brain_widget = self.brain_window.brain_widget\n\n                for name in brain_widget.original_neurons:\n                    brain_widget.visible_neurons.add(name)\n                if hasattr(brain_widget, 'neurogenesis_data'):\n                    for name in brain_widget.neurogenesis_data.get('new_neurons_details', {}):\n                        brain_widget.visible_neurons.add(name)\n\n                core = brain_widget.original_neurons\n                for idx, name in enumerate(core):\n                    QtCore.QTimer.singleShot(idx * 500, lambda n=name: brain_widget.reveal_neuron(n))\n\n                self.brain_window.show()\n                self.user_interface.brain_action.setChecked(True)\n        else:\n            print(\"\\x1b[92m--------------  STARTING A NEW SIMULATION --------------\\x1b[0m\")\n\n            self.create_new_game(self.specified_personality)\n            self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n\n            self.squid.tamagotchi_logic = self.tamagotchi_logic\n            self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n            self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n            if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n                self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n            if not self.save_manager.save_exists():\n                QtCore.QTimer.singleShot(500, self.delayed_tutorial_check)\n\n        self._initialization_complete = True\n\n    def delayed_tutorial_check(self):\n        \"\"\"Check if the user wants to see the tutorial after UI is responsive\"\"\"\n        # Process pending events to ensure UI is responsive\n        QtWidgets.QApplication.processEvents()\n        \n        # Now check tutorial preference\n        self.check_tutorial_preference()\n        \n        # If tutorial was chosen, schedule it for later\n        if self.show_tutorial:\n            # We'll show tutorial when the game starts\n            pass\n        else:\n            # Just open initial windows if no tutorial\n            QtCore.QTimer.singleShot(500, self.open_initial_windows)\n\n    def create_new_game(self, specified_personality=None):\n        \"\"\"Create a new game instance\"\"\"\n        # Delete any existing save to ensure clean start\n        if self.save_manager.save_exists():\n            self.save_manager.delete_save()\n        \n        # Choose personality randomly if not specified\n        if specified_personality is None:\n            personality = random.choice(list(Personality))\n        else:\n            personality = specified_personality\n        \n        # Create new squid with chosen personality\n        self.squid = Squid(\n            user_interface=self.user_interface,\n            tamagotchi_logic=None,\n            personality=personality,\n            neuro_cooldown=self.neuro_cooldown\n        )\n        \n        print(f\"    \")\n        print(f\">> Generated squid personality: {self.squid.personality.value}\")\n        print(f\"    \")\n        if self.neuro_cooldown:\n            print(f\"\\x1b[43m Neurogenesis cooldown:\\033[0m {self.neuro_cooldown}\")\n        \n        self.squid.memory_manager.clear_all_memories()\n        self.show_splash_screen()\n\n    def check_tutorial_preference(self):\n        \"\"\"Show a dialog asking if the user wants to see the tutorial with 5-second timeout\"\"\"\n        # Don't ask about tutorial if save data exists\n        if self.save_manager.save_exists():\n            self.show_tutorial = False\n            return\n            \n        # Show timed dialog\n        dialog = TimedMessageBox(\n            self,\n            Localisation.instance().get(\"startup\"),\n            Localisation.instance().get(\"show_tutorial_q\"),\n            timeout_seconds=5\n        )\n        dialog.exec_()\n        \n        # Set flag based on user's choice (defaults to No if timeout)\n        self.show_tutorial = (dialog.get_result() == QtWidgets.QMessageBox.Yes)\n    \n    def position_and_show_decoration_window(self):\n        \"\"\"Position the decoration window in the bottom right and show it\"\"\"\n        if hasattr(self.user_interface, 'decoration_window') and self.user_interface.decoration_window:\n            # Get screen geometry\n            screen_geometry = QtWidgets.QApplication.desktop().availableGeometry()\n            \n            # Position window in bottom right\n            decoration_window = self.user_interface.decoration_window\n            decoration_window.move(\n                screen_geometry.right() - decoration_window.width() - 20,\n                screen_geometry.bottom() - decoration_window.height() - 20\n            )\n            decoration_window.show()\n            self.user_interface.decorations_action.setChecked(True)\n\n    def start_new_game(self):\n        \"\"\"Start a new game, deleting any existing save\"\"\"\n        # First, ask for confirmation with a timed dialog\n        confirm_dialog = TimedMessageBox(\n            self,\n            \"Confirm New Game\",\n            \"Are you sure you want to start a new game? This will delete all current progress and save data.\",\n            timeout_seconds=10\n        )\n        confirm_dialog.exec_()\n        \n        # If user declined or let it timeout, abort\n        if confirm_dialog.get_result() != QtWidgets.QMessageBox.Yes:\n            print(\"New game cancelled by user\")\n            return\n        \n        print(\"Starting new game...\")\n        \n        # Ask about tutorial\n        tutorial_dialog = TimedMessageBox(\n            self,\n            Localisation.instance().get(\"tutorial_title\"),\n            Localisation.instance().get(\"tutorial_query\"),\n            timeout_seconds=5\n        )\n        tutorial_dialog.exec_()\n        self.show_tutorial = (tutorial_dialog.get_result() == QtWidgets.QMessageBox.Yes)\n        \n        # Stop current simulation if running\n        if hasattr(self, 'tamagotchi_logic'):\n            self.tamagotchi_logic.stop()\n            # Stop autosave timer if it exists\n            if hasattr(self.tamagotchi_logic, 'autosave_timer'):\n                self.tamagotchi_logic.autosave_timer.stop()\n        \n        # Delete all save files (both autosave and manual save)\n        if self.save_manager.save_exists():\n            self.save_manager.delete_save(is_autosave=True)  # Delete autosave\n            self.save_manager.delete_save(is_autosave=False)  # Delete manual save\n            print(\"All save files deleted\")\n        \n        # Clear memory files\n        memory_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '_memory')\n        if os.path.exists(memory_dir):\n            import shutil\n            shutil.rmtree(memory_dir)\n            print(\"Memory directory cleared\")\n        \n        # Clear all neurons and state from brain window\n        if hasattr(self, 'brain_window') and hasattr(self.brain_window, 'brain_widget'):\n            brain_widget = self.brain_window.brain_widget\n            \n            # Clear visible neurons\n            brain_widget.visible_neurons = set()\n            \n            # Clear neurogenesis data\n            if hasattr(brain_widget, 'neurogenesis_data'):\n                brain_widget.neurogenesis_data = {\n                    'new_neurons': [],\n                    'new_neurons_details': {},\n                    'new_synapses': []\n                }\n            \n            # Clear enhanced neurogenesis tracking\n            if hasattr(brain_widget, 'enhanced_neurogenesis'):\n                brain_widget.enhanced_neurogenesis.reset_state()\n            \n            # Reset brain widget state\n            if hasattr(brain_widget, 'state'):\n                brain_widget.state = brain_widget.create_initial_state()\n            \n            # Clear hebbian learning state\n            if hasattr(brain_widget, 'hebbian'):\n                brain_widget.hebbian.reset()\n            \n            print(\"Brain state cleared\")\n        \n        # Clear all decorations and items from the scene\n        if hasattr(self, 'user_interface') and hasattr(self.user_interface, 'scene'):\n            # Remove all items except the background (if it exists)\n            items_to_remove = []\n            background_item = getattr(self.user_interface, 'background', None)\n            \n            for item in self.user_interface.scene.items():\n                # Keep the background (if it exists) and remove everything else\n                if background_item is None or item != background_item:\n                    items_to_remove.append(item)\n            \n            for item in items_to_remove:\n                self.user_interface.scene.removeItem(item)\n            \n            # Clear decoration tracking\n            if hasattr(self.user_interface, 'awarded_decorations'):\n                self.user_interface.awarded_decorations = set()\n            \n            print(\"Scene cleared\")\n        \n        # Create new game (creates squid but not tamagotchi_logic)\n        self.create_new_game(self.specified_personality)\n        \n        # Create TamagotchiLogic\n        self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n        \n        # Update references\n        self.squid.tamagotchi_logic = self.tamagotchi_logic\n        self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n        self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n        if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n            self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n        \n        self.plugin_manager.tamagotchi_logic = self.tamagotchi_logic\n        self.tamagotchi_logic.plugin_manager = self.plugin_manager\n        \n        # Create personality starter neuron if needed\n        squid = self.tamagotchi_logic.squid\n        brain_widget = self.brain_window.brain_widget\n        if (squid and squid.personality and\n            brain_widget and hasattr(brain_widget, 'enhanced_neurogenesis')):\n            if not squid._has_personality_starter_neuron():\n                neuron = brain_widget.enhanced_neurogenesis.create_personality_starter_neuron(\n                    squid.personality.value,\n                    brain_widget.state\n                )\n                if neuron:\n                    print(f\"🧬 Personality starter neuron created: {neuron}\")\n        \n        # Reload plugins to ensure they get the new tamagotchi_logic\n        self.plugin_manager.reload_all_plugins()\n        \n        print(\"New game created successfully!\")\n\n    def load_game(self):\n        \"\"\"Delegate to tamagotchi_logic\"\"\"\n        self.tamagotchi_logic.load_game()\n\n    def save_game(self):\n        \"\"\"Delegate to tamagotchi_logic\"\"\"\n        if self.squid and self.tamagotchi_logic:\n            self.tamagotchi_logic.save_game()\n\n    def _restore_achievements_data(self, achievements_data):\n        \"\"\"Restore achievement data to the achievements plugin after plugin reload.\n        \n        This is needed because plugin instances get replaced during initialization,\n        discarding any previously loaded save data.\n        \"\"\"\n        if not achievements_data:\n            return\n        try:\n            if 'achievements' in self.plugin_manager.plugins:\n                plugin_info = self.plugin_manager.plugins['achievements']\n                instance = plugin_info.get('instance')\n                if instance and hasattr(instance, 'load_save_data'):\n                    instance.load_save_data(achievements_data)\n                    unlocked_count = len(achievements_data.get('unlocked', {}))\n                    print(f\"✓ Restored {unlocked_count} achievements\")\n        except Exception as e:\n            print(f\"[Warning] Could not restore achievements: {e}\")\n\n    def closeEvent(self, event):\n        \"\"\"Handle window close event\"\"\"\n        # Save game before closing\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            self.save_game()\n        \n        # Stop the tamagotchi logic if it has a stop method\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'stop'):\n                self.tamagotchi_logic.stop()\n            # Stop the timer if it exists\n            elif hasattr(self.tamagotchi_logic, 'timer') and self.tamagotchi_logic.timer:\n                self.tamagotchi_logic.timer.stop()\n        \n        # Clean up brain state bridge for designer sync\n        if hasattr(self, 'brain_window') and self.brain_window:\n            if hasattr(self.brain_window, 'brain_widget') and self.brain_window.brain_widget:\n                if hasattr(self.brain_window.brain_widget, 'cleanup_brain_bridge'):\n                    self.brain_window.brain_widget.cleanup_brain_bridge()\n        \n        # Close brain window\n        if hasattr(self, 'brain_window') and self.brain_window:\n            self.brain_window.close()\n        \n        event.accept()\n\n    def show_splash_screen(self):\n        \"\"\"Display splash screen animation with synchronized neuron reveal\"\"\"\n        self.splash = SplashScreen(self)\n        self.splash.finished.connect(self.start_simulation)\n        self.splash.finished.connect(lambda: self.tamagotchi_logic.statistics_window.award(1000))\n        self.splash.second_frame.connect(self.show_hatching_notification)\n\n        # NEW: award 1000 points the instant the splash ends\n        self.splash.finished.connect(\n            lambda: self.tamagotchi_logic.statistics_window.award(1000)\n        )\n\n       # After splash ends, wait 3 s then show the normal feeding hint\n        self.splash.finished.connect(lambda: QtCore.QTimer.singleShot(3000, self.show_feeding_hint))\n\n        # Check if this is a brand new game (no save exists)\n        is_new_game = not self.save_manager.save_exists()\n        print(f\"🎮 show_splash_screen: is_new_game={is_new_game}, save_exists={self.save_manager.save_exists()}\")\n\n        if is_new_game:\n            # Ensure brain widget starts empty\n            if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'visible_neurons'):\n                self.brain_window.brain_widget.visible_neurons = set()\n            \n            # Show brain window first\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n            \n            # Force immediate processing to ensure brain window is painted\n            QtWidgets.QApplication.processEvents()\n            \n            # Give the brain window time to fully render (longer delay)\n            QtCore.QTimer.singleShot(1500, lambda: self._start_splash_with_reveals())\n        else:\n            # For loaded games, show brain window with all neurons visible\n            if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'visible_neurons'):\n                brain_widget = self.brain_window.brain_widget\n                # Add all core neurons to visible set\n                for neuron_name in brain_widget.original_neurons:\n                    brain_widget.visible_neurons.add(neuron_name)\n                \n                # Also add any neurogenesis neurons that exist\n                if hasattr(brain_widget, 'neurogenesis_data') and 'new_neurons_details' in brain_widget.neurogenesis_data:\n                    for neuron_name in brain_widget.neurogenesis_data['new_neurons_details'].keys():\n                        brain_widget.visible_neurons.add(neuron_name)\n            \n            # Show brain window immediately for loaded games\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n            \n            # Force immediate processing to ensure brain window is painted\n            QtWidgets.QApplication.processEvents()\n            \n            # Show splash normally (no animated reveals needed for loaded games)\n            self.splash.show()\n            QtCore.QTimer.singleShot(1000, self.splash.start_animation)\n\n\n    def show_feeding_hint(self):\n        \"\"\"Use the same strip as every other message.\"\"\"\n        self.user_interface.show_message(\"Press D to open the Decorations window\")\n    \n    def _start_splash_with_reveals(self):\n        \"\"\"Start splash screen with neuron reveal synchronization (called after brain window is ready)\"\"\"\n        print(\"     🥚 A squid is hatching...\")\n        \n        # Connect frame changes to neuron reveals\n        self.splash.frame_changed.connect(self._reveal_neuron_for_frame)\n        \n        # Show and start the splash screen animation\n        self.splash.show()\n        QtCore.QTimer.singleShot(500, self.splash.start_animation)  # Small delay for splash to show\n\n    def _reveal_neuron_for_frame(self, frame_index):\n        \"\"\"Reveal core neurons in sequence with animation frames\"\"\"\n        if not hasattr(self.brain_window, 'brain_widget'):\n            return\n            \n        brain_widget = self.brain_window.brain_widget\n        core_neurons = brain_widget.original_neurons\n        \n        # Distribution: 1-2 neurons per frame. Now revised for 8 core neurons (indices 0-7)\n        reveal_map = {\n            0: [0],       # First frame\n            1: [1],       # Second frame\n            2: [2],       # Third frame\n            3: [3],       # Fourth frame\n            4: [4, 5],    # Fifth frame\n            5: [6, 7]     # Sixth frame\n        }\n        \n        # Reveal mapped neurons for this frame\n        for neuron_idx in reveal_map.get(frame_index, []):\n            if neuron_idx < len(core_neurons):\n                neuron_name = core_neurons[neuron_idx]\n                brain_widget.reveal_neuron(neuron_name)\n                #print(f\"🧠 Revealed neuron: {neuron_name} (frame {frame_index})\")\n\n    def show_hatching_notification(self):\n        \"\"\"Display hatching message\"\"\"\n        self.user_interface.show_message(\"Squid is hatching!\")\n\n    def start_simulation(self):\n        \"\"\"Begin the simulation - brain window is already visible for new games\"\"\"\n        self.cleanup_duplicate_squids()\n        self.tamagotchi_logic.set_simulation_speed(1)\n        self.tamagotchi_logic.start_autosave()\n\n        # Get brain widget reference\n        brain_widget = self.brain_window.brain_widget\n\n        # Show tutorial if enabled\n        if self.show_tutorial:\n            QtCore.QTimer.singleShot(1000, self.user_interface.show_tutorial_overlay)\n        else:\n            # === FIX START: Manual cleanup if tutorial is skipped ===\n            if hasattr(brain_widget, 'is_tutorial_mode'):\n                # Set the flag to False, which allows connections to draw\n                brain_widget.is_tutorial_mode = False \n                \n                # OPTIONAL: If setting the flag doesn't immediately refresh the links,\n                # you may need to force a repaint. If the links are set to show,\n                # a simple repaint will usually draw them once the block is gone.\n                brain_widget.update() # Force repaint\n            # === FIX END ===\n\n            # Only open decoration window automatically (brain window already visible for new games)\n            QtCore.QTimer.singleShot(500, self.position_and_show_decoration_window)\n\n    def show_tutorial_overlay(self):\n        \"\"\"Delegate to UI layer and ensure no duplicates remain\"\"\"\n        # First do one more duplicate cleanup\n        self.cleanup_duplicate_squids()\n        \n        # Then show the tutorial via the UI\n        if hasattr(self, 'user_interface') and self.user_interface:\n            self.user_interface.show_tutorial_overlay()\n\n    def open_initial_windows(self):\n        \"\"\"Open brain window and decorations window\"\"\"\n        # Open brain window\n        if hasattr(self, 'brain_window'):\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n\n        # Open decorations window\n        if hasattr(self.user_interface, 'decoration_window'):\n            self.position_and_show_decoration_window()\n            self.user_interface.decorations_action.setChecked(True)\n\n    def cleanup_duplicate_squids(self):\n        \"\"\"Remove any duplicate squid items from the scene\"\"\"\n        if not hasattr(self, 'user_interface') or not self.user_interface:\n            return\n            \n        if not hasattr(self, 'squid') or not self.squid:\n            return\n            \n        try:\n            # Get the reference to our genuine squid item\n            main_squid_item = self.squid.squid_item\n            \n            # Get all items in the scene\n            all_items = self.user_interface.scene.items()\n            \n            # Track how many items we find and remove\n            found_count = 0\n            \n            # Look for graphics items that could be duplicate squids\n            for item in all_items:\n                # Skip our genuine squid item\n                if item == main_squid_item:\n                    continue\n                    \n                # Only check QGraphicsPixmapItems\n                if isinstance(item, QtWidgets.QGraphicsPixmapItem):\n                    # Check if it has the same pixmap dimensions as our squid\n                    if (hasattr(item, 'pixmap') and item.pixmap() and main_squid_item.pixmap() and\n                        item.pixmap().width() == main_squid_item.pixmap().width() and\n                        item.pixmap().height() == main_squid_item.pixmap().height()):\n                        print(f\"Found potential duplicate squid item - removing\")\n                        self.user_interface.scene.removeItem(item)\n                        found_count += 1\n            \n            if found_count > 0:\n                print(f\"Cleaned up {found_count} duplicate squid items\")\n                # Force scene update\n                self.user_interface.scene.update()\n        \n        except Exception as e:\n            print(f\"Error during cleanup: {str(e)}\")\n\n    def initialize_multiplayer_manually(self):\n        \"\"\"Manually initialize multiplayer plugin if needed\"\"\"\n        try:\n            # Import the plugin module directly\n            import sys\n            import os\n            plugin_path = os.path.join(os.path.dirname(__file__), 'plugins', 'multiplayer')\n            if plugin_path not in sys.path:\n                sys.path.insert(0, plugin_path)\n                \n            import main as multiplayer_main\n            \n            # Create plugin instance\n            multiplayer_plugin = multiplayer_main.MultiplayerPlugin()\n            \n            # Find it in plugin_manager and add the instance\n            for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n                if plugin_name.lower() == \"multiplayer\":\n                    plugin_data['instance'] = multiplayer_plugin\n                    print(f\"Manually added multiplayer plugin instance to {plugin_name}\")\n                    \n                    # Initialize the plugin\n                    if hasattr(multiplayer_plugin, 'setup'):\n                        multiplayer_plugin.setup(self.plugin_manager)\n                    \n                    # Register menu actions\n                    if hasattr(multiplayer_plugin, 'register_menu_actions'):\n                        multiplayer_plugin.register_menu_actions()\n                    \n                    break\n                    \n            # Force the UI to refresh plugin menu\n            self.user_interface.setup_plugin_menu(self.plugin_manager)\n            \n            #print(\"Manual multiplayer initialization complete\")\n            return True\n            \n        except Exception as e:\n            print(f\"Error in manual multiplayer initialization: {e}\")\n            import traceback\n            traceback.print_exc()\n            return False\n\ndef main():\n    \"\"\"Main entry point\"\"\"\n    # CRITICAL for PyInstaller + multiprocessing on Windows\n    multiprocessing.freeze_support()\n    \n    sys.excepthook = global_exception_handler\n\n    parser = argparse.ArgumentParser(description=\"Dosidicus digital squid with a neural network\")\n    parser.add_argument('-p', '--personality', type=str, \n                       choices=[p.value for p in Personality], \n                       help='Specify squid personality')\n    parser.add_argument('-d', '--debug', action='store_true', \n                       help='Enable debug mode with console logging')\n    parser.add_argument('-nc', '--neurocooldown', type=int, \n                       help='Set neurogenesis cooldown in seconds')\n    parser.add_argument('-c', '--clean', action='store_true',\n                       help='Clean __pycache__ and logs folders before starting')\n    parser.add_argument('-designer', '--designer', action='store_true',\n                       help='Launch Brain Designer standalone')\n    args = parser.parse_args()\n\n    # Perform cleanup if requested before logging setup\n    if args.clean:\n        perform_cleanup_and_exit()\n\n    # Launch designer if flag is set\n    if args.designer:\n        print(\"Launching Brain Designer standalone...\")\n        try:\n            # Import and run designer's main function\n            try:\n                from src import brain_designer\n            except ImportError:\n                import brain_designer\n            \n            # brain_designer.main() will parse sys.argv and handle -d and -c flags automatically\n            brain_designer.main()\n        except ImportError as e:\n            print(f\"Error: Could not import brain_designer module: {e}\")\n            sys.exit(1)\n        except Exception as e:\n            print(f\"Error launching designer: {e}\")\n            sys.exit(1)\n        return  # Exit after designer closes\n\n    # Initialize logging (replaces previous global setup)\n    setup_logging_configuration()\n    \n    print(f\"    Personality: {args.personality}\")\n    print(f\"    Debug mode: {args.debug}\")\n    print(f\"    Cooldown {args.neurocooldown or 'will be loaded from config'}\")\n\n    app = QtWidgets.QApplication(sys.argv)\n    \n    try:\n        personality = Personality(args.personality) if args.personality else None\n        main_window = MainWindow(personality, args.debug, args.neurocooldown)\n        main_window.show()\n        sys.exit(app.exec_())\n    except Exception as e:\n        logging.exception(\"Fatal error in main\")\n        QtWidgets.QMessageBox.critical(None, \"Error\", \n                                     f\"Critical error: {str(e)}\\nSee dosidicus_log.txt for details.\")\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "plugins/achievements/__init__.py",
    "content": ""
  },
  {
    "path": "plugins/achievements/achievements_data.py",
    "content": "# File: achievements_data.py\n# All achievement definitions for the Achievements Plugin\n# Separated from main.py for cleaner organization\n\nfrom dataclasses import dataclass, asdict\nfrom typing import Dict\nfrom enum import Enum\n\n\n# =============================================================================\n# ENUMS AND DATA CLASSES\n# =============================================================================\n\nclass AchievementCategory(Enum):\n    FEEDING = \"feeding\"\n    NEUROGENESIS = \"neurogenesis\"\n    SLEEP = \"sleep\"\n    MILESTONES = \"milestones\"\n    EXPLORATION = \"exploration\"\n    CLEANING = \"cleaning\"\n    HEALTH = \"health\"\n    INTERACTION = \"interaction\"\n    INK = \"ink\"\n    MEMORY = \"memory\"\n    EMOTIONAL = \"emotional\"\n    SECRET = \"secret\"\n    META = \"meta\"\n\n\n@dataclass\nclass Achievement:\n    id: str\n    name: str # Now stores the localisation KEY\n    description: str # Now stores the localisation KEY\n    icon: str = \"🏆\"\n    category: str = \"milestones\"\n    hidden: bool = False\n    points: int = 10\n    tier: int = 1\n    target_count: int = 1\n\n    def to_dict(self) -> dict:\n        return asdict(self)\n\n\n@dataclass\nclass UnlockedAchievement:\n    achievement_id: str\n    unlocked_at: str\n    progress: int = 0\n    notified: bool = False\n\n    def to_dict(self) -> dict:\n        return asdict(self)\n\n    @classmethod\n    def from_dict(cls, data: dict) -> 'UnlockedAchievement':\n        return cls(**data)\n\n\n# Tier colors for UI\nTIER_COLORS = {\n    1: \"#CD7F32\",  # Bronze\n    2: \"#C0C0C0\",  # Silver\n    3: \"#FFD700\",  # Gold\n    4: \"#E5E4E2\",  # Platinum\n    5: \"#B9F2FF\",  # Diamond\n}\n\n\n# =============================================================================\n# ALL ACHIEVEMENT DEFINITIONS (50+ achievements)\n# =============================================================================\n\nACHIEVEMENT_DEFINITIONS: Dict[str, Achievement] = {\n    \n    # =========================================================================\n    # FEEDING CATEGORY (5)\n    # =========================================================================\n    \"first_feeding\": Achievement(\n        id=\"first_feeding\", \n        name=\"ach_first_feeding_name\",\n        description=\"ach_first_feeding_desc\",\n        icon=\"🍽️\", category=\"feeding\", points=10, tier=1,\n    ),\n    \"fed_10_times\": Achievement(\n        id=\"fed_10_times\", \n        name=\"ach_fed_10_times_name\",\n        description=\"ach_fed_10_times_desc\",\n        icon=\"🥄\", category=\"feeding\", points=15, tier=1, target_count=10,\n    ),\n    \"fed_50_times\": Achievement(\n        id=\"fed_50_times\", \n        name=\"ach_fed_50_times_name\",\n        description=\"ach_fed_50_times_desc\",\n        icon=\"🍴\", category=\"feeding\", points=25, tier=2, target_count=50,\n    ),\n    \"fed_100_times\": Achievement(\n        id=\"fed_100_times\", \n        name=\"ach_fed_100_times_name\",\n        description=\"ach_fed_100_times_desc\",\n        icon=\"👨‍🍳\", category=\"feeding\", points=50, tier=3, target_count=100,\n    ),\n    \"fed_500_times\": Achievement(\n        id=\"fed_500_times\", \n        name=\"ach_fed_500_times_name\",\n        description=\"ach_fed_500_times_desc\",\n        icon=\"🌟\", category=\"feeding\", points=100, tier=4, target_count=500, hidden=True,\n    ),\n\n    # =========================================================================\n    # NEUROGENESIS CATEGORY (6)\n    # =========================================================================\n    \"first_neuron\": Achievement(\n        id=\"first_neuron\", \n        name=\"ach_first_neuron_name\",\n        description=\"ach_first_neuron_desc\",\n        icon=\"🧠\", category=\"neurogenesis\", points=20, tier=1,\n    ),\n    \"neurons_10\": Achievement(\n        id=\"neurons_10\", \n        name=\"ach_neurons_10_name\",\n        description=\"ach_neurons_10_desc\",\n        icon=\"🔮\", category=\"neurogenesis\", points=30, tier=2, target_count=10,\n    ),\n    \"neurons_50\": Achievement(\n        id=\"neurons_50\", \n        name=\"ach_neurons_50_name\",\n        description=\"ach_neurons_50_desc\",\n        icon=\"💫\", category=\"neurogenesis\", points=50, tier=3, target_count=50,\n    ),\n    \"neurons_100\": Achievement(\n        id=\"neurons_100\", \n        name=\"ach_neurons_100_name\",\n        description=\"ach_neurons_100_desc\",\n        icon=\"🌌\", category=\"neurogenesis\", points=75, tier=4, target_count=100, hidden=True,\n    ),\n    \"first_neuron_levelup\": Achievement(\n        id=\"first_neuron_levelup\", \n        name=\"ach_first_neuron_levelup_name\",\n        description=\"ach_first_neuron_levelup_desc\",\n        icon=\"⚡\", category=\"neurogenesis\", points=15, tier=1,\n    ),\n    \"neuron_max_level\": Achievement(\n        id=\"neuron_max_level\", \n        name=\"ach_neuron_max_level_name\",\n        description=\"ach_neuron_max_level_desc\",\n        icon=\"🌠\", category=\"neurogenesis\", points=40, tier=3,\n    ),\n\n    # =========================================================================\n    # SLEEP CATEGORY (3)\n    # =========================================================================\n    \"first_sleep\": Achievement(\n        id=\"first_sleep\", \n        name=\"ach_first_sleep_name\",\n        description=\"ach_first_sleep_desc\",\n        icon=\"😴\", category=\"sleep\", points=10, tier=1,\n    ),\n    \"slept_10_times\": Achievement(\n        id=\"slept_10_times\", \n        name=\"ach_slept_10_times_name\",\n        description=\"ach_slept_10_times_desc\",\n        icon=\"🛏️\", category=\"sleep\", points=20, tier=2, target_count=10,\n    ),\n    \"dream_state\": Achievement(\n        id=\"dream_state\", \n        name=\"ach_dream_state_name\",\n        description=\"ach_dream_state_desc\",\n        icon=\"💭\", category=\"sleep\", points=25, tier=2, hidden=True,\n    ),\n\n    # =========================================================================\n    # MILESTONES CATEGORY (6)\n    # =========================================================================\n    \"age_1_hour\": Achievement(\n        id=\"age_1_hour\", \n        name=\"ach_age_1_hour_name\",\n        description=\"ach_age_1_hour_desc\",\n        icon=\"⏰\", category=\"milestones\", points=15, tier=1,\n    ),\n    \"age_10_hours\": Achievement(\n        id=\"age_10_hours\", \n        name=\"ach_age_10_hours_name\",\n        description=\"ach_age_10_hours_desc\",\n        icon=\"📅\", category=\"milestones\", points=30, tier=2,\n    ),\n    \"age_24_hours\": Achievement(\n        id=\"age_24_hours\", \n        name=\"ach_age_24_hours_name\",\n        description=\"ach_age_24_hours_desc\",\n        icon=\"🎂\", category=\"milestones\", points=50, tier=3,\n    ),\n    \"age_1_week\": Achievement(\n        id=\"age_1_week\", \n        name=\"ach_age_1_week_name\",\n        description=\"ach_age_1_week_desc\",\n        icon=\"🏅\", category=\"milestones\", points=100, tier=4, hidden=True,\n    ),\n    \"age_1_month\": Achievement(\n        id=\"age_1_month\", \n        name=\"ach_age_1_month_name\",\n        description=\"ach_age_1_month_desc\",\n        icon=\"🎖️\", category=\"milestones\", points=150, tier=5, hidden=True,\n    ),\n    \"happiness_100\": Achievement(\n        id=\"happiness_100\", \n        name=\"ach_happiness_100_name\",\n        description=\"ach_happiness_100_desc\",\n        icon=\"😄\", category=\"milestones\", points=20, tier=2,\n    ),\n    \"all_stats_high\": Achievement(\n        id=\"all_stats_high\", \n        name=\"ach_all_stats_high_name\",\n        description=\"ach_all_stats_high_desc\",\n        icon=\"⚖️\", category=\"milestones\", points=40, tier=3,\n    ),\n\n    # =========================================================================\n    # CLEANING CATEGORY (3)\n    # =========================================================================\n    \"first_clean\": Achievement(\n        id=\"first_clean\", \n        name=\"ach_first_clean_name\",\n        description=\"ach_first_clean_desc\",\n        icon=\"🧼\", category=\"cleaning\", points=10, tier=1,\n    ),\n    \"cleaned_25_times\": Achievement(\n        id=\"cleaned_25_times\", \n        name=\"ach_cleaned_25_times_name\",\n        description=\"ach_cleaned_25_times_desc\",\n        icon=\"✨\", category=\"cleaning\", points=25, tier=2, target_count=25,\n    ),\n    \"germaphobe\": Achievement(\n        id=\"germaphobe\", \n        name=\"ach_germaphobe_name\",\n        description=\"ach_germaphobe_desc\",\n        icon=\"🧹\", category=\"cleaning\", points=30, tier=2,\n    ),\n\n    # =========================================================================\n    # HEALTH CATEGORY (3)\n    # =========================================================================\n    \"first_medicine\": Achievement(\n        id=\"first_medicine\", \n        name=\"ach_first_medicine_name\",\n        description=\"ach_first_medicine_desc\",\n        icon=\"💊\", category=\"health\", points=10, tier=1,\n    ),\n    \"medicine_10_times\": Achievement(\n        id=\"medicine_10_times\", \n        name=\"ach_medicine_10_times_name\",\n        description=\"ach_medicine_10_times_desc\",\n        icon=\"🩺\", category=\"health\", points=20, tier=2, target_count=10,\n    ),\n    \"comeback_kid\": Achievement(\n        id=\"comeback_kid\", \n        name=\"ach_comeback_kid_name\",\n        description=\"ach_comeback_kid_desc\",\n        icon=\"💪\", category=\"health\", points=40, tier=3, hidden=True,\n    ),\n\n    # =========================================================================\n    # INTERACTION - ROCKS (6)\n    # =========================================================================\n    \"first_rock_pickup\": Achievement(\n        id=\"first_rock_pickup\", \n        name=\"ach_first_rock_pickup_name\",\n        description=\"ach_first_rock_pickup_desc\",\n        icon=\"🪨\", category=\"interaction\", points=10, tier=1,\n    ),\n    \"rocks_picked_10\": Achievement(\n        id=\"rocks_picked_10\", \n        name=\"ach_rocks_picked_10_name\",\n        description=\"ach_rocks_picked_10_desc\",\n        icon=\"⛰️\", category=\"interaction\", points=15, tier=1, target_count=10,\n    ),\n    \"rocks_picked_50\": Achievement(\n        id=\"rocks_picked_50\", \n        name=\"ach_rocks_picked_50_name\",\n        description=\"ach_rocks_picked_50_desc\",\n        icon=\"🏔️\", category=\"interaction\", points=30, tier=2, target_count=50,\n    ),\n    \"first_rock_throw\": Achievement(\n        id=\"first_rock_throw\", \n        name=\"ach_first_rock_throw_name\",\n        description=\"ach_first_rock_throw_desc\",\n        icon=\"🎯\", category=\"interaction\", points=10, tier=1,\n    ),\n    \"rocks_thrown_25\": Achievement(\n        id=\"rocks_thrown_25\", \n        name=\"ach_rocks_thrown_25_name\",\n        description=\"ach_rocks_thrown_25_desc\",\n        icon=\"🚀\", category=\"interaction\", points=20, tier=2, target_count=25,\n    ),\n    \"rocks_thrown_100\": Achievement(\n        id=\"rocks_thrown_100\", \n        name=\"ach_rocks_thrown_100_name\",\n        description=\"ach_rocks_thrown_100_desc\",\n        icon=\"💨\", category=\"interaction\", points=40, tier=3, target_count=100, hidden=True,\n    ),\n\n    # =========================================================================\n    # INTERACTION - PLANTS & DECORATIONS (8)\n    # =========================================================================\n    \"first_decoration_push\": Achievement(\n        id=\"first_decoration_push\", \n        name=\"ach_first_decoration_push_name\",\n        description=\"ach_first_decoration_push_desc\",\n        icon=\"🪴\", category=\"interaction\", points=10, tier=1,\n    ),\n    \"decorations_pushed_10\": Achievement(\n        id=\"decorations_pushed_10\", \n        name=\"ach_decorations_pushed_10_name\",\n        description=\"ach_decorations_pushed_10_desc\",\n        icon=\"🏠\", category=\"interaction\", points=15, tier=1, target_count=10,\n    ),\n    \"decorations_pushed_50\": Achievement(\n        id=\"decorations_pushed_50\", \n        name=\"ach_decorations_pushed_50_name\",\n        description=\"ach_decorations_pushed_50_desc\",\n        icon=\"🎨\", category=\"interaction\", points=30, tier=2, target_count=50,\n    ),\n    \"first_plant_interact\": Achievement(\n        id=\"first_plant_interact\", \n        name=\"ach_first_plant_interact_name\",\n        description=\"ach_first_plant_interact_desc\",\n        icon=\"🌱\", category=\"interaction\", points=10, tier=1,\n    ),\n    \"plants_interacted_10\": Achievement(\n        id=\"plants_interacted_10\", \n        name=\"ach_plants_interacted_10_name\",\n        description=\"ach_plants_interacted_10_desc\",\n        icon=\"🌿\", category=\"interaction\", points=15, tier=1, target_count=10,\n    ),\n    \"plants_interacted_50\": Achievement(\n        id=\"plants_interacted_50\", \n        name=\"ach_plants_interacted_50_name\",\n        description=\"ach_plants_interacted_50_desc\",\n        icon=\"🌳\", category=\"interaction\", points=30, tier=2, target_count=50,\n    ),\n    \"objects_investigated_25\": Achievement(\n        id=\"objects_investigated_25\", \n        name=\"ach_objects_investigated_25_name\",\n        description=\"ach_objects_investigated_25_desc\",\n        icon=\"🔍\", category=\"interaction\", points=25, tier=2, target_count=25,\n    ),\n    \"objects_investigated_100\": Achievement(\n        id=\"objects_investigated_100\", \n        name=\"ach_objects_investigated_100_name\",\n        description=\"ach_objects_investigated_100_desc\",\n        icon=\"🕵️\", category=\"interaction\", points=50, tier=3, target_count=100,\n    ),\n\n    # =========================================================================\n    # EXPLORATION - POOP (1)\n    # =========================================================================\n    \"first_poop_throw\": Achievement(\n        id=\"first_poop_throw\", \n        name=\"ach_first_poop_throw_name\",\n        description=\"ach_first_poop_throw_desc\",\n        icon=\"💩\", category=\"exploration\", points=10, tier=1,\n    ),\n\n    # =========================================================================\n    # INK CATEGORY (2)\n    # =========================================================================\n    \"first_ink_cloud\": Achievement(\n        id=\"first_ink_cloud\", \n        name=\"ach_first_ink_cloud_name\",\n        description=\"ach_first_ink_cloud_desc\",\n        icon=\"🖤\", category=\"ink\", points=15, tier=1,\n    ),\n    \"ink_clouds_20\": Achievement(\n        id=\"ink_clouds_20\", \n        name=\"ach_ink_clouds_20_name\",\n        description=\"ach_ink_clouds_20_desc\",\n        icon=\"🌫️\", category=\"ink\", points=25, tier=2, target_count=20,\n    ),\n\n    # =========================================================================\n    # MEMORY CATEGORY (3)\n    # =========================================================================\n    \"first_memory\": Achievement(\n        id=\"first_memory\", \n        name=\"ach_first_memory_name\",\n        description=\"ach_first_memory_desc\",\n        icon=\"💾\", category=\"memory\", points=15, tier=1,\n    ),\n    \"memory_long_term\": Achievement(\n        id=\"memory_long_term\", \n        name=\"ach_memory_long_term_name\",\n        description=\"ach_memory_long_term_desc\",\n        icon=\"🗄️\", category=\"memory\", points=25, tier=2,\n    ),\n    \"memories_50\": Achievement(\n        id=\"memories_50\", \n        name=\"ach_memories_50_name\",\n        description=\"ach_memories_50_desc\",\n        icon=\"📚\", category=\"memory\", points=40, tier=3, target_count=50,\n    ),\n\n    # =========================================================================\n    # EMOTIONAL CATEGORY (4)\n    # =========================================================================\n    \"curiosity_100\": Achievement(\n        id=\"curiosity_100\", \n        name=\"ach_curiosity_100_name\",\n        description=\"ach_curiosity_100_desc\",\n        icon=\"🤔\", category=\"emotional\", points=15, tier=1,\n    ),\n    \"zen_master\": Achievement(\n        id=\"zen_master\", \n        name=\"ach_zen_master_name\",\n        description=\"ach_zen_master_desc\",\n        icon=\"🧘\", category=\"emotional\", points=30, tier=2,\n    ),\n    \"first_startle\": Achievement(\n        id=\"first_startle\", \n        name=\"ach_first_startle_name\",\n        description=\"ach_first_startle_desc\",\n        icon=\"😱\", category=\"emotional\", points=10, tier=1,\n    ),\n    \"nervous_wreck\": Achievement(\n        id=\"nervous_wreck\", \n        name=\"ach_nervous_wreck_name\",\n        description=\"ach_nervous_wreck_desc\",\n        icon=\"😰\", category=\"emotional\", points=15, tier=2, hidden=True,\n    ),\n\n    # =========================================================================\n    # SECRET CATEGORY (3)\n    # =========================================================================\n    \"night_owl\": Achievement(\n        id=\"night_owl\", \n        name=\"ach_night_owl_name\",\n        description=\"ach_night_owl_desc\",\n        icon=\"🦉\", category=\"secret\", points=15, tier=2, hidden=True,\n    ),\n    \"early_bird\": Achievement(\n        id=\"early_bird\", \n        name=\"ach_early_bird_name\",\n        description=\"ach_early_bird_desc\",\n        icon=\"🐦\", category=\"secret\", points=15, tier=2, hidden=True,\n    ),\n    \"weekend_warrior\": Achievement(\n        id=\"weekend_warrior\", \n        name=\"ach_weekend_warrior_name\",\n        description=\"ach_weekend_warrior_desc\",\n        icon=\"🗓️\", category=\"secret\", points=20, tier=2, hidden=True,\n    ),\n\n    # =========================================================================\n    # META CATEGORY (2)\n    # =========================================================================\n    \"brain_surgeon\": Achievement(\n        id=\"brain_surgeon\", \n        name=\"ach_brain_surgeon_name\",\n        description=\"ach_brain_surgeon_desc\",\n        icon=\"🔬\", category=\"meta\", points=10, tier=1,\n    ),\n    \"speed_demon\": Achievement(\n        id=\"speed_demon\", \n        name=\"ach_speed_demon_name\",\n        description=\"ach_speed_demon_desc\",\n        icon=\"⏩\", category=\"meta\", points=15, tier=2,\n    ),\n    \"completionist\": Achievement(\n        id=\"completionist\", \n        name=\"ach_completionist_name\",\n        description=\"ach_completionist_desc\",\n        icon=\"🏆\", category=\"meta\", points=100, tier=4, hidden=True,\n    ),\n}\n\n\n# =============================================================================\n# HELPER FUNCTIONS\n# =============================================================================\n\ndef get_achievement(achievement_id: str) -> Achievement | None:\n    \"\"\"Get an achievement by ID\"\"\"\n    return ACHIEVEMENT_DEFINITIONS.get(achievement_id)\n\n\ndef get_achievements_by_category(category: str) -> Dict[str, Achievement]:\n    \"\"\"Get all achievements in a specific category\"\"\"\n    return {\n        aid: ach for aid, ach in ACHIEVEMENT_DEFINITIONS.items()\n        if ach.category == category\n    }\n\n\ndef get_visible_achievements() -> Dict[str, Achievement]:\n    \"\"\"Get all non-hidden achievements\"\"\"\n    return {\n        aid: ach for aid, ach in ACHIEVEMENT_DEFINITIONS.items()\n        if not ach.hidden\n    }\n\n\ndef get_total_points() -> int:\n    \"\"\"Get total possible points from all achievements\"\"\"\n    return sum(ach.points for ach in ACHIEVEMENT_DEFINITIONS.values())\n\n\ndef get_achievement_count() -> int:\n    \"\"\"Get total number of achievements\"\"\"\n    return len(ACHIEVEMENT_DEFINITIONS)"
  },
  {
    "path": "plugins/achievements/display_scaling.py",
    "content": "\"\"\"\r\nPortable copy of DisplayScaling plugins.\r\nKeep this file in the same folder as main.py\r\n\"\"\"\r\n\r\nimport re\r\n\r\nclass DisplayScaling:\r\n    \"\"\"\r\n    Utility class that computes a single scale-factor from the current\r\n    screen resolution versus the design resolution (2880 × 1920).\r\n    All UI measurements are then multiplied by this factor.\r\n    \"\"\"\r\n\r\n    DESIGN_WIDTH  = 2880\r\n    DESIGN_HEIGHT = 1920\r\n    _scale_factor = 1.0\r\n\r\n    @classmethod\r\n    def initialize(cls, current_width: int, current_height: int) -> None:\r\n        \"\"\"Call once after we know the real screen size.\"\"\"\r\n        width_ratio  = current_width  / cls.DESIGN_WIDTH\r\n        height_ratio = current_height / cls.DESIGN_HEIGHT\r\n        base_scale   = min(width_ratio, height_ratio)\r\n\r\n        # Slightly smaller UI on 1080p screens\r\n        if current_width <= 1920 and current_height <= 1080:\r\n            cls._scale_factor = base_scale * 0.85\r\n        else:\r\n            cls._scale_factor = base_scale\r\n\r\n    @classmethod\r\n    def scale(cls, value: int | float) -> int:\r\n        \"\"\"Scale an integer or float pixel value.\"\"\"\r\n        return int(value * cls._scale_factor)\r\n\r\n    @classmethod\r\n    def font_size(cls, size: int) -> int:\r\n        \"\"\"Return a font size guaranteed to be at least 8 pt.\"\"\"\r\n        return max(8, cls.scale(size))\r\n\r\n    @classmethod\r\n    def get_scale_factor(cls) -> float:\r\n        return cls._scale_factor\r\n\r\n    @classmethod\r\n    def scale_css(cls, css_string: str) -> str:\r\n        \"\"\"Replace font-size:Xpx with scaled value inside a CSS snippet.\"\"\"\r\n        pattern = r'font-size:\\s*(\\d+)px'\r\n        def _repl(m: re.Match) -> str:\r\n            return f\"font-size:{cls.font_size(int(m.group(1)))}px\"\r\n        return re.sub(pattern, _repl, css_string)"
  },
  {
    "path": "plugins/achievements/main.py",
    "content": "import os\nimport json\nimport time\nimport logging\nfrom datetime import datetime\nfrom typing import Dict, List, Any, Optional\nfrom pathlib import Path\n\nfrom PyQt5 import QtCore, QtWidgets, QtGui\n\n# Import the local copy of DisplayScaling\nfrom .display_scaling import DisplayScaling as _DS\n\n# Increase the *nominal* sizes by ~1.4× (tweak multiplier to taste)\n_FONT_BOOST = 1.4\n\ndef _scale_size(pt: int) -> int:\n    \"\"\"Return a bigger base size before DisplayScaling does its job.\"\"\"\n    return int(pt * _FONT_BOOST)\n\n# Monkey-patch the local DisplayScaling.font_size so every caller\n# automatically gets the boosted value.\n_DS.font_size = lambda pt: max(8, _DS.scale(_scale_size(pt)))\n# Re-export the (now patched) class under its original name so UI code can see it\nDisplayScaling = _DS\n\n# Import achievement definitions from separate file\nfrom .achievements_data import (\n    Achievement,\n    UnlockedAchievement,\n    AchievementCategory,\n    ACHIEVEMENT_DEFINITIONS,\n    TIER_COLORS,\n    get_achievement,\n)\n\n# Import localisation\nfrom src import localisation \n\n# Assign the core translation function 't' from the imported module\n_t = localisation.loc\n\n# =============================================================================\n# PLUGIN METADATA - Required by PluginManager\n# =============================================================================\n\nPLUGIN_NAME = \"achievements\"\nPLUGIN_VERSION = \"2.1.0\" # Version bump for localisation support\nPLUGIN_AUTHOR = \"ViciousSquid\"\nPLUGIN_DESCRIPTION = \"Track milestones and unlock achievements as your squid grows\"\nPLUGIN_REQUIRES = []  # No dependencies\n\n# Default language setting (can be changed via main menu or config)\nLANGUAGE = \"en\"  # Options: \"en\", \"es\", \"fr\"\nlocalisation.CURRENT_LANGUAGE = LANGUAGE\n\n\n# =============================================================================\n# UI COMPONENTS\n# =============================================================================\n\nclass AchievementNotification(QtWidgets.QWidget):\n    \"\"\"Toast notification for achievement unlocks - LARGER VERSION with description\"\"\"\n\n    def __init__(self, achievement: Achievement, parent=None):\n        super().__init__(parent)\n        self.achievement = achievement\n        self._setup_ui()\n        self._setup_animation()\n\n    def _setup_ui(self):\n        self.setWindowFlags(\n            QtCore.Qt.FramelessWindowHint |\n            QtCore.Qt.WindowStaysOnTopHint |\n            QtCore.Qt.Tool\n        )\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        \n        # LARGER toast to fit description - increased height significantly\n        self.setFixedSize(DisplayScaling.scale(520), DisplayScaling.scale(160))\n\n        container = QtWidgets.QFrame(self)\n        container.setFixedSize(DisplayScaling.scale(490), DisplayScaling.scale(145))\n        container.move(DisplayScaling.scale(15), DisplayScaling.scale(7))\n\n        # Simple dark rectangle background\n        tier_color = TIER_COLORS.get(self.achievement.tier, \"#CD7F32\")\n        container.setStyleSheet(\"\"\"\n            QFrame {\n                background: rgb(30, 30, 35);\n                border: none;\n                border-radius: 8px;\n            }\n        \"\"\")\n\n        layout = QtWidgets.QHBoxLayout(container)\n        layout.setContentsMargins(\n            DisplayScaling.scale(16),\n            DisplayScaling.scale(12),\n            DisplayScaling.scale(16),\n            DisplayScaling.scale(12)\n        )\n        layout.setSpacing(DisplayScaling.scale(14))\n\n        # Icon - larger\n        icon_label = QtWidgets.QLabel(self.achievement.icon)\n        icon_label.setStyleSheet(f\"\"\"\n            font-size: {DisplayScaling.font_size(52)}px; \n            background: transparent; \n            color: white;\n        \"\"\")\n        icon_label.setFixedWidth(DisplayScaling.scale(80))\n        icon_label.setAlignment(QtCore.Qt.AlignCenter)\n        layout.addWidget(icon_label)\n\n        # Text section\n        text_layout = QtWidgets.QVBoxLayout()\n        text_layout.setSpacing(DisplayScaling.scale(4))\n\n        # Header \"Achievement Unlocked!\"\n        header = QtWidgets.QLabel(_t(\"ui_achievement_unlocked\"))\n        header.setStyleSheet(f\"\"\"\n            color: {tier_color}; \n            font-size: {DisplayScaling.font_size(14)}px; \n            font-weight: bold; \n            background: transparent;\n        \"\"\")\n        text_layout.addWidget(header)\n\n        # Achievement name - Translated\n        name_label = QtWidgets.QLabel(_t(self.achievement.name))\n        name_label.setStyleSheet(f\"\"\"\n            color: white; \n            font-size: {DisplayScaling.font_size(22)}px; \n            font-weight: bold; \n            background: transparent;\n        \"\"\")\n        text_layout.addWidget(name_label)\n\n        # Achievement DESCRIPTION - Translated\n        desc_label = QtWidgets.QLabel(_t(self.achievement.description))\n        desc_label.setStyleSheet(f\"\"\"\n            color: #cccccc; \n            font-size: {DisplayScaling.font_size(13)}px; \n            background: transparent;\n        \"\"\")\n        desc_label.setWordWrap(True)\n        desc_label.setMaximumWidth(DisplayScaling.scale(320))\n        text_layout.addWidget(desc_label)\n\n        # Points earned\n        points_label = QtWidgets.QLabel(f\"+{self.achievement.points} {_t('ui_points_gained')}\")\n        points_label.setStyleSheet(f\"\"\"\n            color: {tier_color}; \n            font-size: {DisplayScaling.font_size(12)}px; \n            font-weight: bold;\n            background: transparent;\n        \"\"\")\n        text_layout.addWidget(points_label)\n\n        layout.addLayout(text_layout)\n        layout.addStretch()\n\n    def _setup_animation(self):\n        self.opacity_effect = QtWidgets.QGraphicsOpacityEffect(self)\n        self.setGraphicsEffect(self.opacity_effect)\n\n        self.fade_in = QtCore.QPropertyAnimation(self.opacity_effect, b\"opacity\")\n        self.fade_in.setDuration(300)\n        self.fade_in.setStartValue(0)\n        self.fade_in.setEndValue(1)\n\n        self.fade_out = QtCore.QPropertyAnimation(self.opacity_effect, b\"opacity\")\n        self.fade_out.setDuration(500)\n        self.fade_out.setStartValue(1)\n        self.fade_out.setEndValue(0)\n        self.fade_out.finished.connect(self.close)\n\n        self.display_timer = QtCore.QTimer(self)\n        self.display_timer.setSingleShot(True)\n        self.display_timer.timeout.connect(self.fade_out.start)\n\n    def show_notification(self, duration_ms=4000):\n        self.show()\n        self.fade_in.start()\n        self.display_timer.start(duration_ms)\n\n\nclass AchievementsWindow(QtWidgets.QDialog):\n    \"\"\"Window displaying all achievements\"\"\"\n\n    def __init__(self, plugin: 'AchievementsPlugin', parent=None):\n        super().__init__(parent)\n        self.plugin = plugin\n        self.setWindowTitle(f\"🏆 {PLUGIN_NAME}\")\n        self.setMinimumSize(DisplayScaling.scale(550), DisplayScaling.scale(650))\n        self._setup_ui()\n\n    def _setup_ui(self):\n        layout = QtWidgets.QVBoxLayout(self)\n\n        # Header\n        header = QtWidgets.QFrame()\n        header.setStyleSheet(\"\"\"\n            QFrame {\n                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,\n                    stop:0 #2c3e50, stop:1 #3498db);\n                border-radius: 8px; padding: 10px;\n            }\n        \"\"\")\n        header_layout = QtWidgets.QHBoxLayout(header)\n\n        total_points = self.plugin.get_total_points()\n        total_unlocked = len(self.plugin.unlocked)\n        total_available = len([a for a in ACHIEVEMENT_DEFINITIONS.values() if not a.hidden])\n\n        points_label = QtWidgets.QLabel(f\"🏆 {total_points} {_t('ui_points')}\")\n        points_label.setStyleSheet(f\"color: gold; \"\n                                   f\"font-size: {DisplayScaling.font_size(18)}px; \"\n                                   f\"font-weight: bold;\")\n        header_layout.addWidget(points_label)\n        header_layout.addStretch()\n\n        progress_label = QtWidgets.QLabel(f\"📊 {total_unlocked}/{total_available} {_t('ui_unlocked')}\")\n        progress_label.setStyleSheet(f\"color: white; \"\n                                     f\"font-size: {DisplayScaling.font_size(14)}px;\")\n        header_layout.addWidget(progress_label)\n\n        layout.addWidget(header)\n\n        # Tabs\n        tabs = QtWidgets.QTabWidget()\n        tabs.addTab(self._create_list(None), _t(\"ui_all\"))\n        for cat in AchievementCategory:\n            cat_achievements = [a for a in ACHIEVEMENT_DEFINITIONS.values() if a.category == cat.value]\n            if cat_achievements:  # Only add tab if category has achievements\n                # Use translated category name\n                tabs.addTab(self._create_list(cat.value), _t(f\"cat_{cat.value}\"))\n        layout.addWidget(tabs)\n\n    def _create_list(self, category_filter: Optional[str]) -> QtWidgets.QScrollArea:\n        scroll = QtWidgets.QScrollArea()\n        scroll.setWidgetResizable(True)\n        container = QtWidgets.QWidget()\n        layout = QtWidgets.QVBoxLayout(container)\n        layout.setSpacing(DisplayScaling.scale(8))\n\n        unlocked_ids = set(self.plugin.unlocked.keys())\n\n        for ach_id, ach in ACHIEVEMENT_DEFINITIONS.items():\n            if category_filter and ach.category != category_filter:\n                continue\n            is_unlocked = ach_id in unlocked_ids\n            if ach.hidden and not is_unlocked:\n                continue\n\n            card = self._create_card(ach, is_unlocked, self.plugin.progress.get(ach_id, 0))\n            layout.addWidget(card)\n\n        layout.addStretch()\n        scroll.setWidget(container)\n        return scroll\n\n    def _create_card(self, ach: Achievement, unlocked: bool, progress: int) -> QtWidgets.QFrame:\n        card = QtWidgets.QFrame()\n        \n        # 🌟 NEW Lighter Colors 🌟\n        # Use the tier color for unlocked, or a light gray for locked.\n        border_color = TIER_COLORS.get(ach.tier, \"#CD7F32\") if unlocked else \"#AAA\"\n        # Use a very light, semi-transparent background for both.\n        bg = \"rgba(255, 255, 255, 230)\" if unlocked else \"rgba(230, 230, 235, 200)\"\n\n        card.setStyleSheet(f\"QFrame {{ background: {bg}; border: 2px solid {border_color}; border-radius: 8px; }}\")\n\n        layout = QtWidgets.QHBoxLayout(card)\n\n        # Icon styling - remains largely the same, but will appear clearer on the lighter background.\n        icon = QtWidgets.QLabel(ach.icon if unlocked else \"🔒\")\n        icon.setStyleSheet(f\"font-size: {DisplayScaling.font_size(28)}px;\")\n        icon.setFixedWidth(DisplayScaling.scale(50))\n        layout.addWidget(icon)\n\n        info = QtWidgets.QVBoxLayout()\n        \n        # 🌟 NEW Name Text Color 🌟\n        # Black for unlocked, Dark Gray for locked (easier to read on light BG)\n        name_color = '#000' if unlocked else '#555' \n        \n        # Translate the name key\n        display_name = _t(ach.name) if unlocked or not ach.hidden else \"???\"\n        \n        name = QtWidgets.QLabel(display_name)\n        name.setStyleSheet(f\"color: {name_color}; \"\n                        f\"font-size: {DisplayScaling.font_size(14)}px; \"\n                        f\"font-weight: bold;\")\n        info.addWidget(name)\n\n        # 🌟 NEW Description Text Color 🌟\n        # Dark Gray for unlocked, Medium Gray for locked (easier to read on light BG)\n        desc_color = '#333' if unlocked else '#777'\n        \n        # Translate the description key\n        display_desc = _t(ach.description) if unlocked or not ach.hidden else _t(\"ui_hidden\")\n        \n        desc = QtWidgets.QLabel(display_desc)\n        desc.setStyleSheet(f\"color: {desc_color}; \"\n                        f\"font-size: {DisplayScaling.font_size(11)}px;\")\n        desc.setWordWrap(True)\n        info.addWidget(desc)\n\n        # Progress bar logic remains the same\n        if ach.target_count > 1 and not unlocked:\n            pbar = QtWidgets.QProgressBar()\n            pbar.setMaximum(ach.target_count)\n            pbar.setValue(min(progress, ach.target_count))\n            pbar.setFormat(f\"{progress}/{ach.target_count}\")\n            pbar.setFixedHeight(DisplayScaling.scale(16))\n            info.addWidget(pbar)\n\n        layout.addLayout(info, 1)\n\n        # Points color remains the tier color, which will stand out well.\n        pts = QtWidgets.QLabel(f\"+{ach.points}\")\n        pts.setStyleSheet(f\"color: {TIER_COLORS.get(ach.tier, '#CD7F32')}; \"\n                        f\"font-size: {DisplayScaling.font_size(12)}px; \"\n                        f\"font-weight: bold;\")\n        layout.addWidget(pts)\n\n        return card\n\n\n# =============================================================================\n# MAIN PLUGIN CLASS\n# =============================================================================\n\nclass AchievementsPlugin:\n    \"\"\"Main achievements plugin class\"\"\"\n\n    def __init__(self):\n        self.logger = None\n        self.plugin_manager = None\n        self.tamagotchi_logic = None\n        self.squid = None\n\n        self.unlocked: Dict[str, UnlockedAchievement] = {}\n        self.progress: Dict[str, int] = {}\n        self.statistics: Dict[str, int] = {}\n\n        # Timers\n        self.age_check_timer: Optional[QtCore.QTimer] = None\n        self.stat_check_timer: Optional[QtCore.QTimer] = None\n        self.notification_timer: Optional[QtCore.QTimer] = None\n        self.notification_queue: List[Achievement] = []\n        self.current_notification: Optional[AchievementNotification] = None\n\n        # Tracking for timed achievements\n        self.cleanliness_high_since: Optional[float] = None  # For germaphobe\n        self.anxiety_low_since: Optional[float] = None  # For zen_master\n        self.max_speed_since: Optional[float] = None  # For speed_demon\n        self.health_was_critical: bool = False  # For comeback_kid\n        self.weekend_saturday: bool = False  # For weekend_warrior\n        self.weekend_sunday: bool = False\n\n        self.parent_window: Optional[QtWidgets.QMainWindow] = None\n        self.is_setup = False\n        self.debug_mode = False\n        self._original_methods: Dict[str, Any] = {}\n\n    # ----------------------------------------------------------\n    #  Write unlock to text file only\n    # ----------------------------------------------------------\n    def _log_unlock_to_text_file(self, ach: Achievement) -> None:\n        \"\"\"Append 'ID | long-date | HHMMSS | name' to achievements_log.txt\"\"\"\n        try:\n            log_path = Path(self._get_save_path()).with_name(\"achievements_log.txt\")\n            time_stamp = datetime.now().strftime(\"%A, %B %d, %Y @ %H%M%S\")\n            # Translate name for log file using current language\n            translated_name = _t(ach.name)\n            line = f\"{ach.id} | {time_stamp} | {translated_name}\\n\"\n            with log_path.open(\"a\", encoding=\"utf-8\") as fh:\n                fh.write(line)\n        except Exception as e:\n            if self.logger:\n                self.logger.warning(f\"Could not write achievement log: {e}\")\n\n    def setup(self, plugin_manager, tamagotchi_logic) -> bool:\n        \"\"\"Called by PluginManager when enabling the plugin\"\"\"\n        self.plugin_manager = plugin_manager\n        self.tamagotchi_logic = tamagotchi_logic\n\n        # Setup logger\n        if hasattr(plugin_manager, 'logger'):\n            self.logger = plugin_manager.logger.getChild(PLUGIN_NAME)\n        else:\n            self.logger = logging.getLogger(f\"{PLUGIN_NAME}_Plugin\")\n            if not self.logger.handlers:\n                handler = logging.StreamHandler()\n                handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))\n                self.logger.addHandler(handler)\n            self.logger.setLevel(logging.INFO)\n\n        self.logger.info(f\"Setting up {PLUGIN_NAME}...\")\n\n        # Get squid reference\n        if tamagotchi_logic and hasattr(tamagotchi_logic, 'squid'):\n            self.squid = tamagotchi_logic.squid\n            self.logger.info(f\"Got squid reference: {self.squid}\")\n\n        # Get parent window\n        if tamagotchi_logic and hasattr(tamagotchi_logic, 'user_interface'):\n            ui = tamagotchi_logic.user_interface\n            if hasattr(ui, 'window'):\n                self.parent_window = ui.window\n            elif isinstance(ui, QtWidgets.QMainWindow):\n                self.parent_window = ui\n\n        self.debug_mode = getattr(tamagotchi_logic, 'debug_mode', False)\n\n        # Setup timers\n        self._setup_timers()\n\n        # Subscribe to plugin manager hooks\n        self._subscribe_to_hooks()\n\n        # Also try direct method hooks as backup\n        self._install_hooks()\n\n        self.is_setup = True\n        self.logger.info(f\"{PLUGIN_NAME} setup complete. {len(self.unlocked)} achievements loaded.\")\n        return True\n\n    def _subscribe_to_hooks(self):\n        \"\"\"Subscribe to plugin manager hooks for event tracking\"\"\"\n        if not self.plugin_manager:\n            if self.logger:\n                self.logger.warning(\"No plugin_manager, cannot subscribe to hooks\")\n            return\n        \n        hook_subscriptions = [\n            # Original hooks\n            (\"on_feed\", self._hook_on_feed),\n            (\"on_wake\", self._hook_on_wake),\n            (\"on_sleep\", self._hook_on_sleep),\n            (\"on_neurogenesis\", self._hook_on_neurogenesis),\n            # New hooks for expanded achievements\n            (\"on_clean\", self._hook_on_clean),\n            (\"on_medicine\", self._hook_on_medicine),\n            (\"on_rock_pickup\", self._hook_on_rock_pickup),\n            (\"on_rock_throw\", self._hook_on_rock_throw),\n            (\"on_decoration_interaction\", self._hook_on_decoration_interaction),\n            (\"on_ink_cloud\", self._hook_on_ink_cloud),\n            (\"on_startle\", self._hook_on_startle),\n            (\"on_memory_created\", self._hook_on_memory_created),\n            (\"on_memory_to_long_term\", self._hook_on_memory_to_long_term),\n            (\"on_curiosity_change\", self._hook_on_curiosity_change),\n            (\"on_anxiety_change\", self._hook_on_anxiety_change),\n            (\"on_speed_change\", self._hook_on_speed_change),\n        ]\n        \n        for hook_name, callback in hook_subscriptions:\n            try:\n                if hasattr(self.plugin_manager, 'subscribe_to_hook'):\n                    result = self.plugin_manager.subscribe_to_hook(hook_name, PLUGIN_NAME, callback)\n                    if self.logger:\n                        self.logger.debug(f\"Subscribed to hook '{hook_name}': {result}\")\n            except Exception as e:\n                if self.logger:\n                    self.logger.warning(f\"Could not subscribe to hook '{hook_name}': {e}\")\n\n    # ----------------------------------------------------------\n    # Hook Callbacks\n    # ----------------------------------------------------------\n    \n    def _hook_on_feed(self, **kwargs):\n        self.on_squid_fed()\n\n    def _hook_on_wake(self, **kwargs):\n        self.on_squid_woke()\n\n    def _hook_on_sleep(self, **kwargs):\n        pass  # Sleep achievement triggers on wake\n\n    def _hook_on_neurogenesis(self, **kwargs):\n        self.on_neuron_created()\n\n    def _hook_on_clean(self, **kwargs):\n        self.on_tank_cleaned()\n\n    def _hook_on_medicine(self, **kwargs):\n        self.on_medicine_given()\n\n    def _hook_on_rock_pickup(self, **kwargs):\n        self.on_rock_picked_up()\n\n    def _hook_on_rock_throw(self, **kwargs):\n        self.on_rock_thrown()\n\n    def _hook_on_decoration_interaction(self, **kwargs):\n        decoration = kwargs.get('decoration')\n        interaction_type = kwargs.get('type', 'push')\n        self.on_decoration_interacted(decoration, interaction_type)\n\n    def _hook_on_ink_cloud(self, **kwargs):\n        self.on_ink_cloud_released()\n\n    def _hook_on_startle(self, **kwargs):\n        self.on_squid_startled()\n\n    def _hook_on_memory_created(self, **kwargs):\n        self.on_memory_formed()\n\n    def _hook_on_memory_to_long_term(self, **kwargs):\n        self.on_memory_promoted()\n\n    def _hook_on_curiosity_change(self, **kwargs):\n        new_value = kwargs.get('new_value', 0)\n        if new_value >= 100:\n            self.unlock_achievement(\"curiosity_100\")\n\n    def _hook_on_anxiety_change(self, **kwargs):\n        new_value = kwargs.get('new_value', 0)\n        if new_value >= 100:\n            self.unlock_achievement(\"nervous_wreck\")\n        # Track for zen_master\n        if new_value < 10:\n            if self.anxiety_low_since is None:\n                self.anxiety_low_since = time.time()\n        else:\n            self.anxiety_low_since = None\n\n    def _hook_on_speed_change(self, **kwargs):\n        speed = kwargs.get('speed', 1)\n        max_speed = kwargs.get('max_speed', 4)\n        if speed >= max_speed:\n            if self.max_speed_since is None:\n                self.max_speed_since = time.time()\n        else:\n            self.max_speed_since = None\n\n    # ----------------------------------------------------------\n    # Event Handlers\n    # ----------------------------------------------------------\n\n    def on_squid_fed(self):\n        self._increment_stat(\"times_fed\")\n        count = self.statistics.get(\"times_fed\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_feeding\")\n        if count >= 10:\n            self.unlock_achievement(\"fed_10_times\")\n        if count >= 50:\n            self.unlock_achievement(\"fed_50_times\")\n        if count >= 100:\n            self.unlock_achievement(\"fed_100_times\")\n        if count >= 500:\n            self.unlock_achievement(\"fed_500_times\")\n        for aid in [\"fed_10_times\", \"fed_50_times\", \"fed_100_times\", \"fed_500_times\"]:\n            self._update_progress(aid, count)\n\n    def on_squid_woke(self):\n        self._increment_stat(\"times_slept\")\n        count = self.statistics.get(\"times_slept\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_sleep\")\n        if count >= 10:\n            self.unlock_achievement(\"slept_10_times\")\n        self._update_progress(\"slept_10_times\", count)\n\n    def on_neuron_created(self):\n        self._increment_stat(\"neurons_created\")\n        count = self.statistics.get(\"neurons_created\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_neuron\")\n        if count >= 10:\n            self.unlock_achievement(\"neurons_10\")\n        if count >= 50:\n            self.unlock_achievement(\"neurons_50\")\n        if count >= 100:\n            self.unlock_achievement(\"neurons_100\")\n        self._update_progress(\"neurons_10\", count)\n        self._update_progress(\"neurons_50\", count)\n        self._update_progress(\"neurons_100\", count)\n\n    def on_neuron_leveled(self):\n        self._increment_stat(\"neurons_leveled\")\n        if self.statistics.get(\"neurons_leveled\", 0) == 1:\n            self.unlock_achievement(\"first_neuron_levelup\")\n\n    def on_poop_thrown(self):\n        self._increment_stat(\"poops_thrown\")\n        if self.statistics.get(\"poops_thrown\", 0) == 1:\n            self.unlock_achievement(\"first_poop_throw\")\n\n    def on_tank_cleaned(self):\n        self._increment_stat(\"times_cleaned\")\n        count = self.statistics.get(\"times_cleaned\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_clean\")\n        if count >= 25:\n            self.unlock_achievement(\"cleaned_25_times\")\n        self._update_progress(\"cleaned_25_times\", count)\n\n    def on_medicine_given(self):\n        self._increment_stat(\"times_medicated\")\n        count = self.statistics.get(\"times_medicated\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_medicine\")\n        if count >= 10:\n            self.unlock_achievement(\"medicine_10_times\")\n        self._update_progress(\"medicine_10_times\", count)\n        \n        # Check for comeback_kid - track if health was critical before medicine\n        if self.health_was_critical and self.squid:\n            health = getattr(self.squid, 'health', 100)\n            if health >= 100:\n                self.unlock_achievement(\"comeback_kid\")\n                self.health_was_critical = False\n\n    def on_rock_picked_up(self):\n        self._increment_stat(\"rocks_picked\")\n        count = self.statistics.get(\"rocks_picked\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_rock_pickup\")\n        if count >= 10:\n            self.unlock_achievement(\"rocks_picked_10\")\n        if count >= 50:\n            self.unlock_achievement(\"rocks_picked_50\")\n        self._update_progress(\"rocks_picked_10\", count)\n        self._update_progress(\"rocks_picked_50\", count)\n        # Also count as object investigated\n        self._on_object_investigated()\n\n    def on_rock_thrown(self):\n        self._increment_stat(\"rocks_thrown\")\n        count = self.statistics.get(\"rocks_thrown\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_rock_throw\")\n        if count >= 25:\n            self.unlock_achievement(\"rocks_thrown_25\")\n        if count >= 100:\n            self.unlock_achievement(\"rocks_thrown_100\")\n        self._update_progress(\"rocks_thrown_25\", count)\n        self._update_progress(\"rocks_thrown_100\", count)\n\n    def on_decoration_interacted(self, decoration=None, interaction_type='push'):\n        \"\"\"Handle decoration interactions (push, investigate, etc.)\"\"\"\n        # Track push interactions\n        if interaction_type == 'push':\n            self._increment_stat(\"decorations_pushed\")\n            count = self.statistics.get(\"decorations_pushed\", 0)\n            if count == 1:\n                self.unlock_achievement(\"first_decoration_push\")\n            if count >= 10:\n                self.unlock_achievement(\"decorations_pushed_10\")\n            if count >= 50:\n                self.unlock_achievement(\"decorations_pushed_50\")\n            self._update_progress(\"decorations_pushed_10\", count)\n            self._update_progress(\"decorations_pushed_50\", count)\n        \n        # Track plant-specific interactions\n        if decoration and hasattr(decoration, 'category'):\n            if decoration.category == 'plant':\n                self._increment_stat(\"plants_interacted\")\n                count = self.statistics.get(\"plants_interacted\", 0)\n                if count == 1:\n                    self.unlock_achievement(\"first_plant_interact\")\n                if count >= 10:\n                    self.unlock_achievement(\"plants_interacted_10\")\n                if count >= 50:\n                    self.unlock_achievement(\"plants_interacted_50\")\n                self._update_progress(\"plants_interacted_10\", count)\n                self._update_progress(\"plants_interacted_50\", count)\n        \n        # Count as object investigated\n        self._on_object_investigated()\n\n    def _on_object_investigated(self):\n        \"\"\"Track unique object investigations\"\"\"\n        self._increment_stat(\"objects_investigated\")\n        count = self.statistics.get(\"objects_investigated\", 0)\n        if count >= 25:\n            self.unlock_achievement(\"objects_investigated_25\")\n        if count >= 100:\n            self.unlock_achievement(\"objects_investigated_100\")\n        self._update_progress(\"objects_investigated_25\", count)\n        self._update_progress(\"objects_investigated_100\", count)\n\n    def on_ink_cloud_released(self):\n        self._increment_stat(\"ink_clouds\")\n        count = self.statistics.get(\"ink_clouds\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_ink_cloud\")\n        if count >= 20:\n            self.unlock_achievement(\"ink_clouds_20\")\n        self._update_progress(\"ink_clouds_20\", count)\n\n    def on_squid_startled(self):\n        self._increment_stat(\"times_startled\")\n        if self.statistics.get(\"times_startled\", 0) == 1:\n            self.unlock_achievement(\"first_startle\")\n\n    def on_memory_formed(self):\n        self._increment_stat(\"memories_formed\")\n        count = self.statistics.get(\"memories_formed\", 0)\n        if count == 1:\n            self.unlock_achievement(\"first_memory\")\n        if count >= 50:\n            self.unlock_achievement(\"memories_50\")\n        self._update_progress(\"memories_50\", count)\n\n    def on_memory_promoted(self):\n        self._increment_stat(\"memories_promoted\")\n        if self.statistics.get(\"memories_promoted\", 0) == 1:\n            self.unlock_achievement(\"memory_long_term\")\n\n    def on_brain_tool_opened(self):\n        \"\"\"Called when brain visualization is opened\"\"\"\n        if \"brain_surgeon\" not in self.unlocked:\n            self.unlock_achievement(\"brain_surgeon\")\n\n    # ----------------------------------------------------------\n    # Periodic Checks\n    # ----------------------------------------------------------\n\n    def _check_age_achievements(self):\n        if not self.squid:\n            return\n        age_hours = 0\n        if hasattr(self.squid, 'birth_time'):\n            age_hours = (time.time() - self.squid.birth_time) / 3600\n        elif hasattr(self.squid, 'age_hours'):\n            age_hours = self.squid.age_hours\n\n        if age_hours >= 1:\n            self.unlock_achievement(\"age_1_hour\")\n        if age_hours >= 10:\n            self.unlock_achievement(\"age_10_hours\")\n        if age_hours >= 24:\n            self.unlock_achievement(\"age_24_hours\")\n        if age_hours >= 168:  # 1 week\n            self.unlock_achievement(\"age_1_week\")\n        if age_hours >= 720:  # 30 days\n            self.unlock_achievement(\"age_1_month\")\n\n        # Time-of-day achievements\n        hour = datetime.now().hour\n        if 0 <= hour < 4:\n            self.unlock_achievement(\"night_owl\")\n        if 5 <= hour < 7:\n            self.unlock_achievement(\"early_bird\")\n        \n        # Weekend warrior\n        day = datetime.now().weekday()\n        if day == 5:  # Saturday\n            self.weekend_saturday = True\n        elif day == 6:  # Sunday\n            self.weekend_sunday = True\n        if self.weekend_saturday and self.weekend_sunday:\n            self.unlock_achievement(\"weekend_warrior\")\n\n    def _check_stat_achievements(self):\n        if not self.squid:\n            return\n        \n        # Happiness\n        if hasattr(self.squid, 'happiness') and self.squid.happiness >= 100:\n            self.unlock_achievement(\"happiness_100\")\n\n        # All stats high\n        stats = ['happiness', 'hunger', 'energy', 'health']\n        all_high = all(getattr(self.squid, s, 0) >= 80 for s in stats if hasattr(self.squid, s))\n        if all_high:\n            self.unlock_achievement(\"all_stats_high\")\n        \n        # Check health for comeback_kid tracking\n        if hasattr(self.squid, 'health'):\n            if self.squid.health < 20:\n                self.health_was_critical = True\n            elif self.squid.health >= 100 and self.health_was_critical:\n                self.unlock_achievement(\"comeback_kid\")\n                self.health_was_critical = False\n        \n        # Cleanliness tracking for germaphobe\n        if hasattr(self.squid, 'cleanliness'):\n            if self.squid.cleanliness >= 90:\n                if self.cleanliness_high_since is None:\n                    self.cleanliness_high_since = time.time()\n                elif time.time() - self.cleanliness_high_since >= 3600:  # 1 hour\n                    self.unlock_achievement(\"germaphobe\")\n            else:\n                self.cleanliness_high_since = None\n        \n        # Anxiety tracking for zen_master\n        if hasattr(self.squid, 'anxiety'):\n            if self.squid.anxiety < 10:\n                if self.anxiety_low_since is None:\n                    self.anxiety_low_since = time.time()\n                elif time.time() - self.anxiety_low_since >= 1800:  # 30 minutes\n                    self.unlock_achievement(\"zen_master\")\n            else:\n                self.anxiety_low_since = None\n        \n        # Speed demon check\n        if self.max_speed_since is not None:\n            if time.time() - self.max_speed_since >= 600:  # 10 minutes\n                self.unlock_achievement(\"speed_demon\")\n        \n        # Completionist check\n        unlocked_count = len(self.unlocked)\n        if \"completionist\" not in self.unlocked and unlocked_count >= 30:\n            self.unlock_achievement(\"completionist\")\n\n    # ----------------------------------------------------------\n    # Core Methods\n    # ----------------------------------------------------------\n\n    def unlock_achievement(self, achievement_id: str, silent: bool = False) -> bool:\n        if achievement_id in self.unlocked:\n            return False\n        if achievement_id not in ACHIEVEMENT_DEFINITIONS:\n            return False\n\n        ach = ACHIEVEMENT_DEFINITIONS[achievement_id]\n        if ach.target_count > 1 and self.progress.get(achievement_id, 0) < ach.target_count:\n            return False\n\n        self.unlocked[achievement_id] = UnlockedAchievement(\n            achievement_id=achievement_id,\n            unlocked_at=datetime.now().isoformat(),\n            progress=ach.target_count,\n            notified=silent\n        )\n\n        # Log with translated name for console/logger (optional, or keep English)\n        if self.logger:\n            # Using English key here might be safer for logs, or translated:\n            self.logger.info(f\"🏆 Unlocked: {_t(ach.name)}\")\n\n        self._log_unlock_to_text_file(ach)\n\n        if not silent:\n            self._queue_notification(ach)\n\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'show_message'):\n            self.tamagotchi_logic.show_message(f\"🏆\")\n\n        return True\n\n    def manually_trigger(self, event_name: str):\n        handlers = {\n            \"fed\": self.on_squid_fed,\n            \"woke\": self.on_squid_woke,\n            \"neuron_created\": self.on_neuron_created,\n            \"neuron_leveled\": self.on_neuron_leveled,\n            \"poop_thrown\": self.on_poop_thrown,\n            \"cleaned\": self.on_tank_cleaned,\n            \"medicine\": self.on_medicine_given,\n            \"rock_pickup\": self.on_rock_picked_up,\n            \"rock_throw\": self.on_rock_thrown,\n            \"ink_cloud\": self.on_ink_cloud_released,\n            \"startle\": self.on_squid_startled,\n            \"memory_formed\": self.on_memory_formed,\n            \"memory_promoted\": self.on_memory_promoted,\n            \"brain_opened\": self.on_brain_tool_opened,\n            \"neuron_max_level\": lambda: self.unlock_achievement(\"neuron_max_level\"),\n            \"dream_state\": lambda: self.unlock_achievement(\"dream_state\"),\n        }\n        if event_name in handlers:\n            handlers[event_name]()\n        else:\n            self.unlock_achievement(event_name)\n\n    def get_total_points(self) -> int:\n        return sum(\n            ACHIEVEMENT_DEFINITIONS[a.achievement_id].points\n            for a in self.unlocked.values()\n            if a.achievement_id in ACHIEVEMENT_DEFINITIONS\n        )\n\n    def _increment_stat(self, name: str, amount: int = 1):\n        self.statistics[name] = self.statistics.get(name, 0) + amount\n\n    def _update_progress(self, achievement_id: str, value: int):\n        self.progress[achievement_id] = value\n\n    # ----------------------------------------------------------\n    # Notifications\n    # ----------------------------------------------------------\n\n    def _queue_notification(self, achievement: Achievement):\n        self.notification_queue.append(achievement)\n        if not self.notification_timer.isActive():\n            self._show_next_notification()\n\n    def _show_next_notification(self):\n        if self.current_notification:\n            self.current_notification.close()\n            self.current_notification = None\n\n        if not self.notification_queue:\n            self.notification_timer.stop()\n            return\n\n        ach = self.notification_queue.pop(0)\n        self.current_notification = AchievementNotification(ach, self.parent_window)\n\n        if self.parent_window:\n            geo = self.parent_window.geometry()\n            x = geo.x() + DisplayScaling.scale(20)\n            y = geo.y() + DisplayScaling.scale(20)\n            self.current_notification.move(x, y)\n\n        self.current_notification.show_notification()\n\n        if self.notification_queue:\n            self.notification_timer.start(4500)  # Slightly longer for reading description\n\n    # ----------------------------------------------------------\n    # Timers & Hooks Setup\n    # ----------------------------------------------------------\n\n    def _setup_timers(self):\n        self.age_check_timer = QtCore.QTimer()\n        self.age_check_timer.timeout.connect(self._check_age_achievements)\n\n        self.stat_check_timer = QtCore.QTimer()\n        self.stat_check_timer.timeout.connect(self._check_stat_achievements)\n\n        self.notification_timer = QtCore.QTimer()\n        self.notification_timer.timeout.connect(self._show_next_notification)\n\n    def _install_hooks(self):\n        \"\"\"Hook into game events via method wrapping\"\"\"\n        if self.logger:\n            self.logger.info(f\"Installing hooks... squid={self.squid is not None}\")\n        \n        if not self.squid:\n            if self.logger:\n                self.logger.warning(\"Cannot install hooks: squid is None\")\n            return\n\n        hooks_installed = []\n        \n        try:\n            # Hook squid.eat\n            if hasattr(self.squid, 'eat') and 'eat' not in self._original_methods:\n                self._original_methods['eat'] = self.squid.eat\n                original_eat = self._original_methods['eat']\n                plugin_self = self\n                \n                def hooked_eat(*args, **kwargs):\n                    result = original_eat(*args, **kwargs)\n                    try:\n                        plugin_self.on_squid_fed()\n                    except Exception as e:\n                        if plugin_self.logger:\n                            plugin_self.logger.error(f\"Error in on_squid_fed: {e}\")\n                    return result\n                \n                self.squid.eat = hooked_eat\n                hooks_installed.append('eat')\n\n            # Hook squid.wake_up\n            if hasattr(self.squid, 'wake_up') and 'wake_up' not in self._original_methods:\n                self._original_methods['wake_up'] = self.squid.wake_up\n                original_wake = self._original_methods['wake_up']\n                plugin_self = self\n                \n                def hooked_wake(*args, **kwargs):\n                    result = original_wake(*args, **kwargs)\n                    try:\n                        plugin_self.on_squid_woke()\n                    except Exception as e:\n                        if plugin_self.logger:\n                            plugin_self.logger.error(f\"Error in on_squid_woke: {e}\")\n                    return result\n                \n                self.squid.wake_up = hooked_wake\n                hooks_installed.append('wake_up')\n\n            # Hook neurogenesis\n            if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'neurogenesis_system'):\n                ng = self.tamagotchi_logic.neurogenesis_system\n                if ng and hasattr(ng, 'create_neuron') and 'create_neuron' not in self._original_methods:\n                    self._original_methods['create_neuron'] = ng.create_neuron\n                    original_create = self._original_methods['create_neuron']\n                    plugin_self = self\n                    \n                    def hooked_create(*args, **kwargs):\n                        result = original_create(*args, **kwargs)\n                        if result:\n                            try:\n                                plugin_self.on_neuron_created()\n                            except Exception as e:\n                                if plugin_self.logger:\n                                    plugin_self.logger.error(f\"Error in on_neuron_created: {e}\")\n                        return result\n                    \n                    ng.create_neuron = hooked_create\n                    hooks_installed.append('create_neuron')\n\n            # Hook poop throwing\n            if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'poop_manager'):\n                pm = self.tamagotchi_logic.poop_manager\n                if pm and hasattr(pm, 'throw_poop') and 'throw_poop' not in self._original_methods:\n                    self._original_methods['throw_poop'] = pm.throw_poop\n                    original_throw = self._original_methods['throw_poop']\n                    plugin_self = self\n                    \n                    def hooked_throw(*args, **kwargs):\n                        result = original_throw(*args, **kwargs)\n                        if result:\n                            try:\n                                plugin_self.on_poop_thrown()\n                            except Exception as e:\n                                if plugin_self.logger:\n                                    plugin_self.logger.error(f\"Error in on_poop_thrown: {e}\")\n                        return result\n                    \n                    pm.throw_poop = hooked_throw\n                    hooks_installed.append('throw_poop')\n\n            # Hook push_decoration on squid\n            if hasattr(self.squid, 'push_decoration') and 'push_decoration' not in self._original_methods:\n                self._original_methods['push_decoration'] = self.squid.push_decoration\n                original_push = self._original_methods['push_decoration']\n                plugin_self = self\n                \n                def hooked_push(decoration, direction):\n                    result = original_push(decoration, direction)\n                    try:\n                        plugin_self.on_decoration_interacted(decoration, 'push')\n                    except Exception as e:\n                        if plugin_self.logger:\n                            plugin_self.logger.error(f\"Error in on_decoration_interacted: {e}\")\n                    return result\n                \n                self.squid.push_decoration = hooked_push\n                hooks_installed.append('push_decoration')\n\n            if self.logger:\n                self.logger.info(f\"Hooks installed: {hooks_installed}\")\n\n        except Exception as e:\n            if self.logger:\n                self.logger.error(f\"Error installing hooks: {e}\", exc_info=True)\n\n    def _uninstall_hooks(self):\n        try:\n            if 'eat' in self._original_methods and self.squid:\n                self.squid.eat = self._original_methods['eat']\n            if 'wake_up' in self._original_methods and self.squid:\n                self.squid.wake_up = self._original_methods['wake_up']\n            if 'push_decoration' in self._original_methods and self.squid:\n                self.squid.push_decoration = self._original_methods['push_decoration']\n        except:\n            pass\n        self._original_methods.clear()\n\n    # ----------------------------------------------------------\n    # Enable / Disable / Shutdown\n    # ----------------------------------------------------------\n\n    def enable(self) -> bool:\n        if self.logger:\n            self.logger.info(f\"{PLUGIN_NAME} enable() called. is_setup={self.is_setup}\")\n        \n        if not self.age_check_timer:\n            self._setup_timers()\n        \n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'squid'):\n            if self.squid != self.tamagotchi_logic.squid:\n                self.squid = self.tamagotchi_logic.squid\n                self._install_hooks()\n                if self.logger:\n                    self.logger.info(f\"Re-acquired squid reference and reinstalled hooks\")\n\n        if self.age_check_timer and not self.age_check_timer.isActive():\n            self.age_check_timer.start(60000)\n                \n        if self.stat_check_timer and not self.stat_check_timer.isActive():\n            self.stat_check_timer.start(5000)\n\n        if self.logger:\n            self.logger.info(f\"{PLUGIN_NAME} enabled successfully\")\n        return True\n\n    def disable(self):\n        if self.logger:\n            self.logger.info(f\"{PLUGIN_NAME} disable() called\")\n            \n        if self.age_check_timer:\n            self.age_check_timer.stop()\n        if self.stat_check_timer:\n            self.stat_check_timer.stop()\n        if self.notification_timer:\n            self.notification_timer.stop()\n        \n        if self.logger:\n            self.logger.info(f\"{PLUGIN_NAME} disabled\")\n\n    def shutdown(self):\n        self.disable()\n        self._uninstall_hooks()\n\n    # ----------------------------------------------------------\n    # Persistence\n    # ----------------------------------------------------------\n\n    def _get_save_path(self) -> str:\n        os.makedirs(\"saves\", exist_ok=True)\n        return os.path.join(\"saves\", \"achievements.json\")\n\n    def get_save_data(self) -> dict:\n        return {\n            \"unlocked\": {k: v.to_dict() for k, v in self.unlocked.items()},\n            \"progress\": self.progress,\n            \"statistics\": self.statistics,\n            \"tracking\": {\n                \"weekend_saturday\": self.weekend_saturday,\n                \"weekend_sunday\": self.weekend_sunday,\n            }\n        }\n\n    def load_save_data(self, data: dict):\n        if not data:\n            return\n        for aid, adata in data.get(\"unlocked\", {}).items():\n            self.unlocked[aid] = UnlockedAchievement.from_dict(adata)\n        self.progress = data.get(\"progress\", {})\n        self.statistics = data.get(\"statistics\", {})\n        tracking = data.get(\"tracking\", {})\n        self.weekend_saturday = tracking.get(\"weekend_saturday\", False)\n        self.weekend_sunday = tracking.get(\"weekend_sunday\", False)\n\n    # ----------------------------------------------------------\n    # UI\n    # ----------------------------------------------------------\n\n    def show_achievements_window(self):\n        window = AchievementsWindow(self, self.parent_window)\n        window.exec_()\n\n    def register_menu_actions(self, main_window: QtWidgets.QMainWindow, menu: QtWidgets.QMenu):\n        action = QtWidgets.QAction(f\"🏆 {PLUGIN_NAME}...\", main_window)\n        action.triggered.connect(self.show_achievements_window)\n        menu.addAction(action)\n\n\n# =============================================================================\n# INITIALIZE FUNCTION - Required by PluginManager\n# =============================================================================\n\ndef initialize(plugin_manager) -> bool:\n    plugin_key = PLUGIN_NAME.lower()\n\n    if plugin_key in plugin_manager.plugins:\n        if hasattr(plugin_manager, 'logger'):\n            plugin_manager.logger.warning(f\"{PLUGIN_NAME} is already registered. Skipping re-registration.\")\n        return True  # Already initialized, no error\n\n    try:\n        instance = AchievementsPlugin()\n\n        plugin_manager.plugins[plugin_key] = {\n            'name': plugin_key,\n            'original_name': PLUGIN_NAME,\n            'version': PLUGIN_VERSION,\n            'author': PLUGIN_AUTHOR,\n            'description': PLUGIN_DESCRIPTION,\n            'requires': PLUGIN_REQUIRES,\n            'instance': instance,\n            'is_setup': False,\n        }\n\n        if hasattr(plugin_manager, 'logger'):\n            plugin_manager.logger.info(f\"{PLUGIN_NAME} v{PLUGIN_VERSION} initialized\")\n\n        return True\n\n    except Exception as e:\n        if hasattr(plugin_manager, 'logger'):\n            plugin_manager.logger.error(f\"Failed to initialize {PLUGIN_NAME}: {e}\")\n        return False"
  },
  {
    "path": "plugins/multiplayer/__init__.py",
    "content": ""
  },
  {
    "path": "plugins/multiplayer/main.py",
    "content": "# File: main.py (Plugin Entry Point)\r\n\r\nimport os\r\nimport sys\r\nimport traceback \r\n\r\n# --- Plugin Metadata ---\r\n# These constants describe the plugin to the system and users.\r\nPLUGIN_NAME = \"multiplayer\"\r\nPLUGIN_VERSION = \"1.2.0\"\r\nPLUGIN_AUTHOR = \"ViciousSquid\"\r\nPLUGIN_DESCRIPTION = \"Enables network sync for squids and objects (Experimental)\"\r\nPLUGIN_REQUIRES = [] # Names of other plugins this one depends on\r\n\r\n# --- Python Path Setup ---\r\n# Adjust these paths if your project structure is different.\r\n# This setup helps Python find your 'src' directory and other plugin modules.\r\ntry:\r\n    current_file_dir = os.path.dirname(os.path.abspath(__file__))\r\n    # Assuming 'main.py' is in 'project_root/plugins/your_plugin_name/'\r\n    project_root_candidate_one_up = os.path.abspath(os.path.join(current_file_dir, '..', '..')) # plugins folder is one up, project root is two up\r\n    project_root_candidate_two_up = os.path.abspath(os.path.join(current_file_dir, '..', '..', '..')) # If nested deeper\r\n\r\n    project_root = None\r\n    # Check if 'src' directory exists at the determined project root level\r\n    if os.path.isdir(os.path.join(project_root_candidate_one_up, 'src')):\r\n        project_root = project_root_candidate_one_up\r\n    elif os.path.isdir(os.path.join(project_root_candidate_two_up, 'src')): # Fallback for deeper nesting\r\n        project_root = project_root_candidate_two_up\r\n    else:\r\n        # Using print here as logger might not be available/configured at this early stage of module loading\r\n        print(f\"Multiplayer Plugin (main.py) Warning: 'src' directory not reliably found from {current_file_dir}. Imports might fail.\")\r\n        # Default to a common structure if unsure (e.g., plugin is in 'project_root/plugins/plugin_name/')\r\n        project_root = project_root_candidate_one_up\r\n\r\n    if project_root and project_root not in sys.path:\r\n        sys.path.insert(0, project_root)\r\n        # Optional: print(f\"Multiplayer Plugin (main.py): Added '{project_root}' to sys.path for src imports.\")\r\n    if current_file_dir not in sys.path: # Add current plugin directory to sys.path (for relative imports if run directly)\r\n        sys.path.insert(0, current_file_dir)\r\n\r\nexcept Exception as e:\r\n    print(f\"Multiplayer Plugin (main.py): Error setting up sys.path: {e}\")\r\n# --- End Python Path Setup ---\r\n\r\n# Import after sys.path modifications\r\ntry:\r\n    from src.tamagotchi_logic import TamagotchiLogic\r\nexcept ImportError:\r\n    # This print is important for diagnosing issues if the main application structure isn't found\r\n    print(\"Multiplayer Plugin (main.py) CRITICAL IMPORT ERROR: TamagotchiLogic could not be imported. Ensure 'src' is in sys.path and contains tamagotchi_logic.py.\")\r\n    TamagotchiLogic = None # Define as None if import fails, plugin should handle this gracefully.\r\n\r\n# Import plugin metadata constants (defined centrally)\r\nfrom . import mp_constants # Use this to access PLUGIN_NAME, etc.\r\n\r\n# Import the main plugin class\r\nfrom .mp_plugin_logic import MultiplayerPlugin\r\n\r\n\r\n# --- Plugin Registration Function ---\r\ndef initialize(plugin_manager_instance): # plugin_manager_instance is the actual PluginManager object\r\n    \"\"\"\r\n    Initializes the Multiplayer plugin and registers it with the plugin manager.\r\n    This function is called by the plugin system.\r\n    \"\"\"\r\n    try:\r\n        # Create an instance of the main plugin class\r\n        plugin_instance = MultiplayerPlugin() # This is MultiplayerPlugin from mp_plugin_logic.py\r\n\r\n        # --- Set plugin_manager on the instance ---\r\n        # Explicitly set the plugin_manager on the plugin instance here.\r\n        # This ensures it's available to the plugin instance's methods like enable() and particularly setup().\r\n        # Assumes MultiplayerPlugin.__init__ defines self.plugin_manager = None\r\n        plugin_instance.plugin_manager = plugin_manager_instance\r\n\r\n        # Define a unique key for the plugin (e.g., based on its name from constants)\r\n        plugin_key = mp_constants.PLUGIN_NAME.lower().replace(\" \", \"_\") # Example: \"multiplayer\"\r\n\r\n        # Register the plugin with the plugin manager\r\n        # The plugin manager will use this information to manage the plugin\r\n        plugin_manager_instance.plugins[plugin_key] = {\r\n            'instance': plugin_instance,          # The actual plugin object\r\n            'name': mp_constants.PLUGIN_NAME,      # Display name of the plugin\r\n            'version': mp_constants.PLUGIN_VERSION,# Version number\r\n            'author': mp_constants.PLUGIN_AUTHOR,  # Author(s)\r\n            'description': mp_constants.PLUGIN_DESCRIPTION, # Brief description\r\n            'requires': mp_constants.PLUGIN_REQUIRES,      # List of dependencies (other plugin names)\r\n            'is_setup': False,                    # Plugin's own setup method will set this to True\r\n            'is_enabled_by_default': False         # This should ALWAYS ALWAYS be set to 'False' or bad things will happen\r\n        }\r\n\r\n        # The plugin manager should ideally pass itself to the plugin instance,\r\n        # which we now do above.\r\n        # The MultiplayerPlugin.enable() method relies on self.plugin_manager being set.\r\n\r\n        # This print uses the global print function, as an instance logger isn't set up for this main.py scope.\r\n        print(f\"{mp_constants.PLUGIN_NAME} (Version: {mp_constants.PLUGIN_VERSION} by {mp_constants.PLUGIN_AUTHOR}) has been registered with the plugin manager.\")\r\n        return True\r\n    except Exception as e:\r\n        # Use global print for errors at this very early stage if a logger isn't available/reliable\r\n        print(f\"Error during {mp_constants.PLUGIN_NAME} plugin initialization (in plugins/multiplayer/main.py): {e}\")\r\n        traceback.print_exc() # Print full traceback for diagnosing initialization errors\r\n        return False\r\n\r\n# --- End of Plugin Registration ---"
  },
  {
    "path": "plugins/multiplayer/mp_constants.py",
    "content": "# File: mp_constants.py\r\n\r\n# --- Plugin Metadata ---\r\n# These constants describe the plugin to the system and users.\r\nPLUGIN_NAME = \"Multiplayer\"\r\nPLUGIN_VERSION = \"1.2.0\"\r\nPLUGIN_AUTHOR = \"ViciousSquid\"\r\nPLUGIN_DESCRIPTION = \"Enables network sync for squids and objects (Experimental)\"\r\nPLUGIN_REQUIRES = [] # Names of other plugins this one depends on\r\n\r\n# --- Network Configuration ---\r\n# These define the network parameters for multicast communication.\r\nMULTICAST_GROUP = '224.3.29.71'   # IP address for the multicast group\r\nMULTICAST_PORT = 10000            # Port number for multicast communication\r\nSYNC_INTERVAL = 1.0               # Default seconds between game state sync broadcasts\r\nMAX_PACKET_SIZE = 1472           # Maximum UDP packet size, to prevent fragmentation\r\n\r\nUSE_TCP        = False   # default – restored from ini\r\nTCP_IP_LIST    = []      # will be ['192.168.1.50','192.168.1.51',…]\r\nTCP_PORT       = 5008\r\n\r\n# --- Visual Settings (Defaults) ---\r\n# These are default visual parameters. The MultiplayerPlugin instance may override these\r\n# based on runtime configuration (e.g., from a settings dialog).\r\nREMOTE_SQUID_OPACITY = 1.0        # Default opacity for remote squids (0.0 to 1.0)\r\nSHOW_REMOTE_LABELS = True         # Default setting for showing labels on remote entities\r\n\r\nSHOW_CONNECTION_LINES = True      # Default setting for showing lines connecting to remote squids\r\n"
  },
  {
    "path": "plugins/multiplayer/mp_network_node.py",
    "content": "# File: mp_network_node.py\n\nimport uuid\nimport time\nimport socket\nimport queue\nimport zlib\nimport json\nimport traceback\nimport threading\nimport logging\n\nfrom .mp_constants import MULTICAST_GROUP, MULTICAST_PORT, MAX_PACKET_SIZE\n\n\nclass NetworkNode:\n    def __init__(self, node_id=None, logger=None):\n        \"\"\"\n        Represents a networked node in the multiplayer system.\n\n        Args:\n            node_id (str, optional): Unique identifier for this node.\n                                     Generated if not provided.\n            logger (logging.Logger, optional): Logger instance to use.\n                                               A default one is created if not provided.\n        \"\"\"\n        try:\n            # Attempt to use NetworkUtilities if available\n            from plugins.multiplayer.network_utilities import NetworkUtilities\n            self.node_id = node_id or NetworkUtilities.generate_node_id()\n            self.utils = NetworkUtilities\n        except ImportError:\n            self.node_id = node_id or f\"squid_{uuid.uuid4().hex[:8]}\"\n            self.utils = None # Mark utils as unavailable\n\n        self.socket = None\n        self.initialized = False # Socket structure initialized (IP_ADD_MEMBERSHIP etc.)\n        self.is_connected = False # Socket bound and ready for I/O\n        \n        self._is_listening_active = False # Flag to control the listening loop\n        self.listener_thread = None      # Thread object for the listening loop\n\n        self.last_connection_attempt = 0\n        self.connection_retry_interval = 5.0 # seconds\n        self.auto_reconnect = True # Flag to control auto-reconnect attempts\n        self.use_compression = True # Flag to control message compression\n\n        self.incoming_queue = queue.Queue(maxsize=500)  # Bounded: if drain stops, old packets are dropped rather than eating RAM\n        self.queue_lock = threading.Lock() # Used with incoming_message_queue in one of the versions, ensure consistency\n\n        # Deduplication: on a single machine every multicast packet is received once\n        # per membership (once per interface + the 0.0.0.0 catch-all), so the same\n        # message can arrive 2-3× in the queue.  We track (node_id, timestamp) tuples\n        # and silently drop duplicates for self._dedup_ttl seconds.\n        self._seen_message_ids: dict = {}  # {(node_id, timestamp): time_first_seen}\n        self._dedup_ttl: float = 10.0      # seconds before a seen-key is expired\n\n        self.known_nodes = {} # Stores info about other detected nodes\n        self.last_sync_time = 0 # Timestamp of the last sync operation\n        self.debug_mode = False # Controlled by MultiplayerPlugin\n\n        # Logger must be set up before _get_local_ip() which uses self.logger\n        if logger is not None:\n            self.logger = logger\n        else:\n            _logger_name = f\"{__name__}.NetworkNode.{self.node_id[:4]}\"\n            self.logger = logging.getLogger(_logger_name)\n            if not self.logger.handlers: # Avoid adding multiple handlers if logger is passed around\n                handler = logging.StreamHandler()\n                formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n                handler.setFormatter(formatter)\n                self.logger.addHandler(handler)\n                # Initial level, can be updated by MultiplayerPlugin if debug_mode changes\n                self.logger.setLevel(logging.DEBUG if self.debug_mode else logging.INFO)\n\n        self.local_ip = self._get_local_ip()\n        \n        self.initialize_socket_structure() # Initialize socket when a NetworkNode is created\n\n    def _get_local_ip(self):\n        \"\"\"\n        Detects the best local LAN IP for multicast by enumerating all interfaces\n        and ranking them. Prefers 192.168.x.x (typical home/office LAN) over\n        172.16.x.x, then 10.x.x.x -- avoiding VPN/virtual adapters that may have\n        captured the default route and share the same IP across machines.\n\n        Set MULTICAST_BIND_IP in mp_constants.py to a specific IP to override.\n        \"\"\"\n        # Allow manual override via config\n        from . import mp_constants as _mc\n        override = getattr(_mc, 'MULTICAST_BIND_IP', '').strip()\n        if override and override != '0.0.0.0':\n            self.logger.info(f\"[IP] Using manually configured MULTICAST_BIND_IP: {override}\")\n            return override\n\n        candidates = []\n\n        # Strategy 1: enumerate all IPs via getaddrinfo on hostname\n        try:\n            hostname = socket.gethostname()\n            for info in socket.getaddrinfo(hostname, None, socket.AF_INET):\n                ip = info[4][0]\n                if ip not in candidates:\n                    candidates.append(ip)\n        except Exception:\n            pass\n\n        # Strategy 2: also grab the default-route IP the OS prefers\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:\n                s.settimeout(0.1)\n                s.connect((\"8.8.8.8\", 80))\n                default_ip = s.getsockname()[0]\n                if default_ip not in candidates:\n                    candidates.append(default_ip)\n        except Exception:\n            pass\n\n        self.logger.info(f\"[IP] All detected IPs on this machine: {candidates}\")\n\n        def score(ip):\n            \"\"\"Higher score = more likely a real LAN interface.\"\"\"\n            if ip.startswith('127.') or ip.startswith('169.254.'):\n                return -1   # loopback / link-local: skip\n            if ip.startswith('192.168.'):\n                return 3    # classic home/office LAN -- highest priority\n            if ip.startswith('172.'):\n                parts = ip.split('.')\n                try:\n                    if 16 <= int(parts[1]) <= 31:\n                        return 2  # RFC1918 172.16-31 range\n                except (IndexError, ValueError):\n                    pass\n            if ip.startswith('10.'):\n                return 1    # Could be LAN or VPN; lower priority\n            return 0\n\n        scored = [(score(ip), ip) for ip in candidates if score(ip) >= 0]\n        scored.sort(key=lambda x: x[0], reverse=True)\n\n        self.logger.info(f\"[IP] Scored candidates (higher=better): {scored}\")\n\n        if scored:\n            chosen = scored[0][1]\n            if len(scored) > 1 and scored[0][0] == scored[1][0]:\n                all_ips = [ip for _, ip in scored]\n                self.logger.warning(\n                    f\"[IP] Multiple equal-score IPs: {all_ips}. Using {chosen}. \"\n                    f\"If wrong (e.g. a VPN IP), set MULTICAST_BIND_IP in mp_constants.py to your real LAN IP.\"\n                )\n            else:\n                self.logger.info(f\"[IP] Selected local IP for multicast: {chosen}\")\n            return chosen\n\n        self.logger.warning(\"[IP] Could not detect a suitable LAN IP. Falling back to 127.0.0.1.\")\n        return '127.0.0.1'\n\n    def _get_all_send_ips(self):\n        \"\"\"\n        Returns all local IPv4 addresses that are valid for multicast sending,\n        scored so the best LAN IPs come first. Always includes 0.0.0.0 (default\n        interface) as a fallback at the end so same-machine loopback always works\n        even if NIC enumeration misses something.\n        \"\"\"\n        def score(ip):\n            if ip.startswith('127.') or ip.startswith('169.254.'):\n                return -1\n            if ip.startswith('192.168.'):\n                return 3\n            if ip.startswith('172.'):\n                try:\n                    if 16 <= int(ip.split('.')[1]) <= 31:\n                        return 2\n                except (IndexError, ValueError):\n                    pass\n            if ip.startswith('10.'):\n                return 1\n            return 0\n\n        candidates = set()\n        try:\n            for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):\n                candidates.add(info[4][0])\n        except Exception:\n            pass\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:\n                s.settimeout(0.1)\n                s.connect((\"8.8.8.8\", 80))\n                candidates.add(s.getsockname()[0])\n        except Exception:\n            pass\n\n        scored = sorted(\n            [(score(ip), ip) for ip in candidates if score(ip) >= 0],\n            key=lambda x: x[0], reverse=True\n        )\n        result = [ip for _, ip in scored]\n\n        # Always ensure 0.0.0.0 is last — it lets the OS pick the default route\n        # interface, which covers same-machine loopback if nothing else does.\n        if '0.0.0.0' not in result:\n            result.append('0.0.0.0')\n\n        self.logger.info(f\"[MCAST] Send interfaces: {result}\")\n        return result\n\n    def initialize_socket_structure(self):\n        \"\"\"Initializes the socket, sets options, binds, and joins the multicast group.\"\"\"\n        if self.is_connected and self.socket: # Check if already properly set up\n            self.logger.info(\"Socket structure already initialized and connected.\")\n            return True\n            \n        try:\n            if self.socket: # If socket exists but not connected, close it first\n                try:\n                    self.socket.close()\n                except Exception: pass # Ignore errors on close if already closed\n            \n            self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)\n            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            # SO_REUSEPORT allows multiple processes to bind to the same port, useful for testing on one machine\n            if hasattr(socket, \"SO_REUSEPORT\"):\n                try:\n                    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)\n                except OSError as e:\n                    if self.debug_mode: self.logger.debug(f\"SO_REUSEPORT not supported or error setting it: {e}\")\n\n            try:\n                # Enable loopback so packets are delivered to other sockets on the same host.\n                # Value 1 = enabled (was incorrectly set to 0, which disabled same-machine peer detection).\n                self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)\n                if self.debug_mode: self.logger.debug(\"Multicast loopback enabled.\")\n            except socket.error as e_loop:\n                self.logger.warning(f\"Could not enable multicast loopback: {e_loop}. May impact same-machine testing.\")\n\n            self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) # Time-to-live for multicast packets\n\n            # All instances MUST bind to the same port (MULTICAST_PORT) so they all\n            # receive packets sent to that port. The old port-increment fallback was\n            # broken: if instance 2 bumped to 10001, it sent to 10000 but listened on\n            # 10001, so it never heard anything. SO_REUSEADDR (set above) allows\n            # multiple processes to share a UDP multicast port on Windows.\n            try:\n                self.socket.bind(('', MULTICAST_PORT))\n                self.logger.info(f\"Socket bound successfully to port {MULTICAST_PORT}.\")\n            except OSError as bind_error:\n                self.logger.error(\n                    f\"Could not bind to port {MULTICAST_PORT}: {bind_error}. \"\n                    f\"Ensure SO_REUSEADDR is supported and no non-multicast process has exclusively claimed this port.\"\n                )\n                self.is_connected = False\n                self.initialized = False\n                return False\n            \n            # Join the multicast group on every valid local interface so we receive\n            # packets whether they arrive from the loopback (same-machine peers) or\n            # from the physical LAN adapter (remote peers).\n            self._multicast_send_ips = self._get_all_send_ips()\n            joined_count = 0\n            for iface_ip in self._multicast_send_ips:\n                try:\n                    mreq = socket.inet_aton(MULTICAST_GROUP) + socket.inet_aton(iface_ip)\n                    self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)\n                    joined_count += 1\n                    self.logger.info(f\"[MCAST] Joined multicast group on interface {iface_ip}\")\n                except OSError as e_join:\n                    self.logger.warning(f\"[MCAST] Could not join group on {iface_ip}: {e_join}\")\n            # Always also join via 0.0.0.0 as a catch-all safety net\n            try:\n                mreq_any = socket.inet_aton(MULTICAST_GROUP) + socket.inet_aton(\"0.0.0.0\")\n                self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq_any)\n                self.logger.info(f\"[MCAST] Also joined via 0.0.0.0 (catch-all). Total explicit joins: {joined_count}\")\n            except OSError:\n                pass  # Already joined on all interfaces - OS treats 0.0.0.0 as duplicate, harmless\n            \n            self.socket.settimeout(0.1) # Non-blocking recvfrom\n\n            self.is_connected = True  # Socket is bound and ready\n            self.initialized = True # Full structure (options, group) is set up\n            self.last_connection_attempt = time.time()\n            self.logger.info(f\"Socket structure initialized on {self.local_ip} (listening on all interfaces, port {MULTICAST_PORT}) for multicast group {MULTICAST_GROUP}.\")\n            return True\n\n        except Exception as e:\n            self.logger.error(f\"Error initializing socket structure: {e}\", exc_info=self.debug_mode)\n            if self.socket:\n                try: self.socket.close()\n                except: pass\n            self.socket = None\n            self.is_connected = False\n            self.initialized = False\n            return False\n\n    def _listen_for_multicast(self):\n        \"\"\"Dedicated thread function to listen for incoming multicast packets.\"\"\"\n        self.logger.info(f\"Listener thread started for node {self.node_id} on IP {self.local_ip}.\")\n        while self._is_listening_active: # Loop controlled by flag\n            if not self.is_connected or not self.socket:\n                self.logger.warning(\"Listening loop: Socket not connected or available. Attempting to reconnect...\")\n                if not self.try_reconnect(): # This will call initialize_socket_structure\n                    self.logger.error(\"Listening loop: Reconnect failed. Pausing before retry.\")\n                    time.sleep(self.connection_retry_interval) \n                    continue # Retry connection\n\n            try:\n                raw_data, addr = self.socket.recvfrom(MAX_PACKET_SIZE)\n\n                if raw_data:\n                    # Use the thread-safe queue for passing data to the main thread.\n                    # put_nowait raises queue.Full (never blocks) so the listener\n                    # thread can't be stalled if the main thread stops draining.\n                    try:\n                        self.incoming_queue.put_nowait({'raw_data': raw_data, 'addr': addr})\n                    except queue.Full:\n                        # Queue is full – main thread has stopped draining.  Drop\n                        # the oldest packet to make room (FIFO discard) rather than\n                        # losing the newest one entirely.\n                        try:\n                            self.incoming_queue.get_nowait()\n                        except queue.Empty:\n                            pass\n                        try:\n                            self.incoming_queue.put_nowait({'raw_data': raw_data, 'addr': addr})\n                        except queue.Full:\n                            pass  # Give up on this packet – main thread is very stuck\n            except socket.timeout:\n                continue # Normal behavior for non-blocking socket, allows checking _is_listening_active\n            except OSError as e: # Handle socket closed or other OS errors\n                if self._is_listening_active: # Log only if we weren't expecting closure\n                    self.logger.error(f\"Socket OS error in listener thread: {e}\", exc_info=True)\n                self.is_connected = False # Assume connection is lost if OS error occurs\n                # No break here, rely on try_reconnect in the next iteration if _is_listening_active is still true\n            except Exception as e: # Catch any other unexpected errors\n                if self._is_listening_active:\n                     self.logger.error(f\"Unexpected error in listener thread: {e}\", exc_info=True)\n                time.sleep(0.1) # Brief pause before retrying or exiting based on flag\n\n        self.logger.info(f\"Listener thread stopped for node {self.node_id}.\")\n\n\n    def is_listening(self):\n        \"\"\"Checks if the listener thread is active and alive.\"\"\"\n        return self._is_listening_active and self.listener_thread is not None and self.listener_thread.is_alive()\n\n    def watchdog_check(self) -> bool:\n        \"\"\"\n        Call this periodically (e.g. from the plugin's connection_timer) to\n        detect and recover a silently-dead listener thread.\n\n        A thread can die silently when an unexpected exception escapes the\n        while-loop (shouldn't happen given the broad except clause, but OS\n        signal delivery or interpreter shutdown edge cases can still kill it).\n\n        Returns True if everything is healthy, False if a restart was attempted.\n        \"\"\"\n        if not self._is_listening_active:\n            return True  # We deliberately stopped – not a fault\n\n        if self.listener_thread is None or not self.listener_thread.is_alive():\n            self.logger.error(\n                f\"Watchdog: Listener thread for node {self.node_id} has died unexpectedly! \"\n                f\"Attempting automatic restart.\"\n            )\n            self._is_listening_active = False   # Reset flag so start_listening doesn't bail early\n            self.listener_thread = None\n            restarted = self.start_listening()\n            if restarted:\n                self.logger.info(\"Watchdog: Listener thread restarted successfully.\")\n            else:\n                self.logger.error(\"Watchdog: Listener thread restart FAILED.\")\n            return False  # Signal that a recovery was needed\n\n        return True  # Thread is alive and well\n\n    def start_listening(self):\n        \"\"\"Starts the multicast listener thread if not already running.\"\"\"\n        if self.is_listening():\n            self.logger.info(\"Attempted to start listening, but already listening.\")\n            return True # Already doing its job\n\n        # Ensure socket is initialized and connected before starting to listen\n        if not self.initialized or not self.is_connected:\n            self.logger.warning(\"Socket not initialized or connected. Attempting to initialize before listening.\")\n            if not self.initialize_socket_structure():\n                self.logger.error(\"Failed to initialize socket structure. Cannot start listening.\")\n                return False\n        \n        self.logger.info(\"Starting network listener thread...\")\n        try:\n            self._is_listening_active = True # Set flag before starting thread\n            self.listener_thread = threading.Thread(target=self._listen_for_multicast, daemon=True)\n            self.listener_thread.setName(f\"MPNodeListener-{self.node_id[:4]}\") # Helpful for debugging threads\n            self.listener_thread.start()\n            self.logger.info(\"Listener thread started successfully.\")\n            return True\n        except Exception as e:\n            self.logger.error(f\"Failed to start listener thread: {e}\", exc_info=self.debug_mode)\n            self._is_listening_active = False # Reset flag on error\n            return False\n\n    def stop_listening(self):\n        \"\"\"Stops the multicast listener thread.\"\"\"\n        if not self._is_listening_active and (self.listener_thread is None or not self.listener_thread.is_alive()):\n            # If the flag is already false and thread is gone or never started.\n            self.logger.info(\"Listener already stopped or was never started.\")\n            return\n\n        self.logger.info(\"Stopping listener thread...\")\n        self._is_listening_active = False # Signal the loop to terminate\n        \n        if self.listener_thread and self.listener_thread.is_alive():\n            self.listener_thread.join(timeout=2.0) # Wait for the thread to finish\n            if self.listener_thread.is_alive():\n                # This might happen if socket.recvfrom() is stuck, though timeout should prevent it.\n                self.logger.warning(\"Listener thread did not stop in time (join timeout).\")\n            else:\n                self.logger.info(\"Listener thread joined successfully.\")\n        else:\n            self.logger.info(\"Listener thread was not active or did not exist at explicit stop.\")\n            \n        self.listener_thread = None # Clear the thread object\n\n\n    def try_reconnect(self):\n        \"\"\"Attempts to re-establish the socket connection and restart listening if needed.\"\"\"\n        if self.is_listening(): # If already listening, assume connection is fine\n            return True\n\n        current_time = time.time()\n        if current_time - self.last_connection_attempt < self.connection_retry_interval:\n            # Avoid rapid reconnection attempts\n            return False \n\n        self.logger.info(\"Attempting to reconnect and restart listener...\")\n        self.last_connection_attempt = current_time\n        \n        self.stop_listening() # Ensure any old listener is stopped\n\n        if self.initialize_socket_structure(): # Re-initialize socket (binds, joins group)\n            return self.start_listening()      # Start listening again\n        else:\n            self.logger.error(\"Reconnect failed: Could not re-initialize socket structure.\")\n            return False\n\n    def send_message(self, message_type: str, payload: dict):\n        \"\"\"Sends a single message to the multicast group.\"\"\"\n        if not self.is_connected: # Check if socket is ready\n            self.logger.warning(f\"Cannot send '{message_type}', socket not connected.\")\n            if self.auto_reconnect and not self.try_reconnect(): # Attempt to reconnect if enabled\n                self.logger.error(f\"Send failed for '{message_type}': Reconnect attempt failed.\")\n                return False\n            elif not self.is_connected: # If still not connected after attempt\n                 self.logger.error(f\"Send failed for '{message_type}': Still not connected after reconnect check.\")\n                 return False\n\n        # Construct the full message with metadata\n        message_data = {\n            'node_id': self.node_id,       # Sender's ID\n            'timestamp': time.time(),    # Time of sending\n            'type': message_type,        # Type of message (e.g., \"squid_exit\")\n            'payload': payload           # The actual data payload\n        }\n\n        try:\n            data_to_send: bytes\n            serialized_message = json.dumps(message_data).encode('utf-8')\n\n            if self.use_compression:\n                # Use NetworkUtilities for compression if available, else direct zlib\n                if self.utils and hasattr(self.utils, 'compress_message'):\n                    # Assuming compress_message takes the dict and returns bytes\n                    data_to_send = self.utils.compress_message(message_data) \n                else: # Fallback to direct zlib if NetworkUtilities or method missing\n                    data_to_send = zlib.compress(serialized_message)\n                if self.debug_mode and message_type.upper() in [\"SQUID_EXIT\", \"SQUID_RETURN\"]:\n                    self.logger.debug(f\"DEBUG_COMPRESS (send): Type: {message_type}. Original: {len(serialized_message)}, Compressed: {len(data_to_send)}\")\n            else: \n                data_to_send = serialized_message\n\n            if len(data_to_send) > MAX_PACKET_SIZE:\n                self.logger.warning(f\"Message '{message_type}' size ({len(data_to_send)}) exceeds MAX_PACKET_SIZE. May fail or be fragmented (UDP handles this, but can be less reliable).\")\n\n            if not self.socket:\n                self.logger.error(f\"Cannot send '{message_type}', socket is None.\")\n                return False\n\n            # Send on every valid local interface so that:\n            #   - same-machine peers receive it via IP_MULTICAST_LOOP loopback\n            #   - LAN peers receive it via the physical NIC\n            send_ips = getattr(self, '_multicast_send_ips', None) or ['0.0.0.0']\n            sent_ok = False\n            for iface_ip in send_ips:\n                try:\n                    iface_bytes = socket.inet_aton(iface_ip)\n                    self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, iface_bytes)\n                    self.socket.sendto(data_to_send, (MULTICAST_GROUP, MULTICAST_PORT))\n                    sent_ok = True\n                except OSError as e_send:\n                    self.logger.warning(f\"[MCAST] Send on interface {iface_ip} failed: {e_send}\")\n\n            if self.debug_mode and message_type not in ['object_sync', 'squid_move', 'heartbeat']:\n                self.logger.debug(f\"Sent '{message_type}' ({len(data_to_send)} bytes) on {len(send_ips)} interface(s).\")\n            return sent_ok\n        except socket.error as sock_err: # Specific socket errors\n            self.logger.error(f\"Socket error sending message '{message_type}': {sock_err}\")\n            self.is_connected = False # Assume connection is broken\n            self.stop_listening() # Stop listener as connection is likely bad\n        except Exception as e: # Other errors (JSON encoding, compression etc.)\n            self.logger.error(f\"Error sending message '{message_type}': {e}\", exc_info=self.debug_mode)\n        return False\n\n    def send_message_batch(self, messages: list):\n        \"\"\"Sends a batch of messages in a single packet.\"\"\"\n        # Connection check similar to send_message\n        if not self.is_connected:\n            self.logger.warning(\"Cannot send batch, socket not connected.\")\n            if self.auto_reconnect and not self.try_reconnect():\n                self.logger.error(\"Send batch failed: Reconnect attempt failed.\")\n                return False\n            elif not self.is_connected:\n                 self.logger.error(\"Send batch failed: Still not connected after reconnect check.\")\n                 return False\n\n        # Structure for batch message\n        batch_data = {\n            'node_id': self.node_id,\n            'timestamp': time.time(),\n            'batch': True, # Indicates this packet contains multiple messages\n            'messages': [{'type': msg_type, 'payload': payload} for msg_type, payload in messages]\n        }\n\n        try:\n            data_to_send: bytes\n            serialized_batch = json.dumps(batch_data).encode('utf-8')\n\n            if self.use_compression:\n                if self.utils and hasattr(self.utils, 'compress_message'):\n                    data_to_send = self.utils.compress_message(batch_data)\n                else:\n                    data_to_send = zlib.compress(serialized_batch)\n            else: \n                data_to_send = serialized_batch\n\n            if len(data_to_send) > MAX_PACKET_SIZE:\n                self.logger.warning(f\"Batch message size ({len(data_to_send)}) exceeds MAX_PACKET_SIZE. Transmission may fail.\")\n\n            if not self.socket:\n                self.logger.error(\"Cannot send batch, socket is None.\")\n                return False\n\n            send_ips = getattr(self, '_multicast_send_ips', None) or ['0.0.0.0']\n            sent_ok = False\n            for iface_ip in send_ips:\n                try:\n                    self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface_ip))\n                    self.socket.sendto(data_to_send, (MULTICAST_GROUP, MULTICAST_PORT))\n                    sent_ok = True\n                except OSError as e_send:\n                    self.logger.warning(f\"[MCAST] Batch send on interface {iface_ip} failed: {e_send}\")\n            if self.debug_mode:\n                self.logger.debug(f\"Sent batch ({len(data_to_send)} bytes, {len(messages)} msgs) on {len(send_ips)} interface(s).\")\n            return sent_ok\n        except socket.error as sock_err:\n            self.logger.error(f\"Socket error sending message batch: {sock_err}\")\n            self.is_connected = False\n            self.stop_listening()\n        except Exception as e:\n            self.logger.error(f\"Error sending message batch: {e}\", exc_info=self.debug_mode)\n        return False\n\n    def receive_messages(self):\n        \"\"\"\n        Processes all currently queued raw datagrams from the listener thread.\n        This should be called by the main application thread.\n        Returns:\n            list: A list of (message_dict, address_tuple) for successfully decoded messages.\n        \"\"\"\n        if not self.is_connected and not self.initialized : # If socket isn't even set up\n            # This case might occur if receive_messages is called before successful initialization\n            # or after a critical failure.\n            # self.logger.debug(\"receive_messages called but socket not initialized/connected.\")\n            return [] \n\n        received_messages_this_call = []\n        \n        # Process all items currently in the queue\n        while not self.incoming_queue.empty():\n            try:\n                item = self.incoming_queue.get_nowait() # Get item from queue\n                raw_data = item['raw_data']\n                addr = item['addr']\n            except queue.Empty: # Should not happen with while not empty(), but as safeguard\n                break\n            except Exception as e_q: # Should not happen for basic queue ops\n                self.logger.error(f\"Error getting item from incoming_queue: {e_q}\")\n                continue\n\n            # Peek at sender_node_id from raw data if possible (for debug log context)\n            temp_node_id_peek = \"unknown_at_raw_recv\"\n            try: # This peeking is best-effort for logging, might fail if data is not as expected\n                peek_data_bytes = raw_data\n                if self.use_compression: # Try decompressing a copy for peeking\n                    try: peek_data_bytes = zlib.decompress(raw_data)\n                    except zlib.error: pass # If not zlib compressed, peek_data_bytes remains raw_data\n                \n                j_peek = json.loads(peek_data_bytes.decode('utf-8', errors='ignore'))\n                temp_node_id_peek = j_peek.get('node_id', 'peek_decode_fail')\n            except: # Broad except as peeking can fail in many ways\n                temp_node_id_peek = 'peek_failed_entirely'\n\n\n            # Log raw reception if it's not from self (based on peek)\n            if temp_node_id_peek != self.node_id:\n                if self.debug_mode: print(f\"DEBUG_RAW_RECEIVE (Node {self.node_id}) from {addr} (Peeked Sender: {temp_node_id_peek}). Size: {len(raw_data)}. Data[:60]: {raw_data[:60]}\")\n            \n            message_dict = None\n            decoded_successfully = False\n            # Try decoding (with or without compression)\n            try:\n                data_for_json_decode = raw_data\n                if self.use_compression:\n                    try:\n                        if self.utils and hasattr(self.utils, 'decompress_message'):\n                            # Assumes decompress_message returns a dict or raises error\n                            message_dict = self.utils.decompress_message(raw_data)\n                        else: # Fallback to direct zlib + json\n                            data_for_json_decode = zlib.decompress(raw_data)\n                            message_dict = json.loads(data_for_json_decode.decode('utf-8'))\n                        decoded_successfully = True\n                    except (zlib.error, TypeError) as e_zlib: # TypeError if utils.decompress_message fails unexpectedly\n                        # If zlib fails, it might be an uncompressed message. Try decoding raw_data as JSON.\n                        if self.debug_mode: self.logger.debug(f\"Zlib decompression failed from {addr} (Sender: {temp_node_id_peek}): {e_zlib}. Trying as uncompressed JSON.\")\n                        # data_for_json_decode remains raw_data\n                        message_dict = json.loads(raw_data.decode('utf-8'))\n                        decoded_successfully = True # If this line is reached, uncompressed JSON was successful\n                else: # Not using compression, just decode JSON\n                    message_dict = json.loads(raw_data.decode('utf-8'))\n                    decoded_successfully = True\n            \n            except (json.JSONDecodeError, UnicodeDecodeError) as e_decode:\n                if self.debug_mode: self.logger.warning(f\"Failed to decode JSON/UTF-8 from {addr} (Sender: {temp_node_id_peek}). Error: {e_decode}. Data: {raw_data[:80]}\")\n                continue # Skip this malformed packet\n            except Exception as e_general_decode: # Catch-all for other unexpected decoding issues\n                if self.debug_mode: self.logger.error(f\"General error decoding packet from {addr} (Sender: {temp_node_id_peek}): {e_general_decode}\", exc_info=True)\n                continue\n\n            if not decoded_successfully or not isinstance(message_dict, dict) or 'node_id' not in message_dict:\n                if self.debug_mode: self.logger.debug(f\"Invalid or incomplete message structure after all decode attempts from {addr} (Sender: {temp_node_id_peek}): {message_dict}\")\n                continue\n            \n            final_sender_node_id = message_dict.get('node_id')\n            \n            # Critical filter: Ignore messages from self\n            if final_sender_node_id == self.node_id:\n                continue\n\n            # ── Deduplication ────────────────────────────────────────────────────────\n            # On a single machine every multicast send is received once per membership\n            # (once per NIC interface + the 0.0.0.0 catch-all join), so the same\n            # logical message can arrive 2-3× in the queue.  We key on\n            # (sender_node_id, timestamp) – every outgoing envelope already stamps a\n            # float timestamp – and silently drop copies seen within _dedup_ttl seconds.\n            _now = time.time()\n            # Prune expired entries so the dict stays small\n            self._seen_message_ids = {\n                k: v for k, v in self._seen_message_ids.items()\n                if _now - v < self._dedup_ttl\n            }\n            msg_dedup_key = (final_sender_node_id, message_dict.get('timestamp'))\n            if msg_dedup_key in self._seen_message_ids:\n                if self.debug_mode:\n                    self.logger.debug(\n                        f\"Dropping duplicate message key={msg_dedup_key} from {addr}\"\n                    )\n                continue\n            self._seen_message_ids[msg_dedup_key] = _now\n            # ─────────────────────────────────────────────────────────────────────────\n\n            # Log decoded message details\n            if self.debug_mode:\n                payload_keys_str = list(message_dict.get('payload', {}).keys()) if isinstance(message_dict.get('payload'), dict) else 'Payload_Not_Dict'\n                print(f\"DEBUG_DECODED (Node {self.node_id}) from {addr}: Type '{message_dict.get('type', 'N/A')}', From Node '{final_sender_node_id}', PayloadKeys: {payload_keys_str}\")\n                if message_dict.get('type') == 'squid_exit': # Specific debug for SQUID_EXIT payload\n                    print(f\"DEBUG_SQUID_EXIT_PAYLOAD_RECEIVED: {message_dict.get('payload')}\")\n            \n            # Update known_nodes (this is a simplified version, a more robust presence system might be needed)\n            # The payload of interest for squid's last known state might be deeper, e.g., message_dict['payload']['payload'] for SQUID_EXIT\n            squid_info_for_known_nodes = message_dict.get('payload', {}) \n            if message_dict.get('type') == 'squid_exit' and isinstance(squid_info_for_known_nodes.get('payload'), dict):\n                squid_info_for_known_nodes = squid_info_for_known_nodes.get('payload')\n\n            self.known_nodes[final_sender_node_id] = (addr[0], time.time(), squid_info_for_known_nodes)\n            \n            # Add the fully processed message and its original address to the list for the caller\n            received_messages_this_call.append((message_dict, addr))\n\n        return received_messages_this_call\n\n\n    def process_messages(self, plugin_manager_ref): \n        \"\"\"\n        Retrieves messages from the internal queue (filled by receive_messages via listener thread)\n        and triggers hooks in the PluginManager.\n        This method is intended to be called by the main application thread.\n        \"\"\"\n        messages_to_process_from_queue = []\n        while not self.incoming_queue.empty(): # Drain the queue\n            try:\n                # Item from queue is expected to be {'raw_data': ..., 'addr': ...} from _listen_for_multicast\n                # NO, item from queue should be (decoded_message_dict, addr) if receive_messages puts decoded ones.\n                # Let's clarify: _listen_for_multicast puts raw data.\n                # receive_messages (called by this process_messages or similar) decodes them.\n                # This process_messages should be working with DECODED messages.\n                \n                # The current structure has receive_messages called by process_messages.\n                # So, call receive_messages first to get decoded messages.\n                decoded_messages_and_addrs = self.receive_messages() # This call processes the queue internally.\n\n                for message_data, addr in decoded_messages_and_addrs:\n                    # Now message_data is a decoded dict\n                    if not isinstance(message_data, dict) or 'type' not in message_data or 'node_id' not in message_data:\n                        if self.debug_mode: self.logger.debug(f\"process_messages: Discarding malformed message: {message_data}\")\n                        continue\n\n                    # Redundant self-check, receive_messages should have handled this.\n                    # if message_data['node_id'] == self.node_id: \n                    #     continue \n\n                    message_type = message_data.get('type', 'unknown_message')\n                    hook_name = f\"on_network_{message_type}\" # Convention for hook names\n\n                    if self.debug_mode: \n                        print(f\"DEBUG_STEP_2A: NetworkNode {self.node_id} attempting to trigger hook: '{hook_name}' for msg type '{message_type}' from node {message_data['node_id']}\")\n                    \n                    # Trigger hook via PluginManager\n                    if hasattr(plugin_manager_ref, 'trigger_hook'): \n                        plugin_manager_ref.trigger_hook(\n                            hook_name, \n                            node=self,          # Pass this NetworkNode instance\n                            message=message_data, # Pass the decoded message dictionary\n                            addr=addr            # Pass the original address tuple\n                        )\n                    # Fallback if PluginManager has a different direct processing method (less common for hook systems)\n                    elif hasattr(plugin_manager_ref, '_process_network_message'): \n                       plugin_manager_ref._process_network_message(message_data, addr)\n                    else: # Log if no way to dispatch the message\n                        if self.debug_mode: self.logger.warning(f\"Plugin manager has no trigger_hook or _process_network_message method for hook {hook_name}\")\n                \n                break # process_messages should ideally process one batch from receive_messages at a time.\n\n            except Exception as e: # Catch any errors during the processing loop\n                self.logger.error(f\"Error in process_messages loop: {e}\", exc_info=self.debug_mode)\n                break # Exit loop on error to avoid continuous failure on same bad data\n\n    def close(self):\n        \"\"\"Cleans up the network node, stops listening, and closes the socket.\"\"\"\n        self.logger.info(f\"Closing network node {self.node_id}...\")\n        self.auto_reconnect = False # Prevent any further reconnect attempts during closure\n        \n        self.stop_listening() # Signal listener thread to stop and wait for it\n               \n        if self.socket:\n            socket_was_initialized_and_connected = self.initialized and self.is_connected\n            self.is_connected = False # Mark as not connected\n            self.initialized = False  # Mark as not initialized\n\n            # Attempt to leave multicast group if socket was properly set up\n            if socket_was_initialized_and_connected and self.local_ip: \n                try:\n                    # Use \"0.0.0.0\" for imr_interface when leaving, consistent with joining\n                    mreq_leave_struct = socket.inet_aton(MULTICAST_GROUP) + socket.inet_aton(\"0.0.0.0\")\n                    self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq_leave_struct)\n                    self.logger.info(\"Left multicast group.\")\n                except socket.error as e_mcast_leave: \n                    if self.debug_mode: self.logger.debug(f\"Socket error leaving multicast group (may be normal if already disconnected): {e_mcast_leave}\")\n                except AttributeError: # Can happen if local_ip was problematic\n                    if self.debug_mode: self.logger.debug(\"AttributeError leaving multicast group (IP likely invalid during shutdown).\")\n                except Exception as e_general_leave: # Catch any other unexpected errors\n                     if self.debug_mode: self.logger.error(f\"Unexpected error leaving multicast group: {e_general_leave}\", exc_info=True)\n            try:\n                self.socket.close()\n                self.logger.info(\"Socket closed.\")\n            except Exception as e_sock_close:\n                if self.debug_mode: self.logger.debug(f\"Error closing socket (may already be closed): {e_sock_close}\")\n            self.socket = None # Clear socket reference\n            \n\n        self.logger.info(f\"Network node {self.node_id} closed.\")\n\n\n\n\n\n\n"
  },
  {
    "path": "plugins/multiplayer/mp_plugin_logic.py",
    "content": "# File: mp_plugin_logic.py\r\n\r\nimport os\r\n# import sys # sys.path is handled in main.py\r\nimport inspect # For _find_tamagotchi_logic\r\nimport json\r\nimport uuid\r\nimport time\r\n# import socket # Network operations are in NetworkNode\r\nimport threading\r\nimport queue # For plugin's own internal queuing if different from NetworkNode's\r\nimport random\r\nimport traceback # For logging exceptions\r\nimport math\r\nfrom typing import Dict, List, Any, Tuple\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\n# --- Local Imports ---\r\nimport logging # Added for logger\r\nfrom . import mp_constants # Access constants like mp_constants.PLUGIN_NAME\r\nfrom .mp_network_node import NetworkNode\r\nfrom .remote_entity_manager import RemoteEntityManager # Ensure this is imported if type hinting or direct use\r\nfrom .squid_multiplayer_autopilot import RemoteSquidController # Ensure this for autopilot logic\r\n\r\n# TamagotchiLogic is imported in main.py and should be in sys.path\r\n# If type hinting is needed here and main.py's import might not be seen by linters:\r\ntry:\r\n    from src.tamagotchi_logic import TamagotchiLogic\r\nexcept ImportError:\r\n    TamagotchiLogic = None\r\n\r\n\r\nclass MultiplayerPlugin:\r\n    def __init__(self):\r\n        # --- Initialize logger attribute ---\r\n        self.logger = None \r\n\r\n        # --- Locks for thread safety ---\r\n        self.network_lock = threading.RLock()\r\n        self.remote_squids_lock = threading.RLock()\r\n        self.remote_objects_lock = threading.RLock()\r\n\r\n        # --- Core Components ---\r\n        self.network_node: NetworkNode | None = None\r\n        self.plugin_manager = None # Set by plugin system or during setup\r\n        self.tamagotchi_logic: TamagotchiLogic | None = None # Set during setup\r\n\r\n        # --- Timers and Threads ---\r\n        self.sync_thread: threading.Thread | None = None\r\n        self.message_process_timer: QtCore.QTimer | None = None\r\n        self.controller_update_timer: QtCore.QTimer | None = None\r\n        self.controller_creation_timer: QtCore.QTimer | None = None\r\n        self.cleanup_timer_basic: QtCore.QTimer | None = None\r\n        self.connection_timer_basic: QtCore.QTimer | None = None\r\n\r\n        # --- State and Data ---\r\n        self.remote_squids: Dict[str, Dict[str, Any]] = {}\r\n        self.remote_objects: Dict[str, Dict[str, Any]] = {}\r\n        self.remote_squid_controllers: Dict[str, Any] = {} # Should be RemoteSquidController\r\n        self.pending_controller_creations: List[Dict[str, Any]] = []\r\n        self.connection_lines: Dict[str, QtWidgets.QGraphicsLineItem] = {}\r\n        self.last_message_times: Dict[str, float] = {}\r\n\r\n        # --- Configuration ---\r\n        self.MULTICAST_GROUP = mp_constants.MULTICAST_GROUP\r\n        self.MULTICAST_PORT = mp_constants.MULTICAST_PORT\r\n        self.SYNC_INTERVAL = mp_constants.SYNC_INTERVAL\r\n        # MODIFIED for testing: Force full opacity\r\n        self.REMOTE_SQUID_OPACITY = 1.0 # Was mp_constants.REMOTE_SQUID_OPACITY\r\n        self.SHOW_REMOTE_LABELS = mp_constants.SHOW_REMOTE_LABELS\r\n        self.SHOW_CONNECTION_LINES = mp_constants.SHOW_CONNECTION_LINES\r\n\r\n        # --- UI Elements ---\r\n        self.config_dialog: QtWidgets.QDialog | None = None\r\n        self.status_widget: Any | None = None # Should be MultiplayerStatusWidget\r\n        self.status_bar: Any | None = None\r\n\r\n        # --- Flags ---\r\n        self.is_setup = False\r\n        # 1. Try to get the global flag from config.ini\r\n        try:\r\n            self.debug_mode = self.plugin_manager.config_manager.get_debug_flag(\"multiplayer_debug\", fallback=False)\r\n        except Exception:\r\n            # 2. Any problem (no ConfigManager, no key, etc.) → stay quiet\r\n            self.debug_mode = False\r\n\r\n        # 3. Push the value to every sub-component that cares\r\n        if self.network_node:\r\n            self.network_node.debug_mode = self.debug_mode\r\n        if getattr(self, 'entity_manager', None):\r\n            self.entity_manager.debug_mode = self.debug_mode\r\n        self.last_controller_update = time.time() \r\n        self.entity_manager: RemoteEntityManager | None = None # Added type hint\r\n        self.config_manager = None # Placeholder, ensure this is set if used (e.g. in handle_squid_exit_message print)\r\n\r\n    def _initialize_remote_entity_manager(self):\r\n        \"\"\"\r\n        Initializes the RemoteEntityManager instance.\r\n        This method encapsulates the logic for creating and configuring\r\n        the RemoteEntityManager.\r\n        \"\"\"\r\n        if not self.logger:\r\n            # This case should ideally not happen if logger is set up in __init__ or early setup\r\n            print(\"MPPluginLogic ERRA: Logger not available for _initialize_remote_entity_manager\")\r\n            self.entity_manager = None\r\n            return\r\n\r\n        if self.tamagotchi_logic and \\\r\n           hasattr(self.tamagotchi_logic, 'user_interface') and \\\r\n           self.tamagotchi_logic.user_interface and \\\r\n           hasattr(self.tamagotchi_logic.user_interface, 'image_cache'): # Check for image_cache\r\n\r\n            ui = self.tamagotchi_logic.user_interface\r\n            try:\r\n                self.entity_manager = RemoteEntityManager(\r\n                    scene=ui.scene,\r\n                    window_width=ui.window_width,\r\n                    window_height=ui.window_height,\r\n                    #image_cache=ui.image_cache,  # Pass the image_cache instance\r\n                    debug_mode=self.debug_mode, # self.debug_mode should be set in MpPluginLogic\r\n                    logger=self.logger.getChild(\"RemoteEntityManager\") # Pass a child logger\r\n                )\r\n                self.logger.info(\"RemoteEntityManager initialized successfully.\")\r\n            except ImportError: # Should not happen if imports are correct at file top\r\n                self.logger.error(\"RemoteEntityManager import failed during initialization. Visuals for remote entities will be basic or non-functional.\", exc_info=True)\r\n                self.entity_manager = None\r\n                # self.initialize_remote_representation() # Call your fallback if RemoteEntityManager fails\r\n            except Exception as e_rem:\r\n                self.logger.error(f\"Error initializing RemoteEntityManager: {e_rem}\", exc_info=True)\r\n                self.entity_manager = None\r\n                # self.initialize_remote_representation() # Call your fallback\r\n        else:\r\n            self.logger.warning(\"User interface, TamagotchiLogic, or ImageCache not available for RemoteEntityManager setup. Remote visuals may be limited or non-functional.\")\r\n            self.entity_manager = None\r\n            # self.initialize_remote_representation() # Call your fallback\r\n\r\n\r\n    def debug_autopilot_status(self):\r\n        \"\"\"Debug the status of all autopilot controllers for remote squids.\"\"\"\r\n        if not self.logger: # Safeguard\r\n            print(\"Multiplayer ERRA: Logger not initialized in debug_autopilot_status\")\r\n            return\r\n\r\n        if not hasattr(self, 'remote_squid_controllers') or not self.remote_squid_controllers:\r\n            self.logger.debug(\"No remote squid controllers are currently active.\")\r\n            return\r\n\r\n        self.logger.debug(f\"\\n=== AUTOPILOT DEBUG ({len(self.remote_squid_controllers)} controllers) ===\")\r\n        for node_id, controller in self.remote_squid_controllers.items():\r\n            squid_name = node_id[-6:]\r\n            self.logger.debug(f\"Squid {squid_name}:\")\r\n            self.logger.debug(f\"  State: {getattr(controller, 'state', 'N/A')}\")\r\n            squid_data = getattr(controller, 'squid_data', {})\r\n            pos_x = squid_data.get('x', 0.0)\r\n            pos_y = squid_data.get('y', 0.0)\r\n            self.logger.debug(f\"  Position: ({pos_x:.1f}, {pos_y:.1f})\")\r\n            self.logger.debug(f\"  Direction: {squid_data.get('direction', 'N/A')}\")\r\n            self.logger.debug(f\"  Home Dir: {getattr(controller, 'home_direction', 'N/A')}\")\r\n            time_away = getattr(controller, 'time_away', 0.0)\r\n            max_time = getattr(controller, 'max_time_away', 0.0)\r\n            self.logger.debug(f\"  Time Away: {time_away:.1f}s / {max_time:.1f}s\")\r\n            food_count = getattr(controller, 'food_eaten_count', 0)\r\n            rock_count = getattr(controller, 'rock_interaction_count', 0)\r\n            self.logger.debug(f\"  Activities: {food_count} food, {rock_count} rocks\")\r\n            target_obj = getattr(controller, 'target_object', None)\r\n            self.logger.debug(f\"  Target: {'Yes (' + type(target_obj).__name__ + ')' if target_obj else 'No'}\")\r\n        self.logger.debug(\"=====================================\\n\")\r\n\r\n        def enable(self):\r\n            self.logger.info(\"Attempting to enable Multiplayer...\")\r\n\r\n            # --- NEW: Debug log all key objects ---\r\n            self.logger.debug(f\"[ENABLE] status_widget: {self.status_widget}\")\r\n            self.logger.debug(f\"[ENABLE] entity_manager: {self.entity_manager}\")\r\n            self.logger.debug(f\"[ENABLE] network_node: {self.network_node}\")\r\n            self.logger.debug(f\"[ENABLE] tamagotchi_logic: {self.tamagotchi_logic}\")\r\n            self.logger.debug(f\"[ENABLE] config_manager: {getattr(self, 'config_manager', None)}\")\r\n            # --- END DEBUG LOG ---\r\n\r\n            # Plugin already set up?\r\n            if not self.is_setup:\r\n                self.logger.info(\"Multiplayer plugin is not set up. Calling setup()...\")\r\n                # Assuming self.plugin_manager and self.tamagotchi_logic_ref are available\r\n                if not self.setup(self.plugin_manager, self.tamagotchi_logic_ref): # Pass necessary args\r\n                    self.logger.error(\"Multiplayer setup failed during enable(). Cannot enable.\")\r\n                    return False\r\n            else:\r\n                self.logger.info(\"Multiplayer is already marked as set up. Re-enabling components.\")\r\n\r\n            # --- BEGIN NEW/MODIFIED SECTION ---\r\n            # Ensure network node is ready and listening\r\n            if self.network_node:\r\n                # Ensure the socket structure is initialized (it should be by NetworkNode.__init__ or a previous setup)\r\n                # but a re-check or re-init if disconnected can be robust.\r\n                if not self.network_node.is_connected:\r\n                    self.logger.info(\"NetworkNode socket not connected, attempting to initialize in enable()...\")\r\n                    if not self.network_node.initialize_socket_structure():\r\n                        self.logger.error(\"Failed to initialize NetworkNode socket in enable(). Cannot proceed with enabling multiplayer.\")\r\n                        # Potentially set self.enabled = False or similar state management\r\n                        return False # Or handle error appropriately\r\n\r\n                # Explicitly start the listener thread if it's not already active\r\n                if not self.network_node.is_listening():\r\n                    self.logger.info(\"NetworkNode listener not active, starting it explicitly in enable()...\")\r\n                    if not self.network_node.start_listening():\r\n                        self.logger.error(\"Failed to start NetworkNode listener in enable(). Multiplayer might not receive messages.\")\r\n                        # Decide if this is a fatal error for enabling or just a warning\r\n                        # For now, let's treat it as potentially non-fatal but log an error.\r\n                        # Depending on requirements, you might return False here.\r\n                    else:\r\n                        self.logger.info(\">>>>>> NetworkNode listener started successfully!\")\r\n                else:\r\n                    self.logger.info(\"NetworkNode listener was already active.\")\r\n            else:\r\n                self.logger.error(\"NetworkNode not found after setup in enable(). Cannot enable multiplayer fully.\")\r\n                # Potentially set self.enabled = False\r\n                return False # This is likely a critical failure\r\n            # --- END NEW/MODIFIED SECTION ---\r\n\r\n            # Resume original enable logic:\r\n            # For example, re-initialize UI components, timers, etc.\r\n            # Ensure any components that were disabled are re-enabled.\r\n\r\n            # Re-initialize or ensure timers are running (if they were stopped in disable)\r\n            if self.message_process_timer:\r\n                if not self.message_process_timer.isActive():\r\n                    self.message_process_timer.start(50)\r\n                    self.logger.info(\"Message processing timer restarted.\")\r\n            else:\r\n                self.logger.warning(\"message_process_timer is None in enable(). Skipping.\")\r\n\r\n            if hasattr(self, 'sync_timer') and self.sync_timer:\r\n                if not self.sync_timer.isActive():\r\n                    self.logger.info(\"Sync timer not active, starting/restarting it.\")\r\n                    self.start_sync_timer()\r\n            else:\r\n                self.logger.warning(\"sync_timer not found or is None. Skipping.\")\r\n\r\n            # Update status widget if applicable\r\n            if self.status_widget:\r\n                try:\r\n                    self.status_widget.update_status(\"Enabled\", True)\r\n                    current_ip = self.network_node.local_ip if self.network_node else \"N/A\"\r\n                    if hasattr(self.status_widget, 'set_ip_address'):\r\n                        self.status_widget.set_ip_address(current_ip)\r\n                    else:\r\n                        self.logger.warning(\"status_widget has no set_ip_address method.\")\r\n                except Exception as e:\r\n                    self.logger.error(f\"Error updating status_widget in enable(): {e}\", exc_info=True)\r\n            else:\r\n                self.logger.warning(\"status_widget is None in enable(). Skipping UI update.\")\r\n\r\n            self.enabled = True # Mark as enabled\r\n            self.logger.info(\"Multiplayer enabled successfully.\")\r\n            return True\r\n\r\n    def disable(self):\r\n        \"\"\"Disables the multiplayer plugin and cleans up resources.\"\"\"\r\n        if not self.logger: # Should be set by now\r\n             print(\"Multiplayer ERRA: Logger not set in disable()\") # Fallback to print if logger is broken\r\n             return\r\n        self.logger.info(f\"Disabling {mp_constants.PLUGIN_NAME}...\")\r\n        if self.network_node and self.network_node.is_connected:\r\n            self.network_node.send_message(\r\n                'player_leave',\r\n                {'node_id': self.network_node.node_id, 'reason': 'plugin_disabled'}\r\n            )\r\n\r\n        self.cleanup()\r\n\r\n        if self.status_widget: self.status_widget.hide()\r\n        if self.status_bar:\r\n            if hasattr(self.status_bar, 'update_network_status'): self.status_bar.update_network_status(False)\r\n            if hasattr(self.status_bar, 'update_peers_count'): self.status_bar.update_peers_count(0)\r\n\r\n        self.logger.info(f\"{mp_constants.PLUGIN_NAME} disabled.\")\r\n\r\n    def setup(self, plugin_manager_instance, tamagotchi_logic_instance):\r\n        \"\"\"\r\n        Sets up the multiplayer plugin. Called when the plugin is first loaded or enabled.\r\n        Args:\r\n            plugin_manager_instance: A reference to the main plugin manager.\r\n            tamagotchi_logic_instance: A reference to the core tamagotchi logic.\r\n        \"\"\"\r\n        self.plugin_manager = plugin_manager_instance\r\n\r\n        # --- BEGIN LOGGER INITIALIZATION ---\r\n        if hasattr(self.plugin_manager, 'logger') and self.plugin_manager.logger is not None:\r\n            self.logger = self.plugin_manager.logger.getChild(mp_constants.PLUGIN_NAME) # Get a child logger is good practice\r\n        else:\r\n            logger_name = f\"{mp_constants.PLUGIN_NAME}_Plugin\"\r\n            self.logger = logging.getLogger(logger_name)\r\n            if not self.logger.hasHandlers():\r\n                handler = logging.StreamHandler()\r\n                formatter = logging.Formatter('%(asctime)s - %(name)s - [%(levelname)s] - %(message)s')\r\n                handler.setFormatter(formatter)\r\n                self.logger.addHandler(handler)\r\n            # Initial level, might be changed by debug_mode later\r\n            self.logger.setLevel(logging.INFO) \r\n            self.logger.warning(\r\n                \"PluginManager did not provide a valid logger. Using fallback logger.\"\r\n            )\r\n        \r\n        self.logger.info(f\"Initializing setup for {mp_constants.PLUGIN_NAME}...\")\r\n        # --- END LOGGER INITIALIZATION ---\r\n        \r\n        # Attempt to get config_manager from plugin_manager if available (for the print in handle_squid_exit)\r\n        if hasattr(plugin_manager_instance, 'config_manager'):\r\n             self.config_manager = plugin_manager_instance.config_manager\r\n        elif hasattr(tamagotchi_logic_instance, 'config_manager'): # Check on tamagotchi_logic as well\r\n             self.config_manager = tamagotchi_logic_instance.config_manager\r\n        else:\r\n             self.logger.warning(\"ConfigManager not found for debug print in handle_squid_exit_message.\")\r\n             # Create a dummy if needed for the print to not error out, or handle it in the print\r\n             class DummyConfigManager:\r\n                 def get_node_id(self): return \"UnknownNodeID_Setup\" # Differentiate if needed\r\n             if not hasattr(self, 'config_manager') or self.config_manager is None : # Set only if not already set\r\n                self.config_manager = DummyConfigManager()\r\n\r\n\r\n        self.tamagotchi_logic = tamagotchi_logic_instance\r\n\r\n        if not TamagotchiLogic: # Class itself\r\n            self.logger.critical(\"TamagotchiLogic module was not loaded (import failed). Cannot complete setup.\")\r\n            return False\r\n\r\n        if self.tamagotchi_logic is None: # Instance\r\n            self.logger.warning(\"TamagotchiLogic instance was not directly passed or was None. Attempting to find it via PluginManager.\")\r\n            if hasattr(self.plugin_manager, 'core_game_logic'):\r\n                self.tamagotchi_logic = self.plugin_manager.core_game_logic\r\n            elif hasattr(self.plugin_manager, 'tamagotchi_logic'): # Common attribute name\r\n                self.tamagotchi_logic = self.plugin_manager.tamagotchi_logic\r\n            else: # Fallback deep search\r\n                self.tamagotchi_logic = self._find_tamagotchi_logic(self.plugin_manager)\r\n\r\n        if not self.tamagotchi_logic:\r\n            self.logger.critical(\"TamagotchiLogic instance not found. Plugin functionality will be severely limited.\")\r\n            # return False # Decided to proceed with limited functionality if UI parts are missing\r\n        else:\r\n            self.debug_mode = getattr(self.tamagotchi_logic, 'debug_mode', False)\r\n            if self.logger and hasattr(self.logger, 'setLevel'): # Make sure logger has setLevel\r\n                 self.logger.setLevel(logging.DEBUG if self.debug_mode else logging.INFO)\r\n            self.logger.info(f\"TamagotchiLogic instance found. Debug mode: {self.debug_mode}\")\r\n\r\n        node_id_val = f\"squid_{uuid.uuid4().hex[:6]}\"\r\n        self.network_node = NetworkNode(node_id_val, logger=self.logger)\r\n        self.network_node.debug_mode = self.debug_mode # Pass debug mode to network node\r\n        \r\n        if self.tamagotchi_logic: # Ensure tamagotchi_logic exists before setting attribute\r\n            setattr(self.tamagotchi_logic, 'multiplayer_network_node', self.network_node)\r\n\r\n        if not self.message_process_timer:\r\n            self.message_process_timer = QtCore.QTimer()\r\n            self.message_process_timer.timeout.connect(self._process_network_node_queue)\r\n            self.message_process_timer.start(50) # Process queue every 50ms\r\n\r\n        if not self.controller_update_timer:\r\n            self.controller_update_timer = QtCore.QTimer()\r\n            self.controller_update_timer.timeout.connect(self.update_remote_controllers)\r\n            self.controller_update_timer.start(50) # Update controllers every 50ms\r\n\r\n        if not self.controller_creation_timer:\r\n             self._setup_controller_creation_timer() # For deferred controller creation\r\n\r\n        self._register_hooks() # Register message handlers\r\n\r\n        # Clear previous state\r\n        self.remote_squids.clear()\r\n        self.remote_objects.clear()\r\n        self.connection_lines.clear()\r\n        self.remote_squid_controllers.clear()\r\n        self.last_controller_update = time.time()\r\n\r\n\r\n        # === MODIFIED RemoteEntityManager Instantiation Block START ===\r\n        if self.tamagotchi_logic and \\\r\n           hasattr(self.tamagotchi_logic, 'user_interface') and \\\r\n           self.tamagotchi_logic.user_interface:\r\n\r\n            ui = self.tamagotchi_logic.user_interface # This is the GameWindow instance\r\n\r\n            # Removed check for ui.image_cache as it's no longer passed or needed by RemoteEntityManager\r\n            try:\r\n                # RemoteEntityManager should be imported at the top of mp_plugin_logic.py\r\n                self.entity_manager = RemoteEntityManager(\r\n                    scene=ui.scene,\r\n                    window_width=ui.window_width,\r\n                    window_height=ui.window_height,\r\n                    # image_cache=ui.image_cache,  # <<< THIS LINE IS REMOVED\r\n                    debug_mode=self.debug_mode, # Correct: Pass the debug_mode boolean\r\n                    logger=self.logger.getChild(\"RemoteEntityManager\") # Good practice for logger\r\n                )\r\n                self.logger.info(\"RemoteEntityManager initialized.\")\r\n            except ImportError: # Should be caught if RemoteEntityManager isn't imported\r\n                self.logger.error(\"RemoteEntityManager class import failed. Visuals for remote entities will be basic or non-functional.\", exc_info=True)\r\n                self.entity_manager = None\r\n                self.initialize_remote_representation() # Your fallback\r\n            except Exception as e_rem:\r\n                self.logger.error(f\"Error initializing RemoteEntityManager: {e_rem}\", exc_info=True)\r\n                self.entity_manager = None\r\n                self.initialize_remote_representation() # Your fallback\r\n        else:\r\n            self.logger.warning(\"User interface or TamagotchiLogic not available for RemoteEntityManager setup. Remote visuals may be limited.\")\r\n            self.entity_manager = None\r\n            self.initialize_remote_representation() # Your fallback\r\n        # === MODIFIED RemoteEntityManager Instantiation Block END ===\r\n\r\n        self.initialize_status_ui() # Initialize status widget or bar\r\n\r\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'show_message') and self.network_node:\r\n            self.tamagotchi_logic.show_message(f\"Multiplayer active! Node ID: {self.network_node.node_id}\")\r\n\r\n        node_ip = self.network_node.local_ip if self.network_node else \"N/A\"\r\n        node_port = self.MULTICAST_PORT # Use the constant\r\n        self.logger.info(f\"Setup complete. Node: {node_id_val} on IP: {node_ip}. Listening for multicast on port: {node_port}\")\r\n        self.is_setup = True\r\n        return True\r\n\r\n    def _process_network_node_queue(self, **kwargs):\r\n        \"\"\"Called by a QTimer to process messages from the NetworkNode's incoming_queue.\"\"\"\r\n        if not self.logger: return\r\n        if self.network_node and self.plugin_manager:\r\n            try:\r\n                self.network_node.process_messages(self.plugin_manager)\r\n            except Exception as e:\r\n                if self.debug_mode:\r\n                    self.logger.error(f\"Error in _process_network_node_queue: {e}\", exc_info=True)\r\n\r\n    def setup_minimal_network(self):\r\n        \"\"\"(Helper) Creates a basic network interface if one is required but not found.\"\"\"\r\n        if not self.logger: return\r\n        self.logger.info(\"Setting up minimal network interface...\")\r\n        class MinimalNetworkInterface:\r\n            def create_socket(self, socket_type='udp'):\r\n                import socket\r\n                return socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\r\n        if self.plugin_manager:\r\n            self.plugin_manager.plugins['network_interface'] = {\r\n                'instance': MinimalNetworkInterface(), 'name': 'Minimal Network Interface', 'version': '0.1'\r\n            }\r\n            self.logger.info(\"Minimal network interface registered.\")\r\n\r\n    def update_remote_squid_image(self, remote_squid_display_data: Dict, direction: str):\r\n        \"\"\"Updates the visual image of a remote squid based on its direction.\"\"\"\r\n        if not self.logger: return False\r\n        visual_item = remote_squid_display_data.get('visual')\r\n        if not visual_item or not isinstance(visual_item, QtWidgets.QGraphicsPixmapItem):\r\n            return False\r\n        try:\r\n            base_image_path = \"images\"\r\n            squid_image_file = f\"{direction.lower()}1.png\"\r\n            full_image_path = os.path.join(base_image_path, squid_image_file)\r\n            squid_pixmap = QtGui.QPixmap(full_image_path)\r\n            if squid_pixmap.isNull():\r\n                fallback_path = os.path.join(base_image_path, \"right1.png\") # Default fallback\r\n                squid_pixmap = QtGui.QPixmap(fallback_path)\r\n                if squid_pixmap.isNull() and self.debug_mode:\r\n                    self.logger.warning(f\"Could not load squid image '{full_image_path}' or fallback '{fallback_path}'.\")\r\n                    # Create a colored square as an ultimate fallback if even \"right1.png\" fails\r\n                    squid_pixmap = QtGui.QPixmap(60,40) # Default size\r\n                    squid_color = remote_squid_display_data.get('data',{}).get('color',(128,128,128))\r\n                    squid_pixmap.fill(QtGui.QColor(*squid_color))\r\n\r\n            visual_item.setPixmap(squid_pixmap)\r\n            return True\r\n        except Exception as e:\r\n            if self.debug_mode:\r\n                self.logger.error(f\"Error updating remote squid image for direction '{direction}': {e}\", exc_info=True)\r\n            return False\r\n\r\n    def handle_squid_interaction(self, local_squid, remote_node_id, remote_squid_data):\r\n        \"\"\"Handles interactions between the local squid and a detected remote squid.\"\"\"\r\n        if not self.logger: return\r\n        if not local_squid or not remote_squid_data or not self.tamagotchi_logic: return\r\n        local_pos = (local_squid.squid_x, local_squid.squid_y)\r\n        remote_pos = (remote_squid_data.get('x',0.0), remote_squid_data.get('y',0.0))\r\n        distance = math.hypot(local_pos[0] - remote_pos[0], local_pos[1] - remote_pos[1])\r\n        interaction_distance_threshold = 80 # Example threshold\r\n        if distance < interaction_distance_threshold:\r\n            if hasattr(local_squid, 'memory_manager') and hasattr(local_squid.memory_manager, 'add_short_term_memory'):\r\n                local_squid.memory_manager.add_short_term_memory(\r\n                    category='social', event_type='squid_meeting',\r\n                    description=f\"Met squid {remote_node_id[-6:]} from another tank.\",\r\n                    importance=5\r\n                )\r\n            self.attempt_gift_exchange(local_squid, remote_node_id)\r\n\r\n    def attempt_gift_exchange(self, local_squid, remote_node_id: str):\r\n        \"\"\"Allows squids to exchange a random decoration item if conditions are met.\"\"\"\r\n        if not self.logger: return False\r\n        if random.random() > 0.15: return False # 15% chance\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return False\r\n        ui = self.tamagotchi_logic.user_interface\r\n        local_decorations = [\r\n            item for item in ui.scene.items()\r\n            if isinstance(item, QtWidgets.QGraphicsPixmapItem) and\r\n               getattr(item, 'category', '') == 'decoration' and\r\n               item.isVisible() and not getattr(item, 'is_foreign', False) and # Not already from remote\r\n               not getattr(item, 'is_gift_from_remote', False) # Not a gift they received\r\n        ]\r\n        if not local_decorations: return False\r\n        gift_to_send_away = random.choice(local_decorations)\r\n        \r\n        # Simulate receiving a gift (create a new decoration)\r\n        received_gift_item = self.create_gift_decoration(remote_node_id)\r\n        \r\n        if received_gift_item: # If a gift was successfully created for the local scene\r\n            # Make the local squid's chosen decoration disappear (sent away)\r\n            gift_to_send_away.setVisible(False)\r\n            # Optionally, remove it from the scene after a delay or permanently\r\n            QtCore.QTimer.singleShot(15000, lambda item=gift_to_send_away: self._remove_gifted_item_from_scene(item))\r\n\r\n            if hasattr(local_squid, 'memory_manager'):\r\n                local_squid.memory_manager.add_short_term_memory(\r\n                    'social', 'decoration_exchange',\r\n                    f\"Exchanged decorations with squid {remote_node_id[-6:]}!\", importance=7\r\n                )\r\n            if hasattr(self.tamagotchi_logic, 'show_message'):\r\n                self.tamagotchi_logic.show_message(f\"🎁 Your squid exchanged gifts with {remote_node_id[-6:]}!\")\r\n            return True\r\n        return False\r\n\r\n    def _remove_gifted_item_from_scene(self, item_to_remove):\r\n        \"\"\"Safely removes an item from the scene if it's still present.\"\"\"\r\n        if not self.logger: return\r\n        if item_to_remove and self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface'):\r\n            scene = self.tamagotchi_logic.user_interface.scene\r\n            if item_to_remove in scene.items():\r\n                scene.removeItem(item_to_remove)\r\n                if self.debug_mode: self.logger.debug(f\"Removed gifted item '{getattr(item_to_remove,'filename','N/A')}' from scene.\")\r\n\r\n    def create_stolen_rocks(self, local_squid, num_rocks: int, entry_position: tuple):\r\n        \"\"\"Creates rock items in the local scene, representing rocks 'stolen' by the local squid from a remote tank.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface') or num_rocks <= 0:\r\n            return\r\n        ui = self.tamagotchi_logic.user_interface\r\n        scene = ui.scene\r\n        rock_image_files = []\r\n        search_paths = [os.path.join(\"images\", \"decoration\"), \"images\"] # Common paths for rocks\r\n        for path in search_paths:\r\n            if os.path.exists(path):\r\n                for filename in os.listdir(path):\r\n                    if 'rock' in filename.lower() and filename.lower().endswith(('.png', '.jpg')):\r\n                        rock_image_files.append(os.path.join(path, filename))\r\n        \r\n        if not rock_image_files: # Fallback if no specific rocks found\r\n            rock_image_files.append(os.path.join(\"images\", \"rock.png\")) # Assume a default rock image\r\n\r\n        entry_x, entry_y = entry_position\r\n        for i in range(num_rocks):\r\n            try:\r\n                chosen_rock_file = random.choice(rock_image_files)\r\n                # Scatter rocks around the entry point\r\n                angle_offset = random.uniform(-math.pi / 4, math.pi / 4) \r\n                angle = (i * (2 * math.pi / num_rocks)) + angle_offset \r\n                dist = random.uniform(60, 100) # Distance from entry point\r\n                rock_x = entry_x + dist * math.cos(angle)\r\n                rock_y = entry_y + dist * math.sin(angle)\r\n\r\n                rock_pixmap = QtGui.QPixmap(chosen_rock_file)\r\n                if rock_pixmap.isNull(): continue # Skip if image fails to load\r\n\r\n                rock_graphics_item = None\r\n                if hasattr(ui, 'ResizablePixmapItem'): # Check if custom item class exists\r\n                    rock_graphics_item = ui.ResizablePixmapItem(rock_pixmap, chosen_rock_file)\r\n                else:\r\n                    rock_graphics_item = QtWidgets.QGraphicsPixmapItem(rock_pixmap)\r\n                    setattr(rock_graphics_item, 'filename', chosen_rock_file) # Store filename if not ResizablePixmapItem\r\n                \r\n                setattr(rock_graphics_item, 'category', 'rock')\r\n                setattr(rock_graphics_item, 'can_be_picked_up', True)\r\n                setattr(rock_graphics_item, 'is_stolen_from_remote', True)\r\n                setattr(rock_graphics_item, 'is_foreign', True) # Mark as foreign for tinting\r\n                rock_graphics_item.setPos(rock_x, rock_y)\r\n                scene.addItem(rock_graphics_item)\r\n                self.apply_foreign_object_tint(rock_graphics_item) # Apply a visual tint\r\n\r\n                # Simple fade-in for stolen rocks (no complex animation for static testing)\r\n                rock_graphics_item.setOpacity(0.2) # Start slightly transparent\r\n                # Removed QPropertyAnimation for opacity for static testing\r\n                rock_graphics_item.setOpacity(1.0) # Make fully visible immediately\r\n\r\n\r\n            except Exception as e:\r\n                if self.debug_mode: self.logger.error(f\"Error creating stolen rock visuals: {e}\", exc_info=True)\r\n        \r\n        if hasattr(local_squid, 'memory_manager'):\r\n            local_squid.memory_manager.add_short_term_memory(\r\n                'achievement', 'rock_heist',\r\n                f\"Brought back {num_rocks} rocks from an adventure!\", importance=8\r\n            )\r\n\r\n    def apply_foreign_object_tint(self, q_graphics_item: QtWidgets.QGraphicsPixmapItem):\r\n        \"\"\"Applies a visual tint to indicate an object is from a remote instance.\"\"\"\r\n        if not self.logger: return\r\n        if not isinstance(q_graphics_item, QtWidgets.QGraphicsPixmapItem): return\r\n        \r\n        existing_effect = q_graphics_item.graphicsEffect()\r\n        if isinstance(existing_effect, QtWidgets.QGraphicsColorizeEffect):\r\n            # Update existing effect if it's already the right type\r\n            existing_effect.setColor(QtGui.QColor(255, 120, 120, 200)) # Tint color (e.g., reddish)\r\n            existing_effect.setStrength(0.3) # Tint strength\r\n        else:\r\n            # Create and apply new effect\r\n            colorize_effect = QtWidgets.QGraphicsColorizeEffect()\r\n            colorize_effect.setColor(QtGui.QColor(255, 120, 120, 200)) \r\n            colorize_effect.setStrength(0.3)\r\n            q_graphics_item.setGraphicsEffect(colorize_effect)\r\n        setattr(q_graphics_item, 'is_foreign', True) # Ensure flag is set\r\n\r\n\r\n    def show_network_dashboard(self):\r\n        \"\"\"Displays a dialog with detailed network status and peer information.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface') or not self.network_node:\r\n            self.logger.warning(\"Cannot show network dashboard - UI or NetworkNode missing.\")\r\n            return\r\n\r\n        parent_window = self.tamagotchi_logic.user_interface.window\r\n        dashboard_dialog = QtWidgets.QDialog(parent_window)\r\n        dashboard_dialog.setWindowTitle(\"Multiplayer Network Dashboard\")\r\n        dashboard_dialog.setMinimumSize(550, 450)\r\n\r\n        main_layout = QtWidgets.QVBoxLayout(dashboard_dialog)\r\n\r\n        # Connection Info Group\r\n        conn_info_group = QtWidgets.QGroupBox(\"My Connection\")\r\n        conn_info_form = QtWidgets.QFormLayout(conn_info_group)\r\n        node_id_label = QtWidgets.QLabel(self.network_node.node_id)\r\n        ip_label = QtWidgets.QLabel(self.network_node.local_ip)\r\n        status_val_label = QtWidgets.QLabel() # Will be updated\r\n        conn_info_form.addRow(\"Node ID:\", node_id_label)\r\n        conn_info_form.addRow(\"Local IP:\", ip_label)\r\n        conn_info_form.addRow(\"Status:\", status_val_label)\r\n        main_layout.addWidget(conn_info_group)\r\n\r\n        # Peers Group\r\n        peers_group = QtWidgets.QGroupBox(\"Detected Peers\")\r\n        peers_layout = QtWidgets.QVBoxLayout(peers_group)\r\n        peers_table_widget = QtWidgets.QTableWidget()\r\n        peers_table_widget.setColumnCount(4)\r\n        peers_table_widget.setHorizontalHeaderLabels([\"Node ID\", \"IP Address\", \"Last Seen\", \"Status\"])\r\n        peers_table_widget.horizontalHeader().setStretchLastSection(True)\r\n        peers_table_widget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\r\n        peers_table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\r\n        peers_layout.addWidget(peers_table_widget)\r\n        main_layout.addWidget(peers_group)\r\n        \r\n        # Network Stats Group (Conceptual)\r\n        stats_group = QtWidgets.QGroupBox(\"Network Statistics (Conceptual)\")\r\n        stats_form = QtWidgets.QFormLayout(stats_group)\r\n        stats_form.addRow(\"Messages Sent (Total):\", QtWidgets.QLabel(str(getattr(self.network_node, 'total_sent_count', 'N/A'))))\r\n        stats_form.addRow(\"Messages Received (Total):\", QtWidgets.QLabel(str(getattr(self.network_node, 'total_received_count', 'N/A'))))\r\n        main_layout.addWidget(stats_group)\r\n\r\n\r\n        def refresh_dashboard_data():\r\n            # Update connection status\r\n            is_connected = self.network_node.is_connected\r\n            status_val_label.setText(\"Connected\" if is_connected else \"Disconnected\")\r\n            status_val_label.setStyleSheet(\"color: green; font-weight: bold;\" if is_connected else \"color: red; font-weight: bold;\")\r\n\r\n            # Update peers table\r\n            peers_table_widget.setRowCount(0) # Clear table\r\n            if self.network_node: # Check if network_node still exists\r\n                for row, (node_id, (ip, last_seen, _)) in enumerate(self.network_node.known_nodes.items()):\r\n                    peers_table_widget.insertRow(row)\r\n                    peers_table_widget.setItem(row, 0, QtWidgets.QTableWidgetItem(node_id))\r\n                    peers_table_widget.setItem(row, 1, QtWidgets.QTableWidgetItem(ip))\r\n                    time_delta_secs = time.time() - last_seen\r\n                    time_ago_str = f\"{int(time_delta_secs)}s ago\"\r\n                    peers_table_widget.setItem(row, 2, QtWidgets.QTableWidgetItem(time_ago_str))\r\n                    \r\n                    peer_status_str = \"Active\" if time_delta_secs < 20 else \"Inactive\" # Example threshold\r\n                    status_cell_item = QtWidgets.QTableWidgetItem(peer_status_str)\r\n                    status_cell_item.setForeground(QtGui.QBrush(QtGui.QColor(\"green\" if peer_status_str == \"Active\" else \"gray\")))\r\n                    peers_table_widget.setItem(row, 3, status_cell_item)\r\n            peers_table_widget.resizeColumnsToContents()\r\n        \r\n        refresh_dashboard_data() # Initial population\r\n\r\n        # Buttons\r\n        button_box = QtWidgets.QDialogButtonBox()\r\n        refresh_btn = button_box.addButton(\"Refresh\", QtWidgets.QDialogButtonBox.ActionRole)\r\n        close_btn = button_box.addButton(QtWidgets.QDialogButtonBox.Close)\r\n        refresh_btn.clicked.connect(refresh_dashboard_data)\r\n        close_btn.clicked.connect(dashboard_dialog.accept)\r\n        main_layout.addWidget(button_box)\r\n\r\n        dashboard_dialog.exec_()\r\n\r\n\r\n    def initialize_status_ui(self):\r\n        \"\"\"Initializes UI components for displaying multiplayer status.\"\"\"\r\n        if not self.logger: return\r\n        try:\r\n            if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'):\r\n                self.logger.warning(\"Status UI cannot be initialized - user_interface not found.\")\r\n                return\r\n\r\n            ui = self.tamagotchi_logic.user_interface\r\n            try:\r\n                from plugins.multiplayer.multiplayer_status_widget import MultiplayerStatusWidget\r\n                if not hasattr(ui, '_mp_status_widget_instance_'): # Create if not exists\r\n                    ui._mp_status_widget_instance_ = MultiplayerStatusWidget(ui.window)\r\n                    # Position the widget (example: top-right corner)\r\n                    ui._mp_status_widget_instance_.move(\r\n                        ui.window.width() - ui._mp_status_widget_instance_.width() - 15, 15\r\n                    )\r\n                    ui._mp_status_widget_instance_.hide() # Initially hidden\r\n                \r\n                self.status_widget = ui._mp_status_widget_instance_\r\n                if self.network_node and hasattr(self.status_widget, 'set_network_node_reference'):\r\n                    self.status_widget.set_network_node_reference(self.network_node)\r\n                self.logger.info(\"Dedicated status widget initialized.\")\r\n\r\n            except ImportError:\r\n                self.logger.info(\"MultiplayerStatusWidget not found. Will attempt fallback status bar integration.\")\r\n                self.initialize_status_bar() # Fallback\r\n            except Exception as e_msw:\r\n                self.logger.error(f\"Error initializing MultiplayerStatusWidget: {e_msw}. Using fallback.\", exc_info=True)\r\n                self.initialize_status_bar() # Fallback\r\n\r\n        except Exception as e:\r\n            self.logger.error(f\"Could not initialize status UI: {e}\", exc_info=True)\r\n    \r\n    def initialize_status_bar(self): # Added stub, assuming it was missing\r\n        \"\"\"Fallback to initialize status bar component if dedicated widget fails.\"\"\"\r\n        if not self.logger: return\r\n        self.logger.info(\"Attempting to initialize fallback status bar component.\")\r\n        # Implementation for status_bar initialization would go here\r\n        # For now, just log that it's a fallback\r\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface') and \\\r\n           hasattr(self.tamagotchi_logic.user_interface, 'statusBar'): # Example access\r\n            self.status_bar = self.tamagotchi_logic.user_interface.statusBar()\r\n            # Example: Add a permanent widget or label to the status bar\r\n            # if self.status_bar and not hasattr(self, '_mp_status_label_basic'):\r\n            #     self._mp_status_label_basic = QtWidgets.QLabel(\"MP: Disconnected\")\r\n            #     self.status_bar.addPermanentWidget(self._mp_status_label_basic)\r\n            self.logger.info(\"Fallback status bar component obtained.\")\r\n        else:\r\n            self.logger.warning(\"Fallback status bar component could not be obtained.\")\r\n\r\n\r\n    def _find_tamagotchi_logic(self, search_object, depth=0, visited_ids=None):\r\n        \"\"\"Recursively searches for an attribute named 'tamagotchi_logic'.\"\"\"\r\n        if not self.logger: return None # Should not happen if called after setup\r\n        if visited_ids is None: visited_ids = set()\r\n        \r\n        if id(search_object) in visited_ids or depth > 6: # Prevent deep recursion / cycles\r\n            return None\r\n        visited_ids.add(id(search_object))\r\n\r\n        # Direct check: Is the search_object itself the TamagotchiLogic instance?\r\n        if TamagotchiLogic and isinstance(search_object, TamagotchiLogic):\r\n            return search_object\r\n        \r\n        # Check for a 'tamagotchi_logic' attribute specifically\r\n        if hasattr(search_object, 'tamagotchi_logic'):\r\n            tl_attr = getattr(search_object, 'tamagotchi_logic')\r\n            if TamagotchiLogic and isinstance(tl_attr, TamagotchiLogic):\r\n                return tl_attr\r\n        \r\n        # Iterate through attributes if it's not a basic type or module\r\n        try:\r\n            for attr_name in dir(search_object):\r\n                if attr_name.startswith('_'): continue # Skip private/magic attributes\r\n\r\n                try:\r\n                    attr_value = getattr(search_object, attr_name)\r\n                    \r\n                    # Avoid recursing into very common or problematic types\r\n                    if attr_value is None or isinstance(attr_value, (int, str, bool, float, list, dict, set, tuple, bytes)):\r\n                        continue\r\n                    if inspect.ismodule(attr_value) or inspect.isbuiltin(attr_value) or inspect.isroutine(attr_value):\r\n                        continue\r\n                    # Be careful with Qt parent traversals to avoid excessive depth or cycles\r\n                    if isinstance(attr_value, (QtWidgets.QWidget, QtCore.QObject)):\r\n                         if depth > 2 and attr_name in ['parent', 'parentWidget', 'parentItem']: # Limit depth for parent attributes\r\n                            continue\r\n                            \r\n                    found_logic = self._find_tamagotchi_logic(attr_value, depth + 1, visited_ids)\r\n                    if found_logic: return found_logic\r\n                except (AttributeError, RecursionError, TypeError, ReferenceError): # Catch errors during getattr or recursion\r\n                    continue\r\n        except (RecursionError, TypeError, ReferenceError): # Catch errors from dir() or initial checks\r\n            pass # Could happen with problematic objects\r\n            \r\n        return None\r\n\r\n    def _animate_remote_squid_entry(self, squid_graphics_item, status_text_item, entry_direction_str):\r\n        \"\"\"MODIFIED: Animates (or rather, just shows) the visual entry of a remote squid.\"\"\"\r\n        if not self.logger: return\r\n        if not squid_graphics_item: return\r\n\r\n        # Directly set opacity to full (or the plugin's setting)\r\n        squid_graphics_item.setOpacity(self.REMOTE_SQUID_OPACITY) # REMOTE_SQUID_OPACITY is 1.0 for testing\r\n        squid_graphics_item.setScale(1.0) # Ensure normal scale\r\n\r\n        if squid_graphics_item.graphicsEffect(): # Remove any prior effect\r\n            squid_graphics_item.setGraphicsEffect(None)\r\n\r\n        if status_text_item:\r\n            status_text_item.setOpacity(1.0) # Make status text fully visible\r\n            if status_text_item.graphicsEffect(): # Remove any prior effect\r\n                status_text_item.setGraphicsEffect(None)\r\n        \r\n        if self.debug_mode:\r\n            self.logger.debug(f\"Static display for remote squid entry: {entry_direction_str}\")\r\n\r\n\r\n    def get_opposite_direction(self, direction_str: str) -> str:\r\n        \"\"\"Returns the opposite of a given cardinal direction string.\"\"\"\r\n        opposites = {'left': 'right', 'right': 'left', 'up': 'down', 'down': 'up'}\r\n        return opposites.get(direction_str.lower(), 'right') # Default if unknown\r\n\r\n    def create_entry_effect(self, center_x: float, center_y: float, direction_str: str = \"\"):\r\n        \"\"\"Creates a visual effect at the point where a remote squid enters the scene.\r\n           MODIFIED: This effect will be static or very simple for testing.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n\r\n        # Simplified static indicator (e.g., a small, briefly visible circle or text)\r\n        # For testing, we can even skip this visual flourish if the goal is just to see the squid.\r\n        if self.debug_mode:\r\n            self.logger.debug(f\"Skipping dynamic entry effect for static testing at ({center_x},{center_y}).\")\r\n        \r\n        # If you still want a minimal static cue:\r\n        # arrival_text_str = \"New Visitor\"\r\n        # arrival_text_item = scene.addText(arrival_text_str)\r\n        # arrival_font = QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold)\r\n        # arrival_text_item.setFont(arrival_font)\r\n        # text_metrics = QtGui.QFontMetrics(arrival_font)\r\n        # text_rect = text_metrics.boundingRect(arrival_text_str)\r\n        # arrival_text_item.setDefaultTextColor(QtGui.QColor(255, 215, 0)) # Gold\r\n        # arrival_text_item.setPos(center_x - text_rect.width() / 2, center_y - 60) # Position above entry\r\n        # arrival_text_item.setZValue(100)\r\n        # arrival_text_item.setVisible(True)\r\n        # # Timer to remove it after a short period if desired, or let it persist.\r\n        # QtCore.QTimer.singleShot(3000, lambda: scene.removeItem(arrival_text_item) if arrival_text_item.scene() else None)\r\n\r\n    def _setup_controller_immediately(self, node_id: str, squid_initial_data: Dict):\r\n        \"\"\"Creates and initializes a RemoteSquidController.\"\"\"\r\n        if not self.logger: return\r\n        try:\r\n            # from plugins.multiplayer.squid_multiplayer_autopilot import RemoteSquidController # Already at top\r\n            pass # Ensure it's imported\r\n        except ImportError:\r\n            if self.debug_mode: self.logger.error(\"RemoteSquidController module not found. Remote squids will not be autonomous.\")\r\n            return\r\n\r\n        if node_id in self.remote_squid_controllers:\r\n            if self.debug_mode: self.logger.debug(f\"Controller for squid {node_id[-6:]} already exists. Updating its data.\")\r\n            # Ensure squid_data includes x,y if updating position via controller\r\n            self.remote_squid_controllers[node_id].squid_data.update(squid_initial_data)\r\n            # Potentially trigger a state re-evaluation in the controller if data changed significantly\r\n            # self.remote_squid_controllers[node_id].evaluate_state() # If such a method exists\r\n            return\r\n\r\n        if self.debug_mode: self.logger.info(f\"Creating autopilot controller for remote squid {node_id[-6:]}.\")\r\n        try:\r\n            controller_instance = RemoteSquidController(\r\n                squid_data=squid_initial_data, # This now contains x,y from entry_details if available\r\n                scene=self.tamagotchi_logic.user_interface.scene,\r\n                plugin_instance=self, # Pass self (MultiplayerPlugin instance)\r\n                debug_mode=self.debug_mode,\r\n                remote_entity_manager=self.entity_manager # Pass the entity manager\r\n            )\r\n            self.remote_squid_controllers[node_id] = controller_instance\r\n            if self.debug_mode: self.logger.info(f\"Controller for {node_id[-6:]} created. Initial state: {getattr(controller_instance, 'state', 'N/A')}\")\r\n        except Exception as e_create:\r\n            self.logger.error(f\"Failed to create RemoteSquidController for {node_id[-6:]}: {e_create}\", exc_info=True)\r\n            return\r\n            \r\n        # Ensure the controller update timer is running\r\n        if self.controller_update_timer and not self.controller_update_timer.isActive():\r\n            self.controller_update_timer.start()\r\n            if self.debug_mode: self.logger.debug(\"Restarted controller update timer.\")\r\n\r\n\r\n    def handle_squid_exit_message(self, node: Any, message: Dict, addr: tuple):\r\n        \"\"\"\r\n        We just received a 'squid_exit' multicast.\r\n        Draw the giant exit-arrow and then create / update the remote squid visual.\r\n        \"\"\"\r\n        # ----- basic sanity / logging -----\r\n        node_id_for_print = (getattr(self, 'config_manager', None) and\r\n                            getattr(self.config_manager, 'get_node_id', None) and\r\n                            self.config_manager.get_node_id()) or \\\r\n                            (self.network_node and self.network_node.node_id) or \"UnknownNode_HandleExit\"\r\n\r\n        if self.debug_mode:\r\n            print(f\"DEBUG_STEP_1: Node {node_id_for_print} - handle_squid_exit_message CALLED. \"\r\n                f\"Message type: {message.get('type')}\")\r\n\r\n        if not self.logger:\r\n            print(\"MPPluginLogic ERRA: Logger not available in handle_squid_exit_message\")\r\n            return False\r\n\r\n        try:\r\n            self.logger.info(f\"MY NODE ID {self.network_node.node_id if self.network_node else 'Unknown'} - \"\r\n                            f\"Received squid_exit message: {message} from {addr}\")\r\n\r\n            exit_payload_outer = message.get('payload', {})\r\n            exit_payload_inner = exit_payload_outer.get('payload')  # actual exit_data\r\n            if not exit_payload_inner:\r\n                self.logger.warning(\"squid_exit message missing nested 'payload' key.\")\r\n                return False\r\n\r\n            source_node_id = exit_payload_inner.get('node_id')\r\n            if not source_node_id:\r\n                self.logger.warning(\"squid_exit inner payload missing 'node_id'.\")\r\n                return False\r\n\r\n            # ignore our own broadcast\r\n            if self.network_node and source_node_id == self.network_node.node_id:\r\n                self.logger.debug(f\"Ignoring own squid_exit broadcast for {source_node_id}.\")\r\n                return False\r\n\r\n            # ========== NEW: SHOW THE BIG EXIT ARROW ==========\r\n            exit_dir = exit_payload_inner.get('direction', 'right')\r\n            if self.debug_mode:\r\n                self.logger.debug(f\"Showing exit arrow for direction: {exit_dir}\")\r\n            self._show_exit_arrow(exit_dir)\r\n            # ===================================================\r\n\r\n            # create / update the remote squid visual\r\n            if self.entity_manager:\r\n                update_success = self.entity_manager.update_remote_squid(\r\n                    source_node_id, exit_payload_inner, is_new_arrival=True\r\n                )\r\n                if update_success and source_node_id not in self.remote_squid_controllers:\r\n                    self.logger.info(f\"Creating new autopilot for remote squid {source_node_id}\")\r\n                    entry_details = self.entity_manager.get_last_calculated_entry_details(source_node_id)\r\n                    initial_data = exit_payload_inner.copy()\r\n                    if entry_details:\r\n                        initial_data['x'], initial_data['y'] = entry_details['entry_pos']\r\n                        initial_data['entry_direction_on_this_screen'] = entry_details['entry_direction']\r\n\r\n                    autopilot_controller = RemoteSquidController(\r\n                        squid_data=initial_data,\r\n                        scene=self.tamagotchi_logic.user_interface.scene,\r\n                        plugin_instance=self,\r\n                        debug_mode=self.debug_mode,\r\n                        remote_entity_manager=self.entity_manager\r\n                    )\r\n                    self.remote_squid_controllers[source_node_id] = autopilot_controller\r\n                    self.logger.info(f\"Autopilot for {source_node_id} created.\")\r\n                else:\r\n                    self.logger.warning(f\"Failed to create/update remote squid {source_node_id} in entity_manager.\")\r\n            else:\r\n                self.logger.error(\"Remote entity manager (self.entity_manager) not found!\")\r\n\r\n            return True\r\n\r\n        except Exception as e:\r\n            self.logger.error(f\"Error in handle_squid_exit_message: {e}\", exc_info=True)\r\n            return False\r\n\r\n    def _setup_controller_creation_timer(self):\r\n        \"\"\"(Fallback) Sets up a QTimer to process pending controller creations.\"\"\"\r\n        if not self.logger: return\r\n        if self.controller_creation_timer and self.controller_creation_timer.isActive():\r\n            return # Already running\r\n        if not self.controller_creation_timer:\r\n            self.controller_creation_timer = QtCore.QTimer()\r\n            self.controller_creation_timer.timeout.connect(self._process_pending_controller_creations)\r\n        self.controller_creation_timer.start(300) # Check every 300ms\r\n        if self.debug_mode: self.logger.debug(\"Fallback controller creation timer started.\")\r\n\r\n    def _process_pending_controller_creations(self):\r\n        \"\"\"(Fallback) Processes remote squids waiting for controllers.\"\"\"\r\n        if not self.logger: return\r\n        if not hasattr(self, 'pending_controller_creations') or not self.pending_controller_creations:\r\n            return\r\n\r\n        items_to_process = list(self.pending_controller_creations) # Copy for safe iteration\r\n        self.pending_controller_creations.clear() # Clear original list\r\n\r\n        for creation_task in items_to_process:\r\n            node_id = creation_task.get('node_id')\r\n            squid_data = creation_task.get('squid_data')\r\n            if not node_id or not squid_data: continue\r\n\r\n            if node_id not in self.remote_squid_controllers:\r\n                if self.debug_mode: self.logger.debug(f\"Fallback: Creating controller for squid {node_id[-6:]}.\")\r\n                self._setup_controller_immediately(node_id, squid_data)\r\n            elif self.debug_mode:\r\n                self.logger.debug(f\"Fallback: Controller for {node_id[-6:]} already exists, skipping duplicate creation.\")\r\n\r\n\r\n    def update_remote_controllers(self):\r\n        \"\"\"Called by a QTimer to update RemoteSquidController instances.\"\"\"\r\n        if not self.logger:\r\n            return\r\n\r\n        # ----  TRACE  ----\r\n        if self.debug_mode:\r\n            print(f\"TRACE ++++ MP_PLUGIN_LOGIC: update_remote_controllers METHOD ENTERED. \"\r\n                f\"Num controllers: {len(self.remote_squid_controllers)} ++++\")\r\n            self.logger.info(\"DEBUG_MPL_UPDATE: At start of update_remote_controllers. \"\r\n                            \"Current controllers: %s. Dict size: %s\",\r\n                            list(self.remote_squid_controllers.keys()),\r\n                            len(self.remote_squid_controllers))\r\n        # -----------------\r\n\r\n        if not hasattr(self, 'remote_squid_controllers') or not self.remote_squid_controllers:\r\n            return\r\n\r\n        current_time = time.time()\r\n        delta_time = current_time - self.last_controller_update\r\n        if delta_time <= 0.001:\r\n            return\r\n        self.last_controller_update = current_time\r\n\r\n        for node_id, controller in list(self.remote_squid_controllers.items()):\r\n            try:\r\n                if self.debug_mode:\r\n                    print(f\"++++ MP_PLUGIN_LOGIC: Attempting to call update() for controller {node_id[-4:]} ++++\")\r\n                controller.update(delta_time)\r\n            except Exception as e:\r\n                self.logger.error(f\"++++ MP_PLUGIN_LOGIC: Error updating controller for {node_id[-6:]}: {e}\", exc_info=True)\r\n\r\n\r\n    def calculate_entry_position(self, entry_side_direction: str) -> tuple:\r\n        \"\"\"Calculates X,Y coordinates for a squid entering this local screen.\"\"\"\r\n        if not self.logger: return (100,100) # Default fallback\r\n        if not self.tamagotchi_logic or not self.tamagotchi_logic.user_interface:\r\n            return (100, 100) # Fallback if UI not available\r\n        \r\n        window_w = self.tamagotchi_logic.user_interface.window_width\r\n        window_h = self.tamagotchi_logic.user_interface.window_height\r\n        margin = 70 # How far from the edge it should appear\r\n\r\n        if entry_side_direction == 'left':   return (margin, window_h / 2)\r\n        elif entry_side_direction == 'right':return (window_w - margin, window_h / 2)\r\n        elif entry_side_direction == 'up':   return (window_w / 2, margin)\r\n        elif entry_side_direction == 'down': return (window_w / 2, window_h - margin)\r\n        \r\n        return (window_w / 2, window_h / 2) # Default to center if direction unknown\r\n\r\n\r\n    def apply_remote_experiences(self, local_squid, activity_summary: Dict):\r\n        \"\"\"Applies summarized experiences from a remote journey to the local squid.\"\"\"\r\n        if not self.logger: return\r\n        if not local_squid or not activity_summary: return\r\n\r\n        food_eaten = activity_summary.get('food_eaten', 0)\r\n        rocks_interacted = activity_summary.get('rock_interactions', 0)\r\n        rocks_brought_back = activity_summary.get('rocks_stolen', 0)\r\n        time_away_seconds = activity_summary.get('time_away', 0)\r\n        time_str = f\"{int(time_away_seconds/60)}m {int(time_away_seconds%60)}s\"\r\n        \r\n        journey_desc = f\"Returned from a {time_str} journey to another tank. \"\r\n\r\n        if hasattr(local_squid, 'memory_manager'):\r\n            mm = local_squid.memory_manager\r\n            if food_eaten > 0:\r\n                journey_desc += f\"Ate {food_eaten} snacks there. \"\r\n                local_squid.hunger = max(0, local_squid.hunger - 10 * food_eaten) # Reduce hunger\r\n                mm.add_short_term_memory('travel', 'ate_on_trip', f\"Found {food_eaten} yummy treats on my trip!\", 5)\r\n            if rocks_interacted > 0:\r\n                journey_desc += f\"Played with {rocks_interacted} interesting rocks. \"\r\n                local_squid.happiness = min(100, local_squid.happiness + 3 * rocks_interacted) # Increase happiness\r\n                mm.add_short_term_memory('travel', 'played_on_trip', f\"Played with {rocks_interacted} cool rocks elsewhere!\", 4)\r\n            \r\n            mm.add_short_term_memory('travel', 'completed_journey', journey_desc, importance=7)\r\n\r\n            if food_eaten > 1 or rocks_interacted > 3 or rocks_brought_back > 0:\r\n                mm.add_short_term_memory('emotion', 'happy_return', \"It's great to be back home after an exciting adventure!\", 6)\r\n            else:\r\n                mm.add_short_term_memory('emotion', 'calm_return', \"Returned home. It was a quiet trip.\", 3)\r\n\r\n        if hasattr(local_squid, 'curiosity'): # Reduce curiosity after travel\r\n            local_squid.curiosity = max(0, local_squid.curiosity - 25)\r\n\r\n    def create_exit_effect(self, center_x: float, center_y: float, direction_str: str = \"\"):\r\n        \"\"\"Creates a visual effect when a local squid exits the screen.\r\n           MODIFIED: This effect will be static or very simple for testing.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        # scene = self.tamagotchi_logic.user_interface.scene # Scene not used if no visual effects\r\n\r\n        # Simplified static indicator (e.g., a small, briefly visible circle or text)\r\n        if self.debug_mode:\r\n            self.logger.debug(f\"Skipping dynamic exit effect for static testing at ({center_x},{center_y}).\")\r\n\r\n    from PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\n    def _show_exit_arrow(self, exit_direction: str):\r\n        \"\"\"\r\n        Draw a big arrow flush with the screen edge the squid just left through.\r\n        exit_direction : 'left' | 'right' | 'up' | 'down'\r\n        \"\"\"\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'):\r\n            return\r\n        ui = self.tamagotchi_logic.user_interface\r\n        scene = ui.scene\r\n        w, h = ui.window_width, ui.window_height\r\n        colour = QtGui.QColor(255, 200, 0, 200)   # juicy orange\r\n        pen = QtGui.QPen(colour, 16)\r\n        pen.setCapStyle(QtCore.Qt.RoundCap)\r\n        pen.setJoinStyle(QtCore.Qt.RoundJoin)\r\n\r\n        # ---------- geometry ----------\r\n        shaft_len = min(w, h) * 0.55          # long shaft\r\n        head_len  = shaft_len * 0.30          # arrow-head\r\n        shaft_wid = 24                        # visual thickness\r\n\r\n        if exit_direction == 'left':          # leaving LEFT edge → arrow points LEFT\r\n            shaft = QtCore.QLineF(shaft_len, h/2, 0, h/2)\r\n            head1 = QtCore.QLineF(head_len,  h/2 - head_len*0.6, 0, h/2)\r\n            head2 = QtCore.QLineF(head_len,  h/2 + head_len*0.6, 0, h/2)\r\n        elif exit_direction == 'right':       # leaving RIGHT edge → arrow points RIGHT\r\n            shaft = QtCore.QLineF(w - shaft_len, h/2, w, h/2)\r\n            head1 = QtCore.QLineF(w - head_len,  h/2 - head_len*0.6, w, h/2)\r\n            head2 = QtCore.QLineF(w - head_len,  h/2 + head_len*0.6, w, h/2)\r\n        elif exit_direction == 'up':          # leaving TOP edge → arrow points UP\r\n            shaft = QtCore.QLineF(w/2, shaft_len, w/2, 0)\r\n            head1 = QtCore.QLineF(w/2 - head_len*0.6, head_len, w/2, 0)\r\n            head2 = QtCore.QLineF(w/2 + head_len*0.6, head_len, w/2, 0)\r\n        else:                                 # leaving BOTTOM edge → arrow points DOWN\r\n            shaft = QtCore.QLineF(w/2, h - shaft_len, w/2, h)\r\n            head1 = QtCore.QLineF(w/2 - head_len*0.6, h - head_len, w/2, h)\r\n            head2 = QtCore.QLineF(w/2 + head_len*0.6, h - head_len, w/2, h)\r\n\r\n        # ---------- draw ----------\r\n        group = QtWidgets.QGraphicsItemGroup()\r\n        group.setZValue(1000)  # on top of everything\r\n        for line in (shaft, head1, head2):\r\n            group.addToGroup(scene.addLine(line, pen))\r\n        scene.addItem(group)\r\n\r\n        # ---------- fade & self-destruct ----------\r\n        def fade():\r\n            opacity = group.opacity()\r\n            if opacity > 0.01:\r\n                group.setOpacity(max(0, opacity - 0.04))\r\n            else:\r\n                scene.removeItem(group)\r\n                fade_timer.stop()\r\n\r\n        fade_timer = QtCore.QTimer()\r\n        fade_timer.timeout.connect(fade)\r\n        fade_timer.start(40)  # ~25 fps → gone in ~2 s\r\n\r\n    def handle_squid_return(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles a 'squid_return' message for the player's own squid.\"\"\"\r\n        if not self.logger: return\r\n        try:\r\n            return_payload = message.get('payload', {})\r\n            returning_node_id = return_payload.get('node_id')\r\n\r\n            if not self.network_node or returning_node_id != self.network_node.node_id:\r\n                if self.debug_mode:\r\n                    expected_id = self.network_node.node_id if self.network_node else \"N/A\"\r\n                    self.logger.debug(f\"Squid_return message ignored. Expected node '{expected_id}', got '{returning_node_id}'.\")\r\n                return\r\n\r\n            local_squid = self.tamagotchi_logic.squid\r\n            if not local_squid or not local_squid.squid_item:\r\n                if self.debug_mode: self.logger.debug(\"Local squid or its visual item not found for return.\")\r\n                return\r\n\r\n            activity_summary = return_payload.get('activity_summary', {})\r\n            entry_side = return_payload.get('return_direction', 'left') \r\n            entry_coords = self.calculate_entry_position(entry_side)\r\n\r\n            local_squid.squid_x, local_squid.squid_y = entry_coords[0], entry_coords[1]\r\n            local_squid.squid_item.setPos(local_squid.squid_x, local_squid.squid_y)\r\n            local_squid.squid_item.setVisible(True)\r\n            local_squid.squid_item.setOpacity(1.0)\r\n            if local_squid.squid_item.graphicsEffect():\r\n                local_squid.squid_item.graphicsEffect().setEnabled(False)\r\n                local_squid.squid_item.setGraphicsEffect(None)\r\n\r\n            self.apply_remote_experiences(local_squid, activity_summary)\r\n\r\n            # --- MODIFICATION FOR DETAILED ITEM RECREATION ---\r\n            carried_items_details_list = activity_summary.get('carried_items_details', [])\r\n            num_items_brought_back = len(carried_items_details_list) # Or use 'rocks_stolen' if it reliably matches the list length\r\n\r\n            if num_items_brought_back > 0:\r\n                self.logger.info(f\"Local squid returned carrying {num_items_brought_back} item(s). Details: {carried_items_details_list}\")\r\n                # We'll create/modify this method in the next step:\r\n                self.recreate_carried_items_in_tank(local_squid, carried_items_details_list, entry_coords)\r\n                \r\n                if hasattr(self.tamagotchi_logic, 'show_message'):\r\n                    self.tamagotchi_logic.show_message(f\"Your squid returned with {num_items_brought_back} item(s) from its adventure!\")\r\n            else:\r\n                # ... (existing message for no items stolen) ...\r\n                if hasattr(self.tamagotchi_logic, 'show_message'):\r\n                    journey_time_sec = activity_summary.get('time_away', 0)\r\n                    time_str = f\"{int(journey_time_sec/60)}m {int(journey_time_sec%60)}s\"\r\n                    self.tamagotchi_logic.show_message(f\"Welcome back! Your squid explored for {time_str}.\")\r\n            \r\n            local_squid.can_move = True\r\n            if hasattr(local_squid, 'is_transitioning'): local_squid.is_transitioning = False\r\n            local_squid.status = \"just returned home\"\r\n\r\n            if self.debug_mode: self.logger.debug(f\"Local squid '{getattr(local_squid,'name','')}' returned to {entry_coords} from {entry_side}. Brought back {num_items_brought_back} items.\")\r\n\r\n        except Exception as e:\r\n            self.logger.error(f\"Handling local squid's return failed: {e}\", exc_info=True)\r\n\r\n    def recreate_carried_items_in_tank(self, local_squid, carried_items_data_list: List[Dict], entry_position: tuple):\r\n        \"\"\"\r\n        Recreates items in the local scene based on detailed data brought back by the squid.\r\n        The first item is given to the squid to carry; subsequent items are placed in the tank.\r\n        \"\"\"\r\n        if not self.logger:\r\n            print(\"MPPluginLogic ERRA: Logger not available in recreate_carried_items_in_tank\")\r\n            return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface') or not carried_items_data_list:\r\n            self.logger.warning(\"Cannot recreate carried items: TamagotchiLogic, UI, or item data missing.\")\r\n            return\r\n        \r\n        ui = self.tamagotchi_logic.user_interface\r\n        scene = ui.scene\r\n        entry_x, entry_y = entry_position\r\n\r\n        self.logger.info(f\"Recreating {len(carried_items_data_list)} item(s) brought back by squid {local_squid.name if hasattr(local_squid, 'name') else 'N/A'}.\")\r\n\r\n        item_successfully_given_to_squid_to_carry = False\r\n\r\n        for i, item_data in enumerate(carried_items_data_list):\r\n            try:\r\n                original_filename = item_data.get('original_filename')\r\n                original_category = item_data.get('original_category', 'decoration') # Default if not specified\r\n                item_scale = item_data.get('scale', 1.0)\r\n                # Use original_zValue if provided, otherwise default (e.g. for decorations)\r\n                # Add a small positive bias if it's going to be carried to appear above squid if at same base Z\r\n                item_z_value = item_data.get('zValue', 0) \r\n\r\n\r\n                if not original_filename:\r\n                    self.logger.warning(f\"Skipping item recreation: 'original_filename' missing in item data: {item_data}\")\r\n                    continue\r\n\r\n                # --- Asset Path Resolution ---\r\n                # This logic attempts to find the image based on the original_filename.\r\n                # It assumes original_filename might be a base name or include a subfolder like \"decoration/\".\r\n                possible_paths = [\r\n                    os.path.join(\"images\", original_filename), \r\n                    os.path.join(\"images\", \"decoration\", os.path.basename(original_filename)),\r\n                    os.path.join(\"images\", \"items\", os.path.basename(original_filename)),\r\n                    os.path.join(\"images\", \"food\", os.path.basename(original_filename)), # If food can be carried\r\n                    os.path.join(\"images\", \"rocks\", os.path.basename(original_filename)), # If rocks are in subfolder\r\n                    original_filename # If original_filename was already a relative path like \"images/foo.png\"\r\n                ]\r\n                \r\n                item_image_path = None\r\n                for p_path in possible_paths:\r\n                    # Normalize path for consistent checking\r\n                    normalized_path = os.path.normpath(p_path)\r\n                    if os.path.exists(normalized_path):\r\n                        item_image_path = normalized_path\r\n                        break\r\n                \r\n                if not item_image_path:\r\n                    self.logger.warning(f\"Could not find local image asset for '{original_filename}'. Attempting fallback to default rock/item.\")\r\n                    # Fallback to a generic rock image if specific image not found\r\n                    item_image_path = os.path.join(\"images\", \"rock.png\") # Default fallback\r\n                    if not os.path.exists(item_image_path):\r\n                        self.logger.error(f\"Default fallback image 'images/rock.png' also not found. Cannot recreate item.\")\r\n                        continue # Skip this item\r\n                    original_category = 'rock' # Override category if using fallback rock\r\n\r\n                self.logger.info(f\"Attempting to recreate item {i+1}: Path='{item_image_path}', Category='{original_category}', Scale={item_scale:.2f}\")\r\n\r\n                pixmap = QtGui.QPixmap(item_image_path)\r\n                if pixmap.isNull():\r\n                    self.logger.error(f\"Failed to load QPixmap for item image: '{item_image_path}'\")\r\n                    continue\r\n\r\n                # --- Item Creation ---\r\n                created_item_visual = None\r\n                if hasattr(ui, 'ResizablePixmapItem'):\r\n                    created_item_visual = ui.ResizablePixmapItem(pixmap, item_image_path)\r\n                else:\r\n                    created_item_visual = QtWidgets.QGraphicsPixmapItem(pixmap)\r\n                    setattr(created_item_visual, 'filename', item_image_path)\r\n                \r\n                setattr(created_item_visual, 'category', original_category)\r\n                created_item_visual.setScale(item_scale)\r\n                # ZValue for carried item will be handled by squid logic to be on top\r\n                # ZValue for placed items will use item_z_value\r\n                # setattr(created_item_visual, 'original_zValue', item_z_value) # Store for later placement\r\n\r\n                setattr(created_item_visual, 'is_stolen_from_remote', True)\r\n                setattr(created_item_visual, 'is_foreign', True)\r\n                self.apply_foreign_object_tint(created_item_visual)\r\n                \r\n                # Add to scene so it's managed by Qt, positions will be updated\r\n                scene.addItem(created_item_visual)\r\n\r\n                if not item_successfully_given_to_squid_to_carry and hasattr(local_squid, 'start_carrying_item'):\r\n                    # The first successfully created item is given to the squid to carry\r\n                    created_item_visual.setZValue(local_squid.squid_item.zValue() + 0.1) # Ensure it's visually above squid slightly\r\n                    local_squid.start_carrying_item(created_item_visual)\r\n                    item_successfully_given_to_squid_to_carry = True\r\n                    self.logger.info(f\"Squid '{local_squid.name}' is now carrying '{os.path.basename(item_image_path)}'.\")\r\n                else:\r\n                    # Subsequent items, or if squid can't carry, place them in the tank\r\n                    if not hasattr(local_squid, 'start_carrying_item'):\r\n                         self.logger.warning(f\"Squid object does not have 'start_carrying_item' method. Placing item '{os.path.basename(item_image_path)}' near entry.\")\r\n                    else:\r\n                        self.logger.info(f\"Placing additional item '{os.path.basename(item_image_path)}' directly in tank.\")\r\n\r\n                    created_item_visual.setZValue(item_z_value) # Use its original/default Z for placed items\r\n                    \r\n                    # Scatter these additional items around the entry point\r\n                    angle_offset = random.uniform(-math.pi / 2, math.pi / 2) \r\n                    # Use 'i' to ensure different angle for each subsequent item\r\n                    angle = (i * (math.pi / max(1, len(carried_items_data_list) -1 ))) + angle_offset \r\n                    dist_from_entry = random.uniform(80, 150) # Slightly further for placed items\r\n                    \r\n                    item_x = entry_x + dist_from_entry * math.cos(angle)\r\n                    item_y = entry_y + dist_from_entry * math.sin(angle)\r\n                    \r\n                    item_w = pixmap.width() * item_scale\r\n                    item_h = pixmap.height() * item_scale\r\n                    # Basic boundary check\r\n                    item_x = max(item_w/2, min(item_x, ui.window_width - item_w/2))\r\n                    item_y = max(item_h/2, min(item_y, ui.window_height - item_h/2))\r\n\r\n                    created_item_visual.setPos(item_x - item_w/2, item_y - item_h/2)\r\n                    created_item_visual.setOpacity(1.0) # Ensure visible\r\n\r\n                # Add memory for each successfully recreated item\r\n                if hasattr(local_squid, 'memory_manager'):\r\n                    local_squid.memory_manager.add_short_term_memory(\r\n                        'achievement', f'brought_back_{original_category}',\r\n                        f\"Brought back a {os.path.basename(item_image_path)} from an adventure!\", importance=6\r\n                    )\r\n\r\n            except Exception as e:\r\n                self.logger.error(f\"Error processing and recreating a carried item from data '{item_data}': {e}\", exc_info=True)\r\n\r\n    def _create_arrival_animation(self, graphics_item: QtWidgets.QGraphicsPixmapItem):\r\n        \"\"\"MODIFIED: Creates a simple fade-in animation (now static) for newly arrived remote items.\"\"\"\r\n        if not self.logger: return\r\n        if not graphics_item: return\r\n        try:\r\n            # For static testing, just ensure opacity and scale are correct\r\n            target_opacity = self.REMOTE_SQUID_OPACITY # Should be 1.0\r\n            if hasattr(graphics_item, 'is_remote_clone') and getattr(graphics_item, 'is_remote_clone'):\r\n                # Cloned objects might have a slightly different base opacity if desired, but for squid, full.\r\n                pass #  target_opacity *= 0.75 (example if clones were different)\r\n            \r\n            graphics_item.setOpacity(target_opacity)\r\n            graphics_item.setScale(1.0) # Ensure normal scale\r\n\r\n            if graphics_item.graphicsEffect(): # Remove any prior opacity effect\r\n                graphics_item.setGraphicsEffect(None)\r\n\r\n        except Exception as e:\r\n            if self.debug_mode: self.logger.warning(f\"Simple static arrival display error: {e}\")\r\n            if graphics_item: graphics_item.setOpacity(self.REMOTE_SQUID_OPACITY) # Fallback\r\n\r\n\r\n    def _reset_remote_squid_style(self, node_id_or_item):\r\n        \"\"\"Resets the visual style of a remote squid to default (static, full opacity).\"\"\"\r\n        if not self.logger: return\r\n        node_id = None\r\n        squid_display_data = None\r\n\r\n        if isinstance(node_id_or_item, str): # If node_id is passed\r\n            node_id = node_id_or_item\r\n            squid_display_data = self.remote_squids.get(node_id)\r\n        elif isinstance(node_id_or_item, QtWidgets.QGraphicsPixmapItem): # If visual item is passed\r\n            for nid, s_data in self.remote_squids.items():\r\n                if s_data.get('visual') == node_id_or_item:\r\n                    node_id = nid\r\n                    squid_display_data = s_data\r\n                    break\r\n        \r\n        if not squid_display_data: return # Squid not found\r\n\r\n        visual_item = squid_display_data.get('visual')\r\n        status_text_item = squid_display_data.get('status_text')\r\n        id_text_item = squid_display_data.get('id_text')\r\n\r\n        if visual_item:\r\n            visual_item.setZValue(5) # Default Z-order for remote squids\r\n            visual_item.setOpacity(self.REMOTE_SQUID_OPACITY) # Ensure full opacity\r\n            visual_item.setScale(1.0) # Ensure normal scale\r\n            if visual_item.graphicsEffect(): # Remove any effects (like shadow or previous opacity effects)\r\n                visual_item.setGraphicsEffect(None)\r\n        \r\n        # Reset status text style\r\n        if status_text_item:\r\n            current_status_from_data = squid_display_data.get('data', {}).get('status', 'visiting')\r\n            status_text_item.setPlainText(current_status_from_data)\r\n            status_text_item.setDefaultTextColor(QtGui.QColor(200, 200, 200, 220)) # Default color\r\n            status_text_item.setFont(QtGui.QFont(\"Arial\", 9)) # Default font\r\n            status_text_item.setZValue(6) # Above squid\r\n\r\n        # Reset ID text style (usually less dynamic)\r\n        if id_text_item:\r\n            id_text_item.setDefaultTextColor(QtGui.QColor(200, 200, 200, 180))\r\n            id_text_item.setFont(QtGui.QFont(\"Arial\", 8))\r\n            id_text_item.setZValue(6)\r\n\r\n\r\n    def register_menu_actions(self, main_ui_window: QtWidgets.QMainWindow, target_menu: QtWidgets.QMenu):\r\n        \"\"\"Registers menu actions related to the multiplayer plugin.\"\"\"\r\n        if not self.logger: return\r\n\r\n        about_action = QtWidgets.QAction(f\"About {mp_constants.PLUGIN_NAME}...\", main_ui_window)\r\n        about_action.triggered.connect(self.show_about_dialog)\r\n        target_menu.addAction(about_action)\r\n\r\n        config_action = QtWidgets.QAction(\"Network Settings...\", main_ui_window)\r\n        config_action.triggered.connect(self.show_config_dialog)\r\n        target_menu.addAction(config_action)\r\n\r\n        dashboard_action = QtWidgets.QAction(\"Network Dashboard...\", main_ui_window)\r\n        dashboard_action.triggered.connect(self.show_network_dashboard)\r\n        target_menu.addAction(dashboard_action)\r\n        \r\n        target_menu.addSeparator()\r\n\r\n        refresh_connections_action = QtWidgets.QAction(\"Refresh Connections\", main_ui_window)\r\n        refresh_connections_action.triggered.connect(self.refresh_connections)\r\n        target_menu.addAction(refresh_connections_action)\r\n\r\n        # Toggle for connection lines\r\n        self.mp_menu_toggle_connection_lines_action = QtWidgets.QAction(\"Show Connection Lines\", main_ui_window)\r\n        self.mp_menu_toggle_connection_lines_action.setCheckable(True)\r\n        self.mp_menu_toggle_connection_lines_action.setChecked(self.SHOW_CONNECTION_LINES)\r\n        self.mp_menu_toggle_connection_lines_action.triggered.connect(self.toggle_connection_lines)\r\n        target_menu.addAction(self.mp_menu_toggle_connection_lines_action)\r\n        \r\n        if self.debug_mode: # Debug specific actions\r\n            target_menu.addSeparator()\r\n            debug_autopilot_action = QtWidgets.QAction(\"Debug Autopilot Status\", main_ui_window)\r\n            debug_autopilot_action.triggered.connect(self.debug_autopilot_status)\r\n            target_menu.addAction(debug_autopilot_action)\r\n\r\n\r\n    def update_menu_states(self):\r\n        \"\"\"Updates the state of checkable menu items.\"\"\"\r\n        if hasattr(self, 'mp_menu_toggle_connection_lines_action'):\r\n            self.mp_menu_toggle_connection_lines_action.setChecked(self.SHOW_CONNECTION_LINES)\r\n\r\n\r\n    def show_about_dialog(self):\r\n        \"\"\"Displays an 'About' dialog.\"\"\"\r\n        if not self.logger: return\r\n        parent_window = None\r\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface'):\r\n            parent_window = self.tamagotchi_logic.user_interface.window\r\n        \r\n        node_id_str = getattr(self.network_node, 'node_id', \"N/A\") if self.network_node else \"N/A\"\r\n        ip_str = getattr(self.network_node, 'local_ip', \"N/A\") if self.network_node else \"N/A\"\r\n        status_str = \"Connected\" if self.network_node and self.network_node.is_connected else \"Disconnected\"\r\n\r\n        about_text_content = (\r\n            f\"<h3>{mp_constants.PLUGIN_NAME}</h3>\"\r\n            f\"<p><b>Version:</b> {mp_constants.PLUGIN_VERSION}<br>\"\r\n            f\"<b>Author:</b> {mp_constants.PLUGIN_AUTHOR}</p>\"\r\n            f\"<p>{mp_constants.PLUGIN_DESCRIPTION}</p><hr>\"\r\n            f\"<p><b>Node ID:</b> {node_id_str}<br>\"\r\n            f\"<b>Local IP:</b> {ip_str}<br>\"\r\n            f\"<b>Status:</b> {status_str}</p>\"\r\n        )\r\n        QtWidgets.QMessageBox.about(parent_window, f\"About {mp_constants.PLUGIN_NAME}\", about_text_content)\r\n\r\n\r\n    def show_config_dialog(self):\r\n        \"\"\"Displays the configuration dialog for multiplayer settings.\"\"\"\r\n        if not self.logger: return\r\n        try:\r\n            from .multiplayer_config_dialog import MultiplayerConfigDialog\r\n        except ImportError:\r\n            self.logger.error(\"MultiplayerConfigDialog class/file not found.\")\r\n            parent_win = self.tamagotchi_logic.user_interface.window if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface') else None\r\n            QtWidgets.QMessageBox.critical(parent_win, \"Configuration Error\", \"The multiplayer settings dialog could not be loaded.\")\r\n            return\r\n\r\n        parent_window = None\r\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface'):\r\n            parent_window = self.tamagotchi_logic.user_interface.window\r\n\r\n        current_settings = {\r\n            'multicast_group': self.MULTICAST_GROUP, 'port': self.MULTICAST_PORT,\r\n            'sync_interval': self.SYNC_INTERVAL, 'remote_opacity': self.REMOTE_SQUID_OPACITY,\r\n            'show_labels': self.SHOW_REMOTE_LABELS, 'show_connections': self.SHOW_CONNECTION_LINES,\r\n            'debug_mode': self.debug_mode,\r\n            'auto_reconnect': self.network_node.auto_reconnect if self.network_node else True,\r\n            'use_compression': self.network_node.use_compression if self.network_node else True\r\n        }\r\n        \r\n        if not self.config_dialog or not self.config_dialog.isVisible(): # Create new or reuse\r\n            self.config_dialog = MultiplayerConfigDialog(\r\n                plugin_instance=self, parent=parent_window, initial_settings=current_settings\r\n            )\r\n        else:\r\n            self.config_dialog.load_settings(current_settings) # Update existing dialog with current settings\r\n        \r\n        self.config_dialog.exec_() # Show as modal dialog\r\n\r\n    def toggle_connection_lines(self, checked_state: bool):\r\n        \"\"\"Toggles the visibility of connection lines.\"\"\"\r\n        if not self.logger: return\r\n        self.SHOW_CONNECTION_LINES = checked_state\r\n        \r\n        # This will be handled by entity_manager's update_settings or fallback update_connection_lines\r\n        if getattr(self, 'entity_manager', None):\r\n            self.entity_manager.update_settings(show_connections=self.SHOW_CONNECTION_LINES)\r\n        else: # Fallback if no entity manager\r\n            if hasattr(self.tamagotchi_logic, 'user_interface') and self.tamagotchi_logic.user_interface:\r\n                scene = self.tamagotchi_logic.user_interface.scene\r\n                for line_item in self.connection_lines.values():\r\n                    if line_item in scene.items(): # Check if item is still in scene\r\n                        line_item.setVisible(self.SHOW_CONNECTION_LINES)\r\n                if not self.SHOW_CONNECTION_LINES: # If hiding, remove them\r\n                    for node_id_key in list(self.connection_lines.keys()):\r\n                        line_to_remove = self.connection_lines.pop(node_id_key)\r\n                        if line_to_remove in scene.items():\r\n                            scene.removeItem(line_to_remove)\r\n            self.update_connection_lines() # Trigger an update to draw/remove lines\r\n\r\n        if hasattr(self.tamagotchi_logic, 'show_message'):\r\n            self.tamagotchi_logic.show_message(f\"Connection lines to remote squids {'shown' if checked_state else 'hidden'}.\")\r\n\r\n\r\n    def refresh_connections(self):\r\n        \"\"\"Manually triggers a network presence announcement.\"\"\"\r\n        if not self.logger: return\r\n        if not self.network_node:\r\n            msg = \"Multiplayer: Network component not initialized. Cannot refresh.\"\r\n            if hasattr(self.tamagotchi_logic, 'show_message'): self.tamagotchi_logic.show_message(msg)\r\n            else: self.logger.warning(msg)\r\n            return\r\n\r\n        if not self.network_node.is_connected:\r\n            if self.debug_mode: self.logger.debug(\"Attempting to reconnect before refresh...\")\r\n            self.network_node.try_reconnect() # Attempt reconnect if not connected\r\n\r\n        message_to_show = \"\"\r\n        if self.network_node.is_connected:\r\n            self.network_node.send_message(\r\n                'heartbeat',\r\n                {'node_id': self.network_node.node_id, 'status': 'active_refresh_request'}\r\n            )\r\n            message_to_show = \"Multiplayer: Sent network heartbeat. Checking for peers...\"\r\n        else:\r\n            message_to_show = \"Multiplayer: Network disconnected. Could not send heartbeat.\"\r\n\r\n        if hasattr(self.tamagotchi_logic, 'show_message'):\r\n            self.tamagotchi_logic.show_message(message_to_show)\r\n        elif self.debug_mode or not self.network_node.is_connected: # Log if no UI message or on error\r\n            self.logger.info(message_to_show)\r\n\r\n        # Update UI status\r\n        current_peers_count = len(self.network_node.known_nodes if self.network_node else {})\r\n        if self.status_widget:\r\n            self.status_widget.update_peers(self.network_node.known_nodes if self.network_node else {})\r\n            self.status_widget.add_activity(f\"Connections refreshed. {current_peers_count} peers currently detected.\")\r\n        elif self.status_bar: # Fallback to basic status bar\r\n            if hasattr(self.status_bar, 'update_peers_count'): self.status_bar.update_peers_count(current_peers_count)\r\n            if hasattr(self.status_bar, 'showMessage'): self.status_bar.showMessage(f\"Refreshed. {current_peers_count} peers.\", 3000)\r\n\r\n\r\n    def initialize_remote_representation(self):\r\n        \"\"\"(Fallback) Initializes basic timers for managing remote entity visuals if entity_manager is not used.\"\"\"\r\n        if not self.logger: return\r\n        if self.entity_manager: # If entity_manager is active, it handles these.\r\n            if self.debug_mode: self.logger.debug(\"RemoteEntityManager is active, skipping fallback timers.\")\r\n            return\r\n\r\n        if not self.cleanup_timer_basic:\r\n            self.cleanup_timer_basic = QtCore.QTimer()\r\n            self.cleanup_timer_basic.timeout.connect(self.cleanup_stale_nodes) # Fallback cleanup\r\n            self.cleanup_timer_basic.start(7500) # Check every 7.5s\r\n        \r\n        if not self.connection_timer_basic:\r\n            self.connection_timer_basic = QtCore.QTimer()\r\n            self.connection_timer_basic.timeout.connect(self.update_connection_lines) # Fallback line drawing\r\n            self.connection_timer_basic.start(1200) # Update lines every 1.2s\r\n\r\n\r\n    def cleanup_stale_nodes(self):\r\n        \"\"\"(Fallback) Removes visuals of remote nodes that haven't sent updates, used if entity_manager is None.\"\"\"\r\n        if not self.logger: return\r\n        if self.entity_manager: return # entity_manager handles its own cleanup\r\n\r\n        if not self.network_node: return\r\n        current_time = time.time()\r\n        stale_threshold_seconds = 45.0 # Example: 45 seconds timeout\r\n        \r\n        nodes_to_remove_ids = []\r\n        # Check known_nodes from NetworkNode\r\n        for node_id, (_, last_seen_time, _) in list(self.network_node.known_nodes.items()):\r\n            if current_time - last_seen_time > stale_threshold_seconds:\r\n                nodes_to_remove_ids.append(node_id)\r\n        \r\n        for node_id_to_remove in nodes_to_remove_ids:\r\n            if self.debug_mode: self.logger.debug(f\"Basic Cleanup: Node {node_id_to_remove[-6:]} timed out. Removing.\")\r\n            if node_id_to_remove in self.network_node.known_nodes:\r\n                del self.network_node.known_nodes[node_id_to_remove]\r\n            \r\n            # Remove visual representation using this plugin's direct management\r\n            self.remove_remote_squid(node_id_to_remove) # This uses self.remote_squids\r\n            \r\n            if node_id_to_remove in self.remote_squid_controllers: # Also remove controller\r\n                del self.remote_squid_controllers[node_id_to_remove]\r\n\r\n        # Update UI status\r\n        if self.network_node: # Check again as it might be cleaned up\r\n            peers_now = self.network_node.known_nodes if self.network_node else {}\r\n            if self.status_widget: self.status_widget.update_peers(peers_now)\r\n            elif self.status_bar and hasattr(self.status_bar, 'update_peers_count'): self.status_bar.update_peers_count(len(peers_now))\r\n\r\n\r\n    def update_connection_lines(self):\r\n        \"\"\"(Fallback) Updates visual lines connecting local squid to remote squids if entity_manager is None.\"\"\"\r\n        if not self.logger: return\r\n        if self.entity_manager: return # entity_manager handles its own connection lines\r\n\r\n        if not self.SHOW_CONNECTION_LINES or not self.tamagotchi_logic or \\\r\n           not self.tamagotchi_logic.squid or not self.tamagotchi_logic.user_interface or \\\r\n           not self.tamagotchi_logic.squid.squid_item: # Ensure all parts exist\r\n            # Clear existing lines if not showing or prerequisites missing\r\n            if hasattr(self.tamagotchi_logic, 'user_interface') and self.tamagotchi_logic.user_interface:\r\n                scene = self.tamagotchi_logic.user_interface.scene\r\n                for node_id_key in list(self.connection_lines.keys()):\r\n                    line_to_remove = self.connection_lines.pop(node_id_key)\r\n                    if line_to_remove in scene.items(): scene.removeItem(line_to_remove)\r\n            return\r\n\r\n        ui = self.tamagotchi_logic.user_interface\r\n        scene = ui.scene\r\n        local_squid_visual = self.tamagotchi_logic.squid.squid_item\r\n        local_rect = local_squid_visual.boundingRect()\r\n        local_center_pos = local_squid_visual.pos() + local_rect.center() # Center of local squid\r\n\r\n        active_remote_node_ids = set()\r\n        for node_id, remote_squid_info in self.remote_squids.items(): # Iterate this plugin's remote_squids\r\n            remote_visual = remote_squid_info.get('visual')\r\n            if not remote_visual or not remote_visual.isVisible() or remote_visual not in scene.items():\r\n                continue # Skip if no visual or not in scene\r\n            \r\n            active_remote_node_ids.add(node_id)\r\n            remote_rect = remote_visual.boundingRect()\r\n            remote_center_pos = remote_visual.pos() + remote_rect.center() # Center of remote squid\r\n\r\n            line_color_data = remote_squid_info.get('data', {}).get('color', (100, 100, 255)) # Use squid's color\r\n            try:\r\n                pen_color = QtGui.QColor(*line_color_data, 120) # Add alpha\r\n            except TypeError:\r\n                pen_color = QtGui.QColor(100,100,255,120) # Default color\r\n\r\n            pen = QtGui.QPen(pen_color)\r\n            pen.setWidth(2)\r\n            pen.setStyle(QtCore.Qt.DashLine) # Dashed line style\r\n\r\n            if node_id in self.connection_lines: # Update existing line\r\n                line = self.connection_lines[node_id]\r\n                if line not in scene.items(): scene.addItem(line) # Re-add if removed somehow\r\n                line.setLine(local_center_pos.x(), local_center_pos.y(), remote_center_pos.x(), remote_center_pos.y())\r\n                line.setPen(pen)\r\n                line.setVisible(True)\r\n            else: # Create new line\r\n                line = QtWidgets.QGraphicsLineItem(\r\n                    local_center_pos.x(), local_center_pos.y(), remote_center_pos.x(), remote_center_pos.y()\r\n                )\r\n                line.setPen(pen)\r\n                line.setZValue(-10) # Draw behind other items\r\n                scene.addItem(line)\r\n                self.connection_lines[node_id] = line\r\n        \r\n        # Remove lines for squids that are no longer active/present\r\n        for node_id_key in list(self.connection_lines.keys()):\r\n            if node_id_key not in active_remote_node_ids:\r\n                line_to_remove = self.connection_lines.pop(node_id_key)\r\n                if line_to_remove in scene.items():\r\n                    scene.removeItem(line_to_remove)\r\n\r\n\r\n    def _register_hooks(self):\r\n        \"\"\"Registers handlers for network message types with the plugin manager.\"\"\"\r\n        if not self.logger: return\r\n        if not self.plugin_manager:\r\n            self.logger.error(\"Cannot register hooks, plugin_manager is not set.\")\r\n            return\r\n\r\n        hook_handlers = {\r\n            # The hook name generated by NetworkNode is \"on_network_squid_exit\"\r\n            # The subscribe_to_hook call should match this.\r\n            \"on_network_squid_exit\": self.handle_squid_exit_message,\r\n            \"on_network_squid_move\": self.handle_squid_move,\r\n            \"on_network_object_sync\": self.handle_object_sync,\r\n            \"on_network_rock_throw\": self.handle_rock_throw,\r\n            \"on_network_heartbeat\": self.handle_heartbeat,\r\n            \"on_network_state_update\": self.handle_state_update, # Generic state update\r\n            # Add new 'squid_return' handler\r\n            \"on_network_squid_return\": self.handle_squid_return \r\n        }\r\n\r\n        for hook_name_to_register, handler_method_to_call in hook_handlers.items():\r\n            if hook_name_to_register == \"on_network_squid_exit\": \r\n                node_id_for_print_hook = \"UnknownNode_HookReg\"\r\n                if self.network_node: node_id_for_print_hook = self.network_node.node_id\r\n                print(f\"DEBUG_STEP_2B: MultiplayerPluginLogic {node_id_for_print_hook} is subscribing '{handler_method_to_call.__name__}' to hook: '{hook_name_to_register}'\")\r\n\r\n            self.plugin_manager.register_hook(hook_name_to_register) \r\n            self.plugin_manager.subscribe_to_hook(\r\n                hook_name_to_register, \r\n                mp_constants.PLUGIN_NAME, \r\n                handler_method_to_call\r\n            )\r\n        \r\n        # This hook is for the QTimer-based processing of the network queue, not direct network messages.\r\n        # If _process_network_node_queue is solely called by its QTimer, this pre_update might be redundant.\r\n        # However, it doesn't hurt to have it as a fallback or for other potential uses.\r\n        self.plugin_manager.register_hook(\"pre_update\") \r\n        self.plugin_manager.subscribe_to_hook(\"pre_update\", mp_constants.PLUGIN_NAME, self._process_network_node_queue)\r\n\r\n        if self.debug_mode: self.logger.debug(\"Network message hooks and pre_update hook registered.\")\r\n\r\n\r\n    def pre_update(self, *args, **kwargs):\r\n        \"\"\"Called by game's main update loop if subscribed to 'pre_update' hook.\"\"\"\r\n        # Current design uses QTimer for _process_network_node_queue.\r\n        # This method can be used for other periodic tasks if needed.\r\n        pass \r\n\r\n\r\n    def start_sync_timer(self):\r\n        \"\"\"Starts a daemon thread for periodic game state synchronization.\"\"\"\r\n        if not self.logger: return\r\n        if self.sync_thread and self.sync_thread.is_alive():\r\n            if self.debug_mode: self.logger.debug(\"Sync thread already running.\")\r\n            return\r\n\r\n        def game_state_sync_loop():\r\n            while True:\r\n                if not self.is_setup: # Check if plugin is still supposed to be running\r\n                    if self.debug_mode: self.logger.debug(\"SyncLoop: Plugin not setup or disabled, loop exiting.\")\r\n                    break\r\n                try:\r\n                    if self.network_node and self.network_node.is_connected and \\\r\n                       self.tamagotchi_logic and self.tamagotchi_logic.squid:\r\n                        \r\n                        # Dynamic sync interval based on local squid activity and peer count\r\n                        is_local_squid_moving = getattr(self.tamagotchi_logic.squid, 'is_moving', False)\r\n                        sync_delay_seconds = 0.3 if is_local_squid_moving else self.SYNC_INTERVAL # More frequent if moving\r\n                        \r\n                        num_peers = len(getattr(self.network_node, 'known_nodes', {}))\r\n                        if num_peers > 8: sync_delay_seconds *= 1.5 # Reduce load with many peers\r\n                        elif num_peers > 15: sync_delay_seconds *= 2.0\r\n                        sync_delay_seconds = max(0.2, min(sync_delay_seconds, 3.0)) # Clamp interval\r\n\r\n                        self.sync_game_state() # Send current state\r\n                        time.sleep(sync_delay_seconds)\r\n                    else:\r\n                        # If not connected or prerequisites missing, wait longer before retrying\r\n                        time.sleep(2.5) \r\n                except ReferenceError: # Can happen during interpreter shutdown\r\n                    if self.debug_mode: self.logger.debug(\"SyncLoop: ReferenceError (likely app shutting down), loop exiting.\")\r\n                    break\r\n                except Exception as e_sync:\r\n                    if self.debug_mode: self.logger.error(f\"Error in game_state_sync_loop: {e_sync}\", exc_info=True)\r\n                    time.sleep(3.0) # Wait a bit longer after an error\r\n        \r\n        self.sync_thread = threading.Thread(target=game_state_sync_loop, daemon=True)\r\n        self.sync_thread.start()\r\n        if self.debug_mode: self.logger.info(\"Game state synchronization thread started.\")\r\n\r\n\r\n    def sync_game_state(self):\r\n        \"\"\"Collects and sends current local game state.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'squid') or \\\r\n           not self.network_node or not self.network_node.is_connected:\r\n            return # Prerequisites not met\r\n\r\n        try:\r\n            squid_current_state = self._get_squid_state()\r\n            objects_current_state = self._get_objects_state() # Get state of syncable objects\r\n\r\n            sync_payload = {\r\n                'squid': squid_current_state,\r\n                'objects': objects_current_state,\r\n                'node_info': {'id': self.network_node.node_id, 'ip': self.network_node.local_ip}\r\n            }\r\n            self.network_node.send_message('object_sync', sync_payload) # Use 'object_sync' for combined state\r\n            \r\n            # Send heartbeat periodically as well\r\n            time_now = time.time()\r\n            if time_now - self.last_message_times.get('heartbeat_sent', 0) > 8.0: # Every 8 seconds\r\n                heartbeat_payload = {\r\n                    'node_id': self.network_node.node_id, 'status': 'active',\r\n                    'squid_pos': (squid_current_state['x'], squid_current_state['y']) # Include position in heartbeat\r\n                }\r\n                self.network_node.send_message('heartbeat', heartbeat_payload)\r\n                self.last_message_times['heartbeat_sent'] = time_now\r\n        except Exception as e:\r\n            self.logger.error(f\"ERROR during sync_game_state: {e}\", exc_info=True)\r\n\r\n\r\n    def _get_squid_state(self) -> Dict:\r\n        \"\"\"Compiles and returns a dictionary of the local squid's current state.\"\"\"\r\n        if not self.logger: return {}\r\n        if not self.tamagotchi_logic or not self.tamagotchi_logic.squid or not self.network_node:\r\n            return {} # Not enough info to build state\r\n\r\n        squid = self.tamagotchi_logic.squid\r\n        view_direction_rad = self.get_actual_view_direction(squid) # Get view cone direction\r\n\r\n        # --- Logging for sent direction ---\r\n        if self.debug_mode:\r\n            self.logger.debug(f\"Sending squid state: NodeID={self.network_node.node_id}, Direction={squid.squid_direction}, X={squid.squid_x:.1f}, Y={squid.squid_y:.1f}\")\r\n\r\n        return {\r\n            'x': squid.squid_x, \r\n            'y': squid.squid_y, \r\n            'direction': squid.squid_direction,  # General movement/logic direction\r\n            'image_direction_key': squid.squid_direction, # Explicit key for visual rendering direction\r\n            'looking_direction': view_direction_rad, # For view cone\r\n            'view_cone_angle': getattr(squid, 'view_cone_angle_rad', math.radians(60)),\r\n            'hunger': squid.hunger, \r\n            'happiness': squid.happiness,\r\n            'status': getattr(squid, 'status', \"idle\"), # Current action/status\r\n            'carrying_rock': getattr(squid, 'carrying_rock', False),\r\n            'is_sleeping': getattr(squid, 'is_sleeping', False),\r\n            'color': self.get_squid_color(), # Get consistent color based on node ID\r\n            'node_id': self.network_node.node_id, # Include node_id for identification\r\n            'view_cone_visible': getattr(squid, 'view_cone_visible', False), # Is view cone active\r\n            'squid_width': getattr(squid, 'squid_width', 60), # For rendering remote squid\r\n            'squid_height': getattr(squid, 'squid_height', 40) # For rendering remote squid\r\n        }\r\n\r\n\r\n    def get_actual_view_direction(self, squid_instance) -> float:\r\n        \"\"\"Determines the squid's current viewing direction in radians.\"\"\"\r\n        if hasattr(squid_instance, 'current_view_angle_radians'): # If a dynamic view angle exists\r\n            return squid_instance.current_view_angle_radians\r\n        \r\n        # Fallback to movement direction if no specific view angle\r\n        direction_to_radians_map = {\r\n            'right': 0.0, \r\n            'left': math.pi, \r\n            'up': 1.5 * math.pi, # -math.pi/2 or 3*math.pi/2\r\n            'down': 0.5 * math.pi  # math.pi/2\r\n        }\r\n        return direction_to_radians_map.get(getattr(squid_instance, 'squid_direction', 'right'), 0.0)\r\n\r\n\r\n    def get_squid_color(self) -> tuple:\r\n        \"\"\"Generates a persistent color (R,G,B) for the local squid based on its node ID.\"\"\"\r\n        if not hasattr(self, '_local_squid_color_cache'): # Cache to avoid recalculation\r\n            node_id_str = \"default_node\" # Fallback\r\n            if self.network_node and self.network_node.node_id:\r\n                node_id_str = self.network_node.node_id\r\n            \r\n            # Simple hash-like function to generate color from node_id\r\n            hash_value = 0\r\n            for char_code in node_id_str.encode('utf-8'): # Use byte values of chars\r\n                hash_value = (hash_value * 37 + char_code) & 0xFFFFFF # Keep it within 24-bit color range\r\n            \r\n            r = (hash_value >> 16) & 0xFF\r\n            g = (hash_value >> 8) & 0xFF\r\n            b = hash_value & 0xFF\r\n            \r\n            # Ensure colors are reasonably bright and distinct\r\n            r = max(80, min(r, 220)) \r\n            g = max(80, min(g, 220))\r\n            b = max(80, min(b, 220))\r\n            \r\n            self._local_squid_color_cache = (r, g, b)\r\n        return self._local_squid_color_cache\r\n\r\n\r\n    def _get_objects_state(self) -> List[Dict]:\r\n        \"\"\"\r\n        Collects and returns a list of syncable game object states.\r\n        MODIFIED: Returns an empty list to prevent general mirroring of local items.\r\n        Items will now only transfer between instances via the explicit \"stealing/carrying\" mechanic.\r\n        \"\"\"\r\n        if not self.logger: \r\n            # This check is good practice, though an empty list is returned anyway.\r\n            print(\"MPPluginLogic ERRA: Logger not available in _get_objects_state\")\r\n            return [] \r\n        \r\n        # To stop broadcasting general local items like rocks, food, decorations, etc.,\r\n        # simply return an empty list. The 'object_sync' message will still send squid state\r\n        # and any other essential global state, but not these common environmental items.\r\n        if self.debug_mode:\r\n            self.logger.debug(\"_get_objects_state: Intentionally returning empty list to prevent general item mirroring.\")\r\n        \r\n        return []   # Return an empty list (do not sync decoration items)\r\n\r\n    def _determine_object_type(self, scene_item: QtWidgets.QGraphicsItem) -> str:\r\n        \"\"\"Determines a string type for a scene item based on attributes or filename.\"\"\"\r\n        # Prioritize explicit 'category' or 'object_type' attributes\r\n        if hasattr(scene_item, 'category') and isinstance(getattr(scene_item, 'category'), str):\r\n            return getattr(scene_item, 'category')\r\n        if hasattr(scene_item, 'object_type') and isinstance(getattr(scene_item, 'object_type'), str):\r\n            return getattr(scene_item, 'object_type')\r\n\r\n        # Fallback to filename analysis if it's a QGraphicsPixmapItem with a filename\r\n        if isinstance(scene_item, QtWidgets.QGraphicsPixmapItem) and hasattr(scene_item, 'filename'):\r\n            filename_lower = getattr(scene_item, 'filename', '').lower()\r\n            if not filename_lower: return 'unknown_pixmap' # If filename is empty\r\n\r\n            if 'rock' in filename_lower: return 'rock'\r\n            if any(food_kw in filename_lower for food_kw in ['food', 'sushi', 'apple', 'cheese', 'berry']): return 'food'\r\n            if 'poop' in filename_lower: return 'poop'\r\n            # Check common decoration paths or keywords\r\n            if os.path.join(\"images\", \"decoration\") in filename_lower.replace(\"\\\\\", \"/\") or \\\r\n               any(kw in filename_lower for kw in ['decor', 'plant', 'toy', 'shell', 'coral', 'starfish', 'gem']):\r\n                return 'decoration'\r\n                \r\n        return 'generic_item' # Default if type cannot be determined\r\n\r\n\r\n    def handle_object_sync(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles incoming 'object_sync' messages from remote peers.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n\r\n        try:\r\n            sync_payload = message.get('payload', {})\r\n            remote_squid_state = sync_payload.get('squid', {}) # Data about the sender's squid\r\n            remote_objects_list = sync_payload.get('objects', []) # List of objects from sender\r\n            source_node_info = sync_payload.get('node_info', {})\r\n            sender_node_id = source_node_info.get('id') or remote_squid_state.get('node_id') # Get sender's ID\r\n\r\n            if not sender_node_id:\r\n                if self.debug_mode: self.logger.warning(\"Received object_sync with no identifiable sender node_id.\")\r\n                return\r\n            \r\n            if self.network_node and sender_node_id == self.network_node.node_id: return # Ignore own sync messages\r\n\r\n            # Update the visual of the remote squid based on their state\r\n            if remote_squid_state:\r\n                # If entity_manager exists, it will handle the update. Otherwise, basic update.\r\n                if self.entity_manager:\r\n                    self.entity_manager.update_remote_squid(sender_node_id, remote_squid_state, is_new_arrival=False)\r\n                else:\r\n                    self.update_remote_squid(sender_node_id, remote_squid_state, is_new_arrival=False) # Fallback\r\n\r\n            # Process remote objects\r\n            if remote_objects_list:\r\n                active_cloned_ids_for_this_sender = set()\r\n                for remote_obj_data in remote_objects_list:\r\n                    if not all(k in remote_obj_data for k in ['id', 'type', 'x', 'y', 'filename']):\r\n                        if self.debug_mode: self.logger.debug(f\"Skipping incomplete remote object data from {sender_node_id}: {remote_obj_data.get('id', 'No ID')}\")\r\n                        continue\r\n                    \r\n                    original_id_from_sender = remote_obj_data['id']\r\n                    # Create a unique ID for the clone in this instance's scene\r\n                    clone_id = f\"clone_{sender_node_id}_{original_id_from_sender}\"\r\n                    active_cloned_ids_for_this_sender.add(clone_id)\r\n                    \r\n                    # Process (create or update) the visual clone of the remote object\r\n                    self.process_remote_object(remote_obj_data, sender_node_id, clone_id)\r\n                \r\n                # Cleanup: Remove clones of objects that are no longer in the sender's sync list\r\n                with self.remote_objects_lock: # Ensure thread safety for self.remote_objects\r\n                    ids_to_remove = [\r\n                        obj_id for obj_id, obj_info in self.remote_objects.items()\r\n                        if obj_info.get('source_node') == sender_node_id and obj_id not in active_cloned_ids_for_this_sender\r\n                    ]\r\n                    for obj_id_to_remove in ids_to_remove:\r\n                        self.remove_remote_object(obj_id_to_remove) # Method to remove visual and from dict\r\n\r\n            # Optional: Trigger local squid's reaction to seeing a remote squid\r\n            if self.tamagotchi_logic.squid and hasattr(self.tamagotchi_logic.squid, 'process_squid_detection') and remote_squid_state:\r\n                # Pass remote_squid_state as remote_squid_props for position-based fleeing\r\n                self.tamagotchi_logic.squid.process_squid_detection(\r\n                    remote_node_id=sender_node_id, is_visible=True, remote_squid_props=remote_squid_state\r\n                )\r\n        except Exception as e:\r\n            if self.debug_mode: self.logger.error(f\"Handling object_sync from {addr} failed: {e}\", exc_info=True)\r\n\r\n\r\n    def process_remote_object(self, remote_obj_data: Dict, source_node_id: str, clone_id: str):\r\n        \"\"\"\r\n        Creates or updates a visual clone of a remote object in the local scene.\r\n        MODIFIED: Selectively ignores common environmental items to prevent general mirroring.\r\n        \"\"\"\r\n        if not self.logger: \r\n            print(\"MPPluginLogic ERRA: Logger not available in process_remote_object\") # Basic fallback print\r\n            return \r\n\r\n        # --- NEW: SELECTIVE PROCESSING TO PREVENT MIRRORING OF COMMON LOCAL ITEMS ---\r\n        # The 'type' field in remote_obj_data is set by _determine_object_type \r\n        # from _get_objects_state on the sender's side.\r\n        item_type_from_remote = remote_obj_data.get('type', 'unknown').lower()\r\n        \r\n        # Define types that should NOT be mirrored through general sync.\r\n        # These items should be local to each tank unless explicitly carried over.\r\n        types_to_ignore_for_mirroring = ['rock', 'urchin', 'food', 'poop', 'decoration'] \r\n\r\n        if item_type_from_remote in types_to_ignore_for_mirroring:\r\n            # If a mirrored clone of this type of item already exists in our scene \r\n            # (e.g., from before this filtering logic was active, or if the other client is still sending),\r\n            # we should remove it to enforce the non-mirroring rule.\r\n            if clone_id in self.remote_objects: # self.remote_objects tracks visual clones this instance created\r\n                if self.debug_mode:\r\n                    self.logger.debug(f\"process_remote_object: Removing existing undesired mirrored clone '{clone_id}' (type: '{item_type_from_remote}') from node {source_node_id}.\")\r\n                self.remove_remote_object(clone_id) # This method should handle removal from scene and self.remote_objects\r\n            \r\n            if self.debug_mode:\r\n                self.logger.debug(f\"process_remote_object: Ignoring general sync for remote object type '{item_type_from_remote}' (Original ID: {remote_obj_data.get('id', 'N/A')}, Clone ID: {clone_id}) from node {source_node_id}. This item type should only transfer via explicit stealing/carrying.\")\r\n            return # Stop further processing for this item, thus preventing its mirroring.\r\n        # --- END NEW SELECTIVE PROCESSING ---\r\n\r\n        # Original continuation of the method for item types that ARE allowed to be mirrored,\r\n        # or for other types of shared objects if you have any.\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): \r\n            self.logger.warning(\"process_remote_object: TamagotchiLogic or UI not available, cannot process item further.\")\r\n            return\r\n        \r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n        \r\n        # The rest of this method is the original logic for creating/updating clones.\r\n        # It will now only apply to items NOT filtered out by the block above.\r\n        # For example, if you had a special shared item type like 'portal' or 'shared_toy'\r\n        # that you *did* want mirrored, its processing would continue here.\r\n\r\n        base_filename = os.path.basename(remote_obj_data.get('filename', 'unknown_sync_item.png'))\r\n        \r\n        # Asset path resolution (same as before)\r\n        resolved_filename = os.path.join(\"images\", base_filename) \r\n        if not os.path.exists(resolved_filename):\r\n            for subdir in [\"decoration\", \"items\", \"food\", \"rocks\"]: # Add other relevant subdirs if needed\r\n                path_attempt = os.path.join(\"images\", subdir, base_filename)\r\n                if os.path.exists(path_attempt):\r\n                    resolved_filename = path_attempt\r\n                    break\r\n            else: \r\n                if self.debug_mode: self.logger.warning(f\"process_remote_object: Image for allowed sync item '{base_filename}' (type: '{item_type_from_remote}') not found locally for clone '{clone_id}'. Skipping visual.\")\r\n                return\r\n\r\n        # Thread safety for self.remote_objects dictionary\r\n        with self.remote_objects_lock: \r\n            if clone_id in self.remote_objects: # If clone already exists, update it\r\n                existing_clone_info = self.remote_objects[clone_id]\r\n                visual_item = existing_clone_info['visual']\r\n                \r\n                # Update existing visual item's properties\r\n                visual_item.setPos(remote_obj_data['x'], remote_obj_data['y'])\r\n                visual_item.setScale(remote_obj_data.get('scale', 1.0))\r\n                visual_item.setZValue(remote_obj_data.get('zValue', -5)) # Default Z for background items\r\n                visual_item.setVisible(not remote_obj_data.get('is_being_carried', False)) # Hide if carried by remote squid\r\n                \r\n                existing_clone_info['last_update'] = time.time()\r\n                existing_clone_info['data'] = remote_obj_data # Store latest data\r\n                \r\n                # Ensure tint is applied if it's meant to be foreign (clones always are)\r\n                if not getattr(visual_item, 'is_foreign', False): # Should always be true for clones\r\n                     self.apply_foreign_object_tint(visual_item)\r\n                if self.debug_mode:\r\n                    self.logger.debug(f\"process_remote_object: Updated mirrored clone '{clone_id}' (type: '{item_type_from_remote}') from {source_node_id}.\")\r\n\r\n            else: # New clone for an allowed item type, create it\r\n                if remote_obj_data.get('is_being_carried', False): # Don't create visual if it's being carried by the remote squid\r\n                    if self.debug_mode:\r\n                        self.logger.debug(f\"process_remote_object: Item '{clone_id}' (type: '{item_type_from_remote}') is being carried remotely. Not creating visual clone yet.\")\r\n                    return\r\n\r\n                try:\r\n                    pixmap = QtGui.QPixmap(resolved_filename)\r\n                    if pixmap.isNull():\r\n                        if self.debug_mode: self.logger.error(f\"process_remote_object: Failed to load QPixmap for allowed remote object '{resolved_filename}'.\")\r\n                        return\r\n                    \r\n                    cloned_visual = QtWidgets.QGraphicsPixmapItem(pixmap)\r\n                    cloned_visual.setPos(remote_obj_data['x'], remote_obj_data['y'])\r\n                    cloned_visual.setScale(remote_obj_data.get('scale', 1.0))\r\n                    # Cloned objects are typically less prominent than remote squids themselves\r\n                    cloned_visual.setOpacity(self.REMOTE_SQUID_OPACITY * 0.65) # Example: Slightly more transparent\r\n                    cloned_visual.setZValue(remote_obj_data.get('zValue', -5)) # Default Z for background items\r\n                    \r\n                    setattr(cloned_visual, 'filename', resolved_filename) \r\n                    setattr(cloned_visual, 'is_remote_clone', True) # Mark as a clone from a remote instance\r\n                    setattr(cloned_visual, 'original_id_from_sender', remote_obj_data['id'])\r\n                    \r\n                    self.apply_foreign_object_tint(cloned_visual) # Apply visual tint\r\n                    scene.addItem(cloned_visual)\r\n                    \r\n                    self.remote_objects[clone_id] = {\r\n                        'visual': cloned_visual, \r\n                        'type': item_type_from_remote, # Store the determined type\r\n                        'source_node': source_node_id, \r\n                        'last_update': time.time(), \r\n                        'data': remote_obj_data # Store all received data\r\n                    }\r\n                    if self.debug_mode:\r\n                        self.logger.debug(f\"process_remote_object: Created new mirrored clone '{clone_id}' (type: '{item_type_from_remote}') from {source_node_id}.\")\r\n                except Exception as e_create_clone:\r\n                    if self.debug_mode: self.logger.error(f\"process_remote_object: Creating visual clone for allowed item '{clone_id}' failed: {e_create_clone}\", exc_info=True)\r\n\r\n\r\n    def handle_heartbeat(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles heartbeat messages from other peers.\"\"\"\r\n        if not self.logger: return\r\n        if not self.network_node: return # This instance's network node must exist\r\n\r\n        sender_node_id = message.get('payload', {}).get('node_id') # Heartbeat payload contains sender's ID\r\n        if not sender_node_id or sender_node_id == self.network_node.node_id: return # Ignore own or invalid heartbeats\r\n\r\n        # Update UI (status widget or bar) with known peers\r\n        if self.status_widget:\r\n            self.status_widget.update_peers(self.network_node.known_nodes) # known_nodes is updated by NetworkNode\r\n            if sender_node_id not in self.remote_squids: # If this is the first sign of this peer\r\n                self.status_widget.add_activity(f\"Peer {sender_node_id[-6:]} detected via heartbeat.\")\r\n        elif self.status_bar and hasattr(self.status_bar, 'update_peers_count'):\r\n            self.status_bar.update_peers_count(len(self.network_node.known_nodes))\r\n\r\n        heartbeat_payload = message.get('payload', {})\r\n        squid_pos_data = heartbeat_payload.get('squid_pos') # Heartbeat might include basic position\r\n        \r\n        # If this peer is new and sent position, create a basic placeholder visual\r\n        if squid_pos_data and sender_node_id not in self.remote_squids:\r\n            if self.debug_mode: self.logger.debug(f\"Creating placeholder for {sender_node_id[-6:]} from heartbeat.\")\r\n            placeholder_squid_data = {\r\n                'x': squid_pos_data[0], 'y': squid_pos_data[1], 'direction': 'right', # Default direction\r\n                'color': (150, 150, 150), 'node_id': sender_node_id, 'status': 'detected_via_heartbeat',\r\n                'squid_width': 60, 'squid_height': 40 # Default dimensions\r\n            }\r\n            # Use entity_manager if available, otherwise fallback\r\n            if self.entity_manager:\r\n                self.entity_manager.update_remote_squid(sender_node_id, placeholder_squid_data, is_new_arrival=True)\r\n            else:\r\n                self.update_remote_squid(sender_node_id, placeholder_squid_data, is_new_arrival=True)\r\n\r\n\r\n    def update_remote_squid(self, remote_node_id: str, squid_data_dict: Dict, is_new_arrival=False, high_visibility=False):\r\n        \"\"\"Updates or creates the visual representation of a remote squid.\r\n           This method now primarily defers to self.entity_manager if available.\"\"\"\r\n        if not self.logger: return False\r\n        \r\n        if self.entity_manager:\r\n            # entity_manager.update_remote_squid will handle all visual creation and updates\r\n            # Pass is_new_arrival along, high_visibility is implicitly handled by static display now\r\n            success = self.entity_manager.update_remote_squid(remote_node_id, squid_data_dict, is_new_arrival)\r\n            if success and is_new_arrival:\r\n                # If entity_manager handled it, log for clarity, autopilot logic is in handle_squid_exit.\r\n                if self.debug_mode: self.logger.debug(f\"RemoteEntityManager handled update/creation for {remote_node_id}.\")\r\n            elif not success:\r\n                 if self.debug_mode: self.logger.warning(f\"RemoteEntityManager failed to update/create {remote_node_id}.\")\r\n            return success\r\n\r\n        # --- Fallback logic if entity_manager is NOT available (original basic logic) ---\r\n        self.logger.warning(\"entity_manager NOT found. Using fallback remote squid update logic.\")\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return False\r\n        if not squid_data_dict or not all(key in squid_data_dict for key in ['x', 'y', 'direction']):\r\n            if self.debug_mode: self.logger.warning(f\"Fallback: Insufficient data for remote squid {remote_node_id}.\")\r\n            return False\r\n        \r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n        with self.remote_squids_lock: # Ensure lock for fallback access too\r\n            existing_squid_display = self.remote_squids.get(remote_node_id)\r\n            if existing_squid_display:\r\n                visual = existing_squid_display.get('visual')\r\n                id_text = existing_squid_display.get('id_text')\r\n                status_text = existing_squid_display.get('status_text')\r\n\r\n                if visual:\r\n                    visual.setPos(squid_data_dict['x'], squid_data_dict['y'])\r\n                    visual.setOpacity(self.REMOTE_SQUID_OPACITY) # Fallback ensure opacity\r\n                    visual.setScale(1.0) # Fallback ensure scale\r\n                    self.update_remote_squid_image(existing_squid_display, squid_data_dict['direction'])\r\n                \r\n                new_status_str = \"ARRIVING\" if is_new_arrival else squid_data_dict.get('status', 'active')\r\n                if id_text: id_text.setPos(squid_data_dict['x'], squid_data_dict['y'] - 50) # Adjust as needed\r\n                if status_text:\r\n                    status_text.setPlainText(new_status_str)\r\n                    status_text.setPos(squid_data_dict['x'], squid_data_dict['y'] - 35) # Adjust\r\n                    # Static text styling, no animation-dependent changes\r\n                    status_text.setDefaultTextColor(QtGui.QColor(200,200,200,230))\r\n                    status_text.setFont(QtGui.QFont(\"Arial\", 9))\r\n                    if is_new_arrival: # Slight emphasis for new arrivals' status\r\n                        status_text.setDefaultTextColor(QtGui.QColor(255, 223, 0)) # Gold\r\n                        status_text.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\r\n\r\n                existing_squid_display['data'] = squid_data_dict\r\n                existing_squid_display['last_update'] = time.time()\r\n            else: # New squid, fallback creation\r\n                try:\r\n                    initial_direction = squid_data_dict.get('direction', 'right')\r\n                    # Attempt to load image, fallback to placeholder\r\n                    base_image_path = \"images\"\r\n                    squid_image_file = f\"{initial_direction.lower()}1.png\"\r\n                    full_image_path = os.path.join(base_image_path, squid_image_file)\r\n                    \r\n                    squid_pixmap = QtGui.QPixmap(full_image_path)\r\n                    if squid_pixmap.isNull():\r\n                        self.logger.warning(f\"Fallback: Image {full_image_path} not found. Using color placeholder.\")\r\n                        squid_width = squid_data_dict.get('squid_width', 60)\r\n                        squid_height = squid_data_dict.get('squid_height', 40)\r\n                        squid_pixmap = QtGui.QPixmap(int(squid_width), int(squid_height))\r\n                        squid_color_tuple = squid_data_dict.get('color', (100,150,255)) # Default color\r\n                        squid_pixmap.fill(QtGui.QColor(*squid_color_tuple))\r\n\r\n                    visual = QtWidgets.QGraphicsPixmapItem(squid_pixmap)\r\n                    visual.setPos(squid_data_dict['x'], squid_data_dict['y'])\r\n                    visual.setOpacity(self.REMOTE_SQUID_OPACITY) # Full opacity (1.0)\r\n                    visual.setScale(1.0) # Normal scale\r\n                    visual.setZValue(5) # Default Z order\r\n                    scene.addItem(visual)\r\n\r\n                    # ID Text\r\n                    display_id_str = f\"{remote_node_id[-6:]}\" # Show last 6 chars of ID\r\n                    id_text = scene.addText(display_id_str) # Basic text item\r\n                    id_text.setPos(squid_data_dict['x'], squid_data_dict['y'] - 50) # Position above visual\r\n                    id_text.setFont(QtGui.QFont(\"Arial\", 8))\r\n                    id_text.setDefaultTextColor(QtGui.QColor(200,200,200,180)) # Semi-transparent white\r\n                    id_text.setZValue(6) # Above squid visual\r\n                    id_text.setVisible(self.SHOW_REMOTE_LABELS)\r\n\r\n                    # Status Text\r\n                    status_str = \"ARRIVING\" if is_new_arrival else squid_data_dict.get('status', 'active')\r\n                    status_text = scene.addText(status_str)\r\n                    status_text.setPos(squid_data_dict['x'], squid_data_dict['y'] - 35) # Position above visual\r\n                    status_text.setFont(QtGui.QFont(\"Arial\", 9))\r\n                    status_text.setDefaultTextColor(QtGui.QColor(200,200,200,230))\r\n                    if is_new_arrival: # Emphasize for new arrivals\r\n                        status_text.setDefaultTextColor(QtGui.QColor(255, 215, 0)) # Gold\r\n                        status_text.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\r\n                    status_text.setZValue(6) # Above squid visual\r\n                    status_text.setVisible(self.SHOW_REMOTE_LABELS)\r\n                    \r\n                    new_squid_display_data = {\r\n                        'visual': visual, 'id_text': id_text, 'status_text': status_text,\r\n                        'view_cone': None, 'last_update': time.time(), 'data': squid_data_dict\r\n                    }\r\n                    self.remote_squids[remote_node_id] = new_squid_display_data\r\n                    # update_remote_squid_image is implicitly handled by direct pixmap load or placeholder\r\n                    if self.debug_mode: self.logger.debug(f\"Fallback: Created static remote squid visual for {remote_node_id}.\")\r\n\r\n                except Exception as e_create_squid_fb:\r\n                    self.logger.error(f\"Fallback: Creating remote squid visual for {remote_node_id} failed: {e_create_squid_fb}\", exc_info=True)\r\n                    if remote_node_id in self.remote_squids: del self.remote_squids[remote_node_id] # Cleanup partial creation\r\n                    return False\r\n        return True\r\n\r\n\r\n    def _create_enhanced_arrival_animation(self, squid_visual_item: QtWidgets.QGraphicsPixmapItem, at_x: float, at_y: float):\r\n        \"\"\"MODIFIED: Creates a more prominent visual animation (now static) for newly arriving remote squids.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        # scene = self.tamagotchi_logic.user_interface.scene # Scene not used if no visual effects\r\n\r\n        if squid_visual_item:\r\n            squid_visual_item.setOpacity(self.REMOTE_SQUID_OPACITY) # Ensure full visibility (1.0)\r\n            squid_visual_item.setScale(1.0) # Ensure normal scale\r\n            if squid_visual_item.graphicsEffect(): # Remove any prior effect\r\n                 squid_visual_item.setGraphicsEffect(None)\r\n        \r\n        if self.debug_mode:\r\n            self.logger.debug(f\"Static display for enhanced arrival at ({at_x}, {at_y}).\")\r\n\r\n\r\n    def handle_remote_squid_return(self, remote_node_id: str, controller: Any): # Type hint for controller if available\r\n        \"\"\"Initiates the process for a remote squid (controlled by autopilot) to return to its home instance.\"\"\"\r\n        if not self.logger: return\r\n        if self.debug_mode: self.logger.debug(f\"Remote squid {remote_node_id[-6:]} is being returned home by its controller.\")\r\n\r\n        activity_summary_data = controller.get_summary() # Get summary of activities from controller\r\n        home_direction_for_exit = controller.home_direction # Direction it should exit this screen\r\n\r\n        # If entity_manager is handling visuals, tell it to start the removal process\r\n        if self.entity_manager and hasattr(self.entity_manager, 'initiate_squid_departure_animation'):\r\n            self.entity_manager.initiate_squid_departure_animation(\r\n                remote_node_id,\r\n                lambda: self.complete_remote_squid_return(remote_node_id, activity_summary_data, home_direction_for_exit)\r\n            )\r\n            if hasattr(self.tamagotchi_logic, 'show_message'):\r\n                 self.tamagotchi_logic.show_message(f\"👋 Visitor squid {remote_node_id[-6:]} is heading back home!\")\r\n            return\r\n\r\n        # Fallback: Basic immediate removal or simple fade if no entity_manager animation\r\n        remote_squid_display_info = self.remote_squids.get(remote_node_id)\r\n        if not remote_squid_display_info or not remote_squid_display_info.get('visual'):\r\n            if self.debug_mode: self.logger.warning(f\"Visual for returning remote squid {remote_node_id[-6:]} not found. Completing return directly.\")\r\n            self.complete_remote_squid_return(remote_node_id, activity_summary_data, home_direction_for_exit)\r\n            return\r\n\r\n        visual_item = remote_squid_display_info['visual']\r\n        status_text = remote_squid_display_info.get('status_text')\r\n        if status_text: # Update status text\r\n            status_text.setPlainText(\"RETURNING HOME...\")\r\n            status_text.setDefaultTextColor(QtGui.QColor(255, 165, 0)) # Orange\r\n            status_text.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\r\n        \r\n        # For static testing, remove immediately after sending message\r\n        self.logger.info(f\"Static removal for remote squid {remote_node_id[-6:]} returning home.\")\r\n        self.complete_remote_squid_return(remote_node_id, activity_summary_data, home_direction_for_exit)\r\n        \r\n        if hasattr(self.tamagotchi_logic, 'show_message'):\r\n            self.tamagotchi_logic.show_message(f\"👋 Visitor squid {remote_node_id[-6:]} is heading back home!\")\r\n\r\n\r\n    def complete_remote_squid_return(self, remote_node_id: str, activity_summary: Dict, exit_direction: str):\r\n        \"\"\"Finalizes the return of a remote squid: sends message and removes local visuals.\"\"\"\r\n        if not self.logger: return\r\n        try:\r\n            # Send 'squid_return' message to the network so the original instance knows its squid is back\r\n            if self.network_node and self.network_node.is_connected:\r\n                return_message_payload = {\r\n                    'node_id': remote_node_id, # ID of the squid that is returning\r\n                    'activity_summary': activity_summary, # What it did on this instance\r\n                    'return_direction': exit_direction # How it should re-enter its home screen\r\n                }\r\n                self.network_node.send_message('squid_return', return_message_payload)\r\n                if self.debug_mode:\r\n                    rocks = activity_summary.get('rocks_stolen',0)\r\n                    self.logger.info(f\"Sent 'squid_return' for {remote_node_id[-6:]} (summary: {rocks} rocks). Exit dir: {exit_direction}\")\r\n            \r\n            # Remove the remote squid's visual representation from this instance\r\n            if self.entity_manager:\r\n                self.entity_manager.remove_remote_squid(remote_node_id)\r\n            else: # Fallback\r\n                self.remove_remote_squid(remote_node_id) # This plugin's method for basic removal\r\n\r\n            # Remove its controller\r\n            if remote_node_id in self.remote_squid_controllers:\r\n                del self.remote_squid_controllers[remote_node_id]\r\n                if self.debug_mode: self.logger.info(f\"Removed controller for returned remote squid {remote_node_id[-6:]}.\")\r\n        except Exception as e:\r\n            self.logger.error(f\"Completing remote squid return for {remote_node_id[-6:]} failed: {e}\", exc_info=True)\r\n\r\n\r\n    def update_remote_view_cone(self, remote_node_id: str, remote_squid_data: Dict):\r\n        \"\"\"Updates the visual representation of a remote squid's view cone.\r\n           This is the mp_plugin_logic's own implementation, potentially a fallback if entity_manager is not used\r\n           or if this plugin needs to draw cones for controllers it manages directly.\"\"\"\r\n        if not self.logger: return\r\n        \r\n        # If entity_manager is present and handles view cones, defer to it.\r\n        if self.entity_manager and hasattr(self.entity_manager, 'update_remote_view_cone'):\r\n            # Ensure entity_manager's update_remote_view_cone is compatible or adapt the call\r\n            # This assumes entity_manager.update_remote_view_cone(node_id, data) signature\r\n            self.entity_manager.update_remote_view_cone(remote_node_id, remote_squid_data)\r\n            return\r\n\r\n        # --- Fallback or direct implementation if no entity_manager for this ---\r\n        if not self.SHOW_REMOTE_LABELS: # View cones are often tied to label visibility\r\n            if remote_node_id in self.remote_squids and self.remote_squids[remote_node_id].get('view_cone'):\r\n                self._remove_view_cone_for_squid(remote_node_id) # Helper to remove existing cone\r\n            return\r\n\r\n        if remote_node_id not in self.remote_squids or not self.tamagotchi_logic or \\\r\n           not hasattr(self.tamagotchi_logic, 'user_interface'):\r\n            return\r\n\r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n        squid_display_info = self.remote_squids[remote_node_id] # This plugin's own remote_squids dict\r\n        \r\n        # Remove existing cone if any\r\n        self._remove_view_cone_for_squid(remote_node_id) \r\n        \r\n        if not remote_squid_data.get('view_cone_visible', False): # If cone should not be visible\r\n            return\r\n\r\n        # Get squid's visual item from this plugin's tracking (if fallback)\r\n        visual_item = squid_display_info.get('visual') \r\n        if not visual_item: \r\n            if self.debug_mode: self.logger.warning(f\"Fallback: No visual item for {remote_node_id} to draw view cone.\")\r\n            return\r\n\r\n        # Use visual item's current position and size for cone origin\r\n        squid_center_x = visual_item.pos().x() + visual_item.boundingRect().width() / 2\r\n        squid_center_y = visual_item.pos().y() + visual_item.boundingRect().height() / 2\r\n        \r\n        looking_direction_rad = remote_squid_data.get('looking_direction', 0.0) # In radians\r\n        view_cone_angle_rad = remote_squid_data.get('view_cone_angle', math.radians(50)) # Default cone angle\r\n        cone_half_angle = view_cone_angle_rad / 2.0\r\n        cone_length = 150 # Length of the cone\r\n\r\n        # Calculate points of the cone triangle\r\n        point1_origin = QtCore.QPointF(squid_center_x, squid_center_y)\r\n        point2_edge1 = QtCore.QPointF(\r\n            squid_center_x + cone_length * math.cos(looking_direction_rad - cone_half_angle),\r\n            squid_center_y + cone_length * math.sin(looking_direction_rad - cone_half_angle)\r\n        )\r\n        point3_edge2 = QtCore.QPointF(\r\n            squid_center_x + cone_length * math.cos(looking_direction_rad + cone_half_angle),\r\n            squid_center_y + cone_length * math.sin(looking_direction_rad + cone_half_angle)\r\n        )\r\n        \r\n        cone_polygon = QtGui.QPolygonF([point1_origin, point2_edge1, point3_edge2])\r\n        new_cone_item = QtWidgets.QGraphicsPolygonItem(cone_polygon)\r\n        \r\n        squid_color = remote_squid_data.get('color', (150, 150, 255)) # Use squid's color for cone\r\n        try:\r\n            cone_q_color = QtGui.QColor(*squid_color)\r\n        except TypeError:\r\n            cone_q_color = QtGui.QColor(150,150,255) # Fallback color\r\n\r\n        new_cone_item.setPen(QtGui.QPen(QtGui.QColor(cone_q_color.red(), cone_q_color.green(), cone_q_color.blue(), 0))) # Transparent border\r\n        new_cone_item.setBrush(QtGui.QBrush(QtGui.QColor(cone_q_color.red(), cone_q_color.green(), cone_q_color.blue(), 25))) # Semi-transparent fill\r\n        new_cone_item.setZValue(visual_item.zValue() - 1) # Draw behind squid\r\n        \r\n        scene.addItem(new_cone_item)\r\n        squid_display_info['view_cone'] = new_cone_item # Store reference\r\n\r\n\r\n    def _remove_view_cone_for_squid(self, remote_node_id: str):\r\n        \"\"\"Safely removes a view cone for a specific remote squid (used by fallback logic).\"\"\"\r\n        if not self.logger: return\r\n        # This check is for this plugin's remote_squids dictionary\r\n        if remote_node_id in self.remote_squids and self.tamagotchi_logic and hasattr(self.tamagotchi_logic.user_interface, 'scene'):\r\n            squid_display_info = self.remote_squids[remote_node_id]\r\n            cone_item = squid_display_info.get('view_cone')\r\n            if cone_item and cone_item.scene(): # Check if it has a scene before removing\r\n                self.tamagotchi_logic.user_interface.scene.removeItem(cone_item)\r\n            squid_display_info['view_cone'] = None # Clear reference\r\n\r\n\r\n    def create_gift_decoration(self, from_remote_node_id: str) -> QtWidgets.QGraphicsPixmapItem | None:\r\n        \"\"\"Creates a new decoration item representing a received gift.\"\"\"\r\n        if not self.logger: return None\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return None\r\n        \r\n        ui = self.tamagotchi_logic.user_interface\r\n        scene = ui.scene\r\n        \r\n        available_decoration_images = []\r\n        decoration_image_dirs = [os.path.join(\"images\", \"decoration\"), \"images\"] # Search paths\r\n        for img_dir in decoration_image_dirs:\r\n            if os.path.exists(img_dir):\r\n                for filename in os.listdir(img_dir):\r\n                    if filename.lower().endswith(('.png', '.jpg', '.gif')) and \\\r\n                       any(kw in filename.lower() for kw in ['decor', 'plant', 'toy', 'shell', 'coral', 'starfish', 'gem']): # Keywords for decorations\r\n                        available_decoration_images.append(os.path.join(img_dir, filename))\r\n        \r\n        if not available_decoration_images: # Fallback if no specific decorations found\r\n            default_gift_img = os.path.join(\"images\", \"plant.png\") # Example default\r\n            if not os.path.exists(default_gift_img):\r\n                if self.debug_mode: self.logger.warning(\"Default gift image 'plant.png' not found. Cannot create gift.\")\r\n                return None\r\n            available_decoration_images.append(default_gift_img)\r\n\r\n        chosen_gift_image_path = random.choice(available_decoration_images)\r\n        \r\n        try:\r\n            gift_pixmap = QtGui.QPixmap(chosen_gift_image_path)\r\n            if gift_pixmap.isNull():\r\n                if self.debug_mode: self.logger.error(f\"Failed to load gift image '{chosen_gift_image_path}'.\")\r\n                return None\r\n\r\n            gift_item = None\r\n            if hasattr(ui, 'ResizablePixmapItem'): # Use custom item if available\r\n                gift_item = ui.ResizablePixmapItem(gift_pixmap, chosen_gift_image_path)\r\n            else: # Use standard QGraphicsPixmapItem\r\n                gift_item = QtWidgets.QGraphicsPixmapItem(gift_pixmap)\r\n                setattr(gift_item, 'filename', chosen_gift_image_path) # Store filename\r\n\r\n            setattr(gift_item, 'category', 'decoration')\r\n            setattr(gift_item, 'is_gift_from_remote', True) # Mark as a received gift\r\n            setattr(gift_item, 'received_from_node', from_remote_node_id) # Store sender\r\n            gift_item.setToolTip(f\"A surprise gift from tank {from_remote_node_id[-6:]}!\")\r\n\r\n            # Position the gift randomly but within bounds\r\n            item_width = gift_pixmap.width() * gift_item.scale() # Account for scale\r\n            item_height = gift_pixmap.height() * gift_item.scale()\r\n            max_placement_x = ui.window_width - item_width - 30 # 30px margin\r\n            max_placement_y = ui.window_height - item_height - 30\r\n            gift_pos_x = random.uniform(30, max(30, max_placement_x))\r\n            gift_pos_y = random.uniform(30, max(30, max_placement_y))\r\n            gift_item.setPos(gift_pos_x, gift_pos_y)\r\n            \r\n            self.apply_foreign_object_tint(gift_item) # Apply tint to show it's from remote\r\n            scene.addItem(gift_item)\r\n            \r\n            # Static display for gift, no complex animation\r\n            gift_item.setOpacity(0.0) # Start transparent\r\n            # MODIFIED: For static testing, make it immediately visible\r\n            gift_item.setOpacity(1.0)\r\n\r\n\r\n            # Optional: Add a temporary \"🎁 Gift!\" text label above it\r\n            gift_indicator_label = scene.addText(\"🎁 Gift!\")\r\n            label_font = QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold)\r\n            gift_indicator_label.setFont(label_font)\r\n            gift_indicator_label.setDefaultTextColor(QtGui.QColor(255, 100, 100)) # Bright color\r\n            label_x = gift_pos_x + (item_width / 2) - (gift_indicator_label.boundingRect().width() / 2)\r\n            label_y = gift_pos_y - gift_indicator_label.boundingRect().height() - 5 # Above gift\r\n            gift_indicator_label.setPos(label_x, label_y)\r\n            gift_indicator_label.setZValue(gift_item.zValue() + 1) # Ensure label is on top\r\n            # Make label disappear after a few seconds\r\n            QtCore.QTimer.singleShot(4000, lambda item=gift_indicator_label: item.scene().removeItem(item) if item.scene() else None)\r\n\r\n            return gift_item\r\n        except Exception as e_gift:\r\n            if self.debug_mode: self.logger.error(f\"Error creating gift decoration: {e_gift}\", exc_info=True)\r\n            return None\r\n\r\n\r\n    def remove_remote_squid(self, node_id_to_remove: str):\r\n        \"\"\"Removes visual components of a specific remote squid (used by fallback or direct management).\"\"\"\r\n        if not self.logger: return\r\n        if node_id_to_remove not in self.remote_squids: return # Not in this plugin's tracking\r\n        \r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n\r\n        with self.remote_squids_lock: # Thread safety for self.remote_squids\r\n            squid_display_elements = self.remote_squids.pop(node_id_to_remove, None)\r\n        \r\n        if squid_display_elements:\r\n            visual_keys = ['visual', 'view_cone', 'id_text', 'status_text']\r\n            for key in visual_keys:\r\n                item_to_remove = squid_display_elements.get(key)\r\n                if item_to_remove and item_to_remove.scene(): # Check if item has a scene\r\n                    scene.removeItem(item_to_remove)\r\n            \r\n            # Remove associated connection line if managed by this plugin directly\r\n            if node_id_to_remove in self.connection_lines:\r\n                line = self.connection_lines.pop(node_id_to_remove)\r\n                if line.scene(): scene.removeItem(line)\r\n            \r\n            if self.debug_mode: self.logger.debug(f\"Fallback: Removed all visuals for remote squid {node_id_to_remove[-6:]}.\")\r\n\r\n        # Update UI status if network_node is still valid\r\n        if self.network_node: \r\n            if self.status_widget: self.status_widget.update_peers(self.network_node.known_nodes if self.network_node else {})\r\n            elif self.status_bar and hasattr(self.status_bar, 'update_peers_count'): self.status_bar.update_peers_count(len(self.network_node.known_nodes if self.network_node else {}))\r\n\r\n\r\n    def remove_remote_object(self, full_clone_id: str):\r\n        \"\"\"Removes a specific cloned remote object (used by fallback or direct management).\"\"\"\r\n        if not self.logger: return\r\n        if full_clone_id not in self.remote_objects: return # Not in this plugin's tracking\r\n        \r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n\r\n        with self.remote_objects_lock: # Thread safety for self.remote_objects\r\n            object_clone_info = self.remote_objects.pop(full_clone_id, None)\r\n        \r\n        if object_clone_info:\r\n            visual_item = object_clone_info.get('visual')\r\n            if visual_item and visual_item.scene(): # Check if item has a scene\r\n                scene.removeItem(visual_item)\r\n            \r\n            if self.debug_mode: self.logger.debug(f\"Fallback: Removed remote object clone: {full_clone_id}.\")\r\n\r\n\r\n    def throw_rock_network(self, rock_graphics_item: QtWidgets.QGraphicsPixmapItem, direction_thrown: str):\r\n        \"\"\"Broadcasts a 'rock_throw' event when the local squid throws a rock.\"\"\"\r\n        if not self.logger: return\r\n        if not self.network_node or not self.network_node.is_connected or not rock_graphics_item:\r\n            return # Prerequisites not met\r\n\r\n        try:\r\n            rock_filename = getattr(rock_graphics_item, 'filename', \"default_rock.png\") # Get filename\r\n            initial_pos = rock_graphics_item.pos() # Position when thrown\r\n            \r\n            rock_throw_payload = {\r\n                'rock_data': {\r\n                    'filename': rock_filename, \r\n                    'direction': direction_thrown,\r\n                    'initial_pos_x': initial_pos.x(), \r\n                    'initial_pos_y': initial_pos.y(),\r\n                    'scale': rock_graphics_item.scale() if hasattr(rock_graphics_item, 'scale') else 1.0,\r\n                }\r\n            }\r\n            self.network_node.send_message('rock_throw', rock_throw_payload) # Send message\r\n            if self.debug_mode:\r\n                self.logger.debug(f\"Broadcasted local rock throw: {os.path.basename(rock_filename)} towards {direction_thrown}.\")\r\n        except Exception as e_throw:\r\n            if self.debug_mode: self.logger.error(f\"Broadcasting rock throw failed: {e_throw}\", exc_info=True)\r\n\r\n\r\n    def cleanup(self):\r\n        \"\"\"Cleans up all resources used by the multiplayer plugin.\"\"\"\r\n        if self.logger is None: \r\n            emergency_logger = logging.getLogger(f\"{mp_constants.PLUGIN_NAME}_CleanupEmergency\")\r\n            if not emergency_logger.hasHandlers(): emergency_logger.addHandler(logging.StreamHandler())\r\n            emergency_logger.setLevel(logging.INFO)\r\n            self.logger = emergency_logger\r\n            self.logger.warning(\"Logger was None at the start of cleanup. Using emergency logger.\")\r\n\r\n        self.logger.info(f\"Initiating {mp_constants.PLUGIN_NAME} cleanup...\")\r\n        self.is_setup = False # Mark as not setup to stop background threads/timers\r\n\r\n        # Stop all QTimers\r\n        timers_to_manage = [\r\n            'message_process_timer', 'controller_update_timer', 'controller_creation_timer',\r\n            'cleanup_timer_basic', 'connection_timer_basic'\r\n        ]\r\n        for timer_attr_name in timers_to_manage:\r\n            timer_instance = getattr(self, timer_attr_name, None)\r\n            if timer_instance and isinstance(timer_instance, QtCore.QTimer) and timer_instance.isActive():\r\n                timer_instance.stop()\r\n                if self.debug_mode: self.logger.debug(f\"Stopped timer '{timer_attr_name}'.\")\r\n            setattr(self, timer_attr_name, None) # Clear reference\r\n\r\n        # Sync thread is a daemon, will exit with app. Signal it to stop if it checks is_setup.\r\n        if self.sync_thread and self.sync_thread.is_alive():\r\n             if self.debug_mode: self.logger.info(\"Sync thread was active during cleanup. As a daemon, it will exit with app or when its loop condition (is_setup) fails.\")\r\n        self.sync_thread = None # Clear reference\r\n        \r\n        # NetworkNode cleanup\r\n        if self.network_node:\r\n            nn_ref = self.network_node # Temporary reference for cleanup\r\n            self.network_node = None # Nullify early to prevent re-use during its own shutdown\r\n\r\n            if nn_ref.is_connected: # Send leave message if connected\r\n                try:\r\n                    nn_ref.send_message(\r\n                        'player_leave',\r\n                        {'node_id': nn_ref.node_id, 'reason': 'plugin_unloaded_or_disabled'}\r\n                    )\r\n                except Exception as e_leave: # Socket might already be closed\r\n                    if self.debug_mode: self.logger.error(f\"Error sending player_leave message (socket may be closed): {e_leave}\", exc_info=False) # No exc_info if expected\r\n            \r\n            # Close socket and leave multicast group\r\n            if nn_ref.socket: \r\n                try:\r\n                    if nn_ref.is_connected and hasattr(nn_ref, 'local_ip') and nn_ref.local_ip and \\\r\n                       hasattr(mp_constants, 'MULTICAST_GROUP') and mp_constants.MULTICAST_GROUP:\r\n                        import socket as sock_module # Local import for cleanup\r\n                        mreq_leave = sock_module.inet_aton(mp_constants.MULTICAST_GROUP) + sock_module.inet_aton(nn_ref.local_ip)\r\n                        nn_ref.socket.setsockopt(sock_module.IPPROTO_IP, sock_module.IP_DROP_MEMBERSHIP, mreq_leave)\r\n                except AttributeError as e_attr: \r\n                     if self.debug_mode: self.logger.warning(f\"Attribute error during multicast group leave: {e_attr}\")\r\n                except Exception as e_mcast_leave:\r\n                     if self.debug_mode: self.logger.warning(f\"Error leaving multicast group: {e_mcast_leave}\", exc_info=True)\r\n                finally:\r\n                    try:\r\n                        nn_ref.socket.close()\r\n                    except Exception: pass # Ignore errors on closing already closed socket\r\n            nn_ref.is_connected = False\r\n            nn_ref.socket = None\r\n        \r\n        # Cleanup visuals if entity_manager is not handling it or as a final sweep\r\n        if self.entity_manager:\r\n            self.entity_manager.cleanup_all() # Tell entity_manager to clean its own entities\r\n        else: # Fallback: this plugin cleans its own directly managed visuals\r\n            if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'user_interface'):\r\n                with self.remote_squids_lock:\r\n                    for node_id_key in list(self.remote_squids.keys()): self.remove_remote_squid(node_id_key)\r\n                with self.remote_objects_lock:\r\n                    for clone_id_key in list(self.remote_objects.keys()): self.remove_remote_object(clone_id_key)\r\n        \r\n        self.remote_squids.clear()\r\n        self.remote_objects.clear()\r\n        self.connection_lines.clear() # Should be empty if lines removed correctly\r\n        self.remote_squid_controllers.clear()\r\n\r\n        # Update UI status\r\n        if self.status_widget:\r\n             if hasattr(self.status_widget, 'update_connection_status'): self.status_widget.update_connection_status(False)\r\n             if hasattr(self.status_widget, 'update_peers'): self.status_widget.update_peers({})\r\n             if hasattr(self.status_widget, 'add_activity'): self.status_widget.add_activity(f\"{mp_constants.PLUGIN_NAME} has been shut down.\")\r\n        elif self.status_bar: # Fallback\r\n            if hasattr(self.status_bar, 'update_network_status'): self.status_bar.update_network_status(False)\r\n            if hasattr(self.status_bar, 'update_peers_count'): self.status_bar.update_peers_count(0)\r\n            if hasattr(self.status_bar, 'showMessage'): self.status_bar.showMessage(f\"{mp_constants.PLUGIN_NAME} plugin shut down.\", 5000)\r\n        \r\n        self.logger.info(f\"{mp_constants.PLUGIN_NAME} plugin cleanup process completed.\")\r\n\r\n\r\n    def handle_squid_move(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles discrete 'squid_move' messages (less frequent than full sync).\"\"\"\r\n        if not self.logger: return\r\n        \r\n        payload = message.get('payload', {})\r\n        sender_node_id = payload.get('node_id') # Assume payload contains node_id\r\n        \r\n        if not sender_node_id: \r\n            sender_node_id = message.get('node_id') # Fallback if NetworkNode added it to top level\r\n            if not sender_node_id:\r\n                if self.debug_mode: self.logger.warning(\"squid_move message missing sender_node_id.\")\r\n                return\r\n\r\n        if self.network_node and sender_node_id == self.network_node.node_id: return # Ignore own move messages\r\n\r\n        # Defer to entity_manager if available for visual updates\r\n        if self.entity_manager:\r\n            # Ensure payload matches what entity_manager.update_remote_squid expects\r\n            # It needs x, y, direction, and other relevant fields from _get_squid_state\r\n            self.entity_manager.update_remote_squid(sender_node_id, payload, is_new_arrival=False)\r\n        elif sender_node_id in self.remote_squids: # Fallback basic update\r\n            current_display_data = self.remote_squids[sender_node_id]\r\n            visual = current_display_data.get('visual')\r\n            if visual and all(k in payload for k in ['x', 'y', 'direction']):\r\n                visual.setPos(payload['x'], payload['y'])\r\n                self.update_remote_squid_image(current_display_data, payload['direction']) # Update image based on direction\r\n            \r\n            # Update stored data for this squid\r\n            if 'data' in current_display_data:\r\n                current_display_data['data'].update(payload) # Merge new data\r\n                current_display_data['last_update'] = time.time()\r\n\r\n\r\n    def handle_rock_throw(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles 'rock_throw' messages from remote players, creating a visual effect.\"\"\"\r\n        if not self.logger: return\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'user_interface'): return\r\n        \r\n        scene = self.tamagotchi_logic.user_interface.scene\r\n        payload_outer = message.get('payload', {}) # Payload from NetworkNode (contains original sender's payload)\r\n        rock_throw_data = payload_outer.get('rock_data', {}) # Actual rock data\r\n        \r\n        # Get sender_node_id (NetworkNode should add this based on sender's address if not in payload)\r\n        sender_node_id = payload_outer.get('node_id') or message.get('node_id') \r\n        \r\n        if not rock_throw_data or not sender_node_id: \r\n            if self.debug_mode: self.logger.warning(f\"Incomplete rock_throw message: data={rock_throw_data}, sender={sender_node_id}\")\r\n            return\r\n        \r\n        if self.network_node and sender_node_id == self.network_node.node_id: return # Ignore own rock throws\r\n\r\n        if self.debug_mode: self.logger.debug(f\"Received rock_throw from {sender_node_id[-6:]}, data: {rock_throw_data}\")\r\n\r\n        # Create visual for the thrown rock\r\n        rock_filename = rock_throw_data.get('filename', os.path.join(\"images\",\"rock.png\")) # Default rock image\r\n        try:\r\n            pixmap = QtGui.QPixmap(rock_filename)\r\n            if pixmap.isNull(): # Fallback if specified image fails\r\n                pixmap = QtGui.QPixmap(os.path.join(\"images\",\"rock.png\")) \r\n            \r\n            thrown_rock_item = QtWidgets.QGraphicsPixmapItem(pixmap)\r\n            initial_x = rock_throw_data.get('initial_pos_x', scene.width()/2) # Default to center\r\n            initial_y = rock_throw_data.get('initial_pos_y', scene.height()/2)\r\n            thrown_rock_item.setPos(initial_x, initial_y)\r\n            thrown_rock_item.setScale(rock_throw_data.get('scale', 0.8)) # Use scale from payload\r\n            thrown_rock_item.setZValue(20) # High Z-value to be visible\r\n            \r\n            self.apply_foreign_object_tint(thrown_rock_item) # Mark as from remote\r\n            scene.addItem(thrown_rock_item)\r\n            \r\n            # Static display for testing - no animation\r\n            # The rock will just appear and then be removed if it goes off-screen or by other logic.\r\n            # For a simple effect, you could make it disappear after a short time.\r\n            if self.debug_mode: self.logger.debug(f\"Static remote rock '{os.path.basename(rock_filename)}' displayed from {sender_node_id[-6:]}.\")\r\n            QtCore.QTimer.singleShot(1500, lambda item=thrown_rock_item: item.scene().removeItem(item) if item.scene() else None)\r\n\r\n\r\n        except Exception as e_rock_throw_vis:\r\n            if self.debug_mode: self.logger.error(f\"Error visualizing remote rock throw: {e_rock_throw_vis}\", exc_info=True)\r\n\r\n\r\n    def handle_state_update(self, node: NetworkNode, message: Dict, addr: tuple):\r\n        \"\"\"Handles generic 'state_update' messages. Could be used for various game events.\"\"\"\r\n        if not self.logger: return\r\n        \r\n        payload = message.get('payload', {})\r\n        # Sender ID might be in top-level message from NetworkNode or within payload\r\n        sender_node_id = message.get('node_id') or payload.get('node_id') \r\n\r\n        if self.debug_mode: self.logger.debug(f\"Received generic 'state_update' from {sender_node_id[-6:] if sender_node_id else 'Unknown'}. Payload: {payload}\")\r\n        \r\n        # Example: If state_update contains specific info about a remote squid's special action\r\n        # if sender_node_id and 'special_action' in payload:\r\n        #     action_type = payload['special_action']\r\n        #     if self.entity_manager and hasattr(self.entity_manager, 'trigger_remote_squid_special_effect'):\r\n        #         self.entity_manager.trigger_remote_squid_special_effect(sender_node_id, action_type, payload)\r\n        #     elif self.debug_mode:\r\n        #         self.logger.info(f\"Remote squid {sender_node_id[-6:]} performed special action: {action_type}\")\r\n\r\n        # Add more specific logic here based on the content and purpose of 'state_update' messages."
  },
  {
    "path": "plugins/multiplayer/multiplayer_config_dialog.py",
    "content": "# multiplayer_config_dialog.py\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom plugins.multiplayer import mp_constants\r\nimport os\r\n\r\nclass MultiplayerConfigDialog(QtWidgets.QDialog):\r\n    \"\"\"\r\n    Modal dialog that lets the user change multiplayer network settings.\r\n    New: checkbox to switch to TCP/IP list + line-edit for addresses.\r\n    \"\"\"\r\n    def __init__(self, plugin_instance, parent=None, initial_settings=None):\r\n        super().__init__(parent)\r\n        self.plugin = plugin_instance\r\n        self.setWindowTitle(\"Multiplayer Network Settings\")\r\n        self.setModal(True)\r\n        self.resize(500, 400)\r\n        self._build_ui(initial_settings or {})\r\n\r\n    # ------------------------------------------------------------------\r\n    # UI construction\r\n    # ------------------------------------------------------------------\r\n    def _build_ui(self, cfg):\r\n        main_layout = QtWidgets.QVBoxLayout(self)\r\n\r\n        # ----- multicast group / port -----\r\n        grp_multicast = QtWidgets.QGroupBox(\"Multicast (UDP)\")\r\n        form = QtWidgets.QFormLayout(grp_multicast)\r\n        self.mc_group_edit = QtWidgets.QLineEdit(cfg.get(\"multicast_group\", mp_constants.MULTICAST_GROUP))\r\n        self.mc_port_spin = QtWidgets.QSpinBox()\r\n        self.mc_port_spin.setRange(1024, 65535)\r\n        self.mc_port_spin.setValue(cfg.get(\"port\", mp_constants.MULTICAST_PORT))\r\n        form.addRow(\"Group:\", self.mc_group_edit)\r\n        form.addRow(\"Port:\", self.mc_port_spin)\r\n        main_layout.addWidget(grp_multicast)\r\n\r\n        # ----- TCP/IP list -----\r\n        grp_tcp = QtWidgets.QGroupBox(\"TCP/IP Peer List\")\r\n        vbox = QtWidgets.QVBoxLayout(grp_tcp)\r\n        self.use_tcp_check = QtWidgets.QCheckBox(\"Use TCP/IP list instead of multicast\")\r\n        self.use_tcp_check.setToolTip(\"Check this to connect to specific IPs across routers / Internet.\")\r\n        self.tcp_ip_edit = QtWidgets.QLineEdit(cfg.get(\"tcp_ip_list\", \"\"))\r\n        self.tcp_ip_edit.setPlaceholderText(\"e.g. 192.168.1.50,203.0.113.12,example.com\")\r\n        self.tcp_ip_edit.setEnabled(False)\r\n        self.use_tcp_check.toggled.connect(lambda on: self.tcp_ip_edit.setEnabled(on))\r\n        self.use_tcp_check.setChecked(cfg.get(\"use_tcp\", False))\r\n        vbox.addWidget(self.use_tcp_check)\r\n        vbox.addWidget(self.tcp_ip_edit)\r\n        main_layout.addWidget(grp_tcp)\r\n\r\n        # ----- other existing sliders -----\r\n        self.opacity_slider = self._new_slider(cfg.get(\"remote_opacity\", 1.0), 0.2, 1.0, 0.05, \"Remote opacity\")\r\n        self.sync_spin = QtWidgets.QSpinBox()\r\n        self.sync_spin.setRange(1, 10)\r\n        self.sync_spin.setValue(int(cfg.get(\"sync_interval\", 2)))\r\n        form2 = QtWidgets.QFormLayout()\r\n        form2.addRow(\"Remote squid opacity:\", self.opacity_slider)\r\n        form2.addRow(\"Sync interval (s):\", self.sync_spin)\r\n        main_layout.addLayout(form2)\r\n\r\n        # ----- OK / Cancel -----\r\n        buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)\r\n        buttons.accepted.connect(self._on_ok)\r\n        buttons.rejected.connect(self.reject)\r\n        main_layout.addWidget(buttons)\r\n\r\n    # ------------------------------------------------------------------\r\n    # helpers\r\n    # ------------------------------------------------------------------\r\n    def _new_slider(self, val, minv, maxv, step, label):\r\n        slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)\r\n        slider.setRange(int(minv / step), int(maxv / step))\r\n        slider.setValue(int(val / step))\r\n        slider.setTickPosition(QtWidgets.QSlider.TicksBelow)\r\n        slider.setTickInterval(1)\r\n        slider.setSingleStep(1)\r\n        return slider\r\n\r\n    def _on_ok(self):\r\n        self.new_settings = {\r\n            \"multicast_group\": self.mc_group_edit.text().strip(),\r\n            \"port\": self.mc_port_spin.value(),\r\n            \"remote_opacity\": self.opacity_slider.value() * 0.05,\r\n            \"sync_interval\": self.sync_spin.value(),\r\n            \"use_tcp\": self.use_tcp_check.isChecked(),\r\n            \"tcp_ip_list\": self.tcp_ip_edit.text().strip()\r\n        }\r\n        self.accept()\r\n\r\n    # ------------------------------------------------------------------\r\n    # let caller re-load values if dialog is re-opened\r\n    # ------------------------------------------------------------------\r\n    def load_settings(self, cfg):\r\n        self.mc_group_edit.setText(cfg.get(\"multicast_group\", mp_constants.MULTICAST_GROUP))\r\n        self.mc_port_spin.setValue(cfg.get(\"port\", mp_constants.MULTICAST_PORT))\r\n        self.opacity_slider.setValue(int(cfg.get(\"remote_opacity\", 1.0) / 0.05))\r\n        self.sync_spin.setValue(int(cfg.get(\"sync_interval\", 2)))\r\n        self.use_tcp_check.setChecked(cfg.get(\"use_tcp\", False))\r\n        self.tcp_ip_edit.setText(cfg.get(\"tcp_ip_list\", \"\"))"
  },
  {
    "path": "plugins/multiplayer/multiplayer_events.py",
    "content": "from PyQt5 import QtCore\r\nfrom typing import Dict, Any, List, Optional, Callable\r\n\r\nclass MultiplayerEventDispatcher(QtCore.QObject):\r\n    \"\"\"Dispatches multiplayer events to registered handlers\"\"\"\r\n    \r\n    # Define signals for various event types\r\n    squid_joined = QtCore.pyqtSignal(str, dict)  # node_id, squid_data\r\n    squid_left = QtCore.pyqtSignal(str, str)  # node_id, reason\r\n    squid_moved = QtCore.pyqtSignal(str, dict)  # node_id, position_data\r\n    squid_action = QtCore.pyqtSignal(str, str, dict)  # node_id, action_type, action_data\r\n    object_synced = QtCore.pyqtSignal(str, list)  # node_id, objects_data\r\n    rock_thrown = QtCore.pyqtSignal(str, dict)  # node_id, rock_data\r\n    squid_exited = QtCore.pyqtSignal(str, str, dict)  # node_id, direction, exit_data\r\n    squid_arrived = QtCore.pyqtSignal(str, dict)  # node_id, arrival_data\r\n    \r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        self.handlers = {}\r\n        self.debug_mode = False\r\n    \r\n    def register_handler(self, event_type: str, handler: Callable):\r\n        \"\"\"Register a handler for a specific event type\"\"\"\r\n        if event_type not in self.handlers:\r\n            self.handlers[event_type] = []\r\n        self.handlers[event_type].append(handler)\r\n        \r\n        # Connect to corresponding signal if it exists\r\n        signal_map = {\r\n            'squid_joined': self.squid_joined,\r\n            'squid_left': self.squid_left,\r\n            'squid_moved': self.squid_moved,\r\n            'squid_action': self.squid_action,\r\n            'object_synced': self.object_synced,\r\n            'rock_thrown': self.rock_thrown,\r\n            'squid_exited': self.squid_exited,\r\n            'squid_arrived': self.squid_arrived\r\n        }\r\n        \r\n        if event_type in signal_map:\r\n            signal_map[event_type].connect(handler)\r\n    \r\n    def dispatch_event(self, event_type: str, *args, **kwargs):\r\n        \"\"\"Dispatch an event to all registered handlers\"\"\"\r\n        if self.debug_mode:\r\n            print(f\"Dispatching event: {event_type}\")\r\n        \r\n        # Emit the corresponding signal if it exists\r\n        signal_map = {\r\n            'squid_joined': self.squid_joined,\r\n            'squid_left': self.squid_left,\r\n            'squid_moved': self.squid_moved,\r\n            'squid_action': self.squid_action,\r\n            'object_synced': self.object_synced,\r\n            'rock_thrown': self.rock_thrown,\r\n            'squid_exited': self.squid_exited,\r\n            'squid_arrived': self.squid_arrived\r\n        }\r\n        \r\n        if event_type in signal_map:\r\n            signal = signal_map[event_type]\r\n            signal.emit(*args)\r\n        \r\n        # Call direct handlers\r\n        if event_type in self.handlers:\r\n            for handler in self.handlers[event_type]:\r\n                try:\r\n                    handler(*args, **kwargs)\r\n                except Exception as e:\r\n                    print(f\"Error in event handler for {event_type}: {e}\")"
  },
  {
    "path": "plugins/multiplayer/multiplayer_status_widget.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nimport time\r\n\r\nclass MultiplayerStatusWidget(QtWidgets.QWidget):\r\n    def __init__(self, plugin_manager=None, parent=None): # MODIFIED: Added plugin_manager\r\n        super().__init__(parent)\r\n        self.plugin_manager = plugin_manager # MODIFIED: Store plugin_manager\r\n        self.setObjectName(\"MultiplayerStatusWidget\")\r\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\r\n        \r\n        # Keep track of peers and connection status\r\n        self.connection_active = False\r\n        self.node_id = \"Unknown\" # Initial node_id\r\n        self.local_ip = \"N/A\"    # Initial local_ip\r\n        self.peers = []\r\n        self.last_activity = {}\r\n        \r\n        # Setup UI\r\n        self.setup_ui()\r\n        \r\n        # Update timer\r\n        self.update_timer = QtCore.QTimer(self)\r\n        self.update_timer.timeout.connect(self.update_display)\r\n        self.update_timer.start(1000)  # Update every second\r\n    \r\n    def setup_ui(self):\r\n        layout = QtWidgets.QVBoxLayout(self)\r\n        layout.setContentsMargins(5, 5, 5, 5)\r\n        \r\n        frame = QtWidgets.QFrame(self)\r\n        frame.setStyleSheet(\"\"\"\r\n            QFrame {\r\n                background-color: rgba(0, 0, 0, 170);\r\n                border-radius: 12px;\r\n                color: white;\r\n                border: 1px solid rgba(255, 255, 255, 100);\r\n            }\r\n        \"\"\")\r\n        frame_layout = QtWidgets.QVBoxLayout(frame)\r\n        \r\n        title_label = QtWidgets.QLabel(\"🌐 Multiplayer\")\r\n        title_label.setStyleSheet(\"color: #FFFFFF; font-weight: bold; font-size: 14px;\")\r\n        title_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        frame_layout.addWidget(title_label)\r\n        \r\n        status_layout = QtWidgets.QHBoxLayout()\r\n        self.status_icon = QtWidgets.QLabel(\"⚠️\") \r\n        status_layout.addWidget(self.status_icon)\r\n        \r\n        self.status_label = QtWidgets.QLabel(\"Disconnected\")\r\n        self.status_label.setStyleSheet(\"color: #FF6666; font-weight: bold;\")\r\n        status_layout.addWidget(self.status_label)\r\n        \r\n        # Node ID and IP display label\r\n        self.node_id_label = QtWidgets.QLabel(f\"Node ID: {self.node_id} IP: {self.local_ip}\") # MODIFIED: Shows both\r\n        self.node_id_label.setStyleSheet(\"color: #DDDDDD; margin-left: 10px;\")\r\n        status_layout.addWidget(self.node_id_label)\r\n        status_layout.addStretch()\r\n        frame_layout.addLayout(status_layout)\r\n        \r\n        self.activity_log = QtWidgets.QListWidget()\r\n        self.activity_log.setMaximumHeight(120)\r\n        self.activity_log.setStyleSheet(\"\"\"\r\n            QListWidget {\r\n                background-color: rgba(0, 0, 0, 100);\r\n                border: 1px solid #444444;\r\n                border-radius: 5px;\r\n                color: white;\r\n            }\r\n            QListWidget::item {\r\n                padding: 2px;\r\n            }\r\n        \"\"\")\r\n        frame_layout.addWidget(self.activity_log)\r\n\r\n        self.peers_label = QtWidgets.QLabel(\"Connected Peers: 0\")\r\n        self.peers_label.setStyleSheet(\"color: #DDDDDD; margin-top: 5px;\")\r\n        frame_layout.addWidget(self.peers_label)\r\n\r\n        self.peers_list = QtWidgets.QListWidget()\r\n        self.peers_list.setMaximumHeight(80)\r\n        self.peers_list.setStyleSheet(\"\"\"\r\n            QListWidget {\r\n                background-color: rgba(0, 0, 0, 80);\r\n                border: 1px solid #333333;\r\n                border-radius: 5px;\r\n                color: white;\r\n            }\r\n            QListWidget::item {\r\n                padding: 2px;\r\n            }\r\n        \"\"\")\r\n        frame_layout.addWidget(self.peers_list)\r\n        \r\n        toggle_button = QtWidgets.QPushButton(\"▼\")\r\n        toggle_button.setMaximumWidth(30)\r\n        toggle_button.clicked.connect(self.toggle_expanded)\r\n        frame_layout.addWidget(toggle_button, alignment=QtCore.Qt.AlignRight)\r\n        \r\n        layout.addWidget(frame)\r\n        \r\n        self.is_expanded = True\r\n        self.activity_log.setVisible(self.is_expanded)\r\n        self.peers_label.setVisible(self.is_expanded)\r\n        self.peers_list.setVisible(self.is_expanded)\r\n\r\n    def add_activity(self, message):\r\n        timestamp = QtCore.QTime.currentTime().toString(\"hh:mm:ss\")\r\n        item = QtWidgets.QListWidgetItem(f\"{timestamp}: {message}\")\r\n        self.activity_log.insertItem(0, item)\r\n        if self.activity_log.count() > 50:\r\n            self.activity_log.takeItem(self.activity_log.count() - 1)\r\n\r\n    def toggle_expanded(self):\r\n        self.is_expanded = not self.is_expanded\r\n        self.activity_log.setVisible(self.is_expanded)\r\n        self.peers_label.setVisible(self.is_expanded)\r\n        self.peers_list.setVisible(self.is_expanded)\r\n        \r\n        sender = self.sender()\r\n        if isinstance(sender, QtWidgets.QPushButton):\r\n            sender.setText(\"▼\" if self.is_expanded else \"▲\")\r\n        \r\n        if self.is_expanded:\r\n            self.parentWidget().adjustSize()\r\n            self.setMaximumHeight(10000)\r\n        else:\r\n            self.parentWidget().adjustSize()\r\n            min_height = self.layout().itemAt(0).widget().layout().itemAt(0).widget().sizeHint().height()\r\n            min_height += self.layout().itemAt(0).widget().layout().itemAt(1).layout().sizeHint().height()\r\n            min_height += self.layout().itemAt(0).widget().layout().itemAt(4).widget().sizeHint().height()\r\n            min_height += self.layout().itemAt(0).widget().layout().contentsMargins().top() + self.layout().itemAt(0).widget().layout().contentsMargins().bottom()\r\n            min_height += self.layout().contentsMargins().top() + self.layout().contentsMargins().bottom()\r\n            self.setMaximumHeight(min_height + 20)\r\n\r\n    def update_connection_status(self, is_connected, node_id=None):\r\n        self.connection_active = is_connected\r\n        \r\n        if node_id: # If a node_id is provided, update it\r\n            self.node_id = node_id\r\n        \r\n        if is_connected:\r\n            self.status_label.setText(\"Connected\")\r\n            self.status_label.setStyleSheet(\"color: #66FF66; font-weight: bold;\")\r\n            self.status_icon.setText(\"✔️\")\r\n        else:\r\n            self.status_label.setText(\"Disconnected\")\r\n            self.status_label.setStyleSheet(\"color: #FF6666; font-weight: bold;\")\r\n            self.status_icon.setText(\"⚠️\")\r\n        \r\n        # Update the node_id_label with current node_id and local_ip\r\n        self.node_id_label.setText(f\"Node ID: {self.node_id} IP: {self.local_ip}\")\r\n\r\n\r\n    def update_peers(self, peers_data):\r\n        self.peers = []\r\n        if not hasattr(self, 'peers_list'): return\r\n        self.peers_list.clear()\r\n        current_time = time.time()\r\n        \r\n        for node_id, (ip, last_seen, _) in peers_data.items():\r\n            status = \"Active\" if current_time - last_seen < 10 else \"Inactive\"\r\n            self.peers.append({\r\n                'node_id': node_id,\r\n                'ip': ip,\r\n                'last_seen': last_seen,\r\n                'status': status\r\n            })\r\n            item_text = f\"{node_id[-6:]} ({ip})\"\r\n            item = QtWidgets.QListWidgetItem(item_text)\r\n            if status == \"Active\":\r\n                item.setForeground(QtGui.QBrush(QtGui.QColor(100, 255, 100)))\r\n            else:\r\n                item.setForeground(QtGui.QBrush(QtGui.QColor(150, 150, 150)))\r\n            self.peers_list.addItem(item)\r\n        \r\n        active_count = sum(1 for p in self.peers if p['status'] == \"Active\")\r\n        if hasattr(self, 'peers_label'):\r\n            self.peers_label.setText(f\"Connected Peers: {active_count}\")\r\n    \r\n    def update_display(self):\r\n        if self.peers:\r\n            current_time = time.time()\r\n            update_needed = False\r\n            for peer in self.peers:\r\n                old_status = peer['status']\r\n                if isinstance(peer.get('last_seen'), (int, float)):\r\n                    peer['status'] = \"Active\" if current_time - peer['last_seen'] < 10 else \"Inactive\"\r\n                else:\r\n                    peer['status'] = \"Unknown\"\r\n                if old_status != peer['status']:\r\n                    update_needed = True\r\n            if update_needed:\r\n                self.refresh_peers_list()\r\n    \r\n    def refresh_peers_list(self):\r\n        if not hasattr(self, 'peers_list') or not hasattr(self, 'peers_label'): return\r\n        self.peers_list.clear()\r\n        for peer in self.peers:\r\n            item_text = f\"{peer.get('node_id', 'N/A')[-6:]} ({peer.get('ip', 'N/A')})\"\r\n            item = QtWidgets.QListWidgetItem(item_text)\r\n            if peer.get('status') == \"Active\":\r\n                item.setForeground(QtGui.QBrush(QtGui.QColor(100, 255, 100)))\r\n            else:\r\n                item.setForeground(QtGui.QBrush(QtGui.QColor(150, 150, 150)))\r\n            self.peers_list.addItem(item)\r\n        active_count = sum(1 for p in self.peers if p.get('status') == \"Active\")\r\n        self.peers_label.setText(f\"Connected Peers: {active_count}\")\r\n\r\n    # --- ADDED/MODIFIED METHODS TO MATCH mp_plugin_logic.py EXPECTATIONS ---\r\n\r\n    def update_status(self, status_text, is_enabled):\r\n        \"\"\"\r\n        Called by mp_plugin_logic.py to update the general enabled/disabled status.\r\n        It maps to the widget's more specific 'update_connection_status'.\r\n        Note: This version does not receive node_id directly. Node ID is managed\r\n        by calls to update_connection_status (potentially by mp_plugin_logic.py\r\n        if it calls that) or set initially.\r\n        \"\"\"\r\n        # Update connection status (visuals like \"Connected\"/\"Disconnected\" and icon)\r\n        # It uses the currently stored self.node_id.\r\n        self.update_connection_status(is_connected=is_enabled, node_id=self.node_id) \r\n\r\n        # Logging through plugin_manager if available\r\n        if self.plugin_manager and hasattr(self.plugin_manager, 'logger'):\r\n            self.plugin_manager.logger.debug(\r\n                f\"MultiplayerStatusWidget: 'update_status' called. Status Text='{status_text}', IsEnabled={is_enabled}\"\r\n            )\r\n        else: # Fallback print for debugging if logger is not available\r\n            print(f\"DEBUG: MultiplayerStatusWidget: 'update_status' called. Status Text='{status_text}', IsEnabled={is_enabled}\")\r\n\r\n    def set_ip_address(self, ip_address_text):\r\n        \"\"\"\r\n        Called by mp_plugin_logic.py to set the local IP address display.\r\n        \"\"\"\r\n        self.local_ip = ip_address_text if ip_address_text else \"N/A\"\r\n        \r\n        # Update the display to show the new IP along with the current node_id\r\n        self.node_id_label.setText(f\"Node ID: {self.node_id} IP: {self.local_ip}\")\r\n\r\n        if self.plugin_manager and hasattr(self.plugin_manager, 'logger'):\r\n            self.plugin_manager.logger.debug(\r\n                f\"MultiplayerStatusWidget: 'set_ip_address' called. IP='{self.local_ip}'\"\r\n            )\r\n        else:\r\n            print(f\"DEBUG: MultiplayerStatusWidget: 'set_ip_address' called. IP='{self.local_ip}'\")\r\n\r\n    def update_icon(self, is_enabled):\r\n        \"\"\"\r\n        Placeholder to prevent AttributeError if called.\r\n        Actual icon update is handled by update_connection_status.\r\n        \"\"\"\r\n        # The actual icon (self.status_icon) is updated in self.update_connection_status.\r\n        # This method is here to satisfy any external calls that might have been based on earlier designs.\r\n        pass\r\n        # If direct control over the icon from this method signature is needed later,\r\n        # you can add logic here, e.g.:\r\n        # if is_enabled:\r\n        #     self.status_icon.setText(\"✔️\")\r\n        # else:\r\n        #     self.status_icon.setText(\"⚠️\")"
  },
  {
    "path": "plugins/multiplayer/network_utilities.py",
    "content": "# In plugins/multiplayer/network_utilities.py\r\n\r\nimport json\r\nimport zlib\r\nimport socket # Make sure socket is imported if get_local_ip uses it\r\nimport time   # Make sure time is imported if is_node_active uses it\r\nimport uuid   # Make sure uuid is imported if generate_node_id uses it\r\nfrom typing import Dict, Any, Union, List, Tuple # Ensure all used types are imported\r\n\r\n# It's good practice to have a logger for utilities if they are complex\r\n# import logging\r\n# logger = logging.getLogger(__name__)\r\n\r\n\r\nclass NetworkUtilities:\r\n    \"\"\"\r\n    A collection of static utility methods for network operations\r\n    including message compression, decompression, and node ID generation.\r\n    \"\"\"\r\n\r\n    @staticmethod\r\n    def get_local_ip() -> str:\r\n        \"\"\"\r\n        Attempts to discover the local IP address of the machine.\r\n        Fallback to '127.0.0.1' if discovery fails.\r\n        \"\"\"\r\n        try:\r\n            # Create a temporary socket to connect to an external server (doesn't send data)\r\n            temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\r\n            temp_socket.settimeout(0.5) # Prevent long blocking\r\n            # Google's public DNS server is a common choice for this\r\n            temp_socket.connect(('8.8.8.8', 80))\r\n            local_ip = temp_socket.getsockname()[0]\r\n            temp_socket.close()\r\n            return local_ip\r\n        except socket.error: # Catch socket-specific errors\r\n            # Fallback if the above method fails (e.g., no network, firewall)\r\n            try:\r\n                # Get hostname and resolve it\r\n                hostname = socket.gethostname()\r\n                local_ip = socket.gethostbyname(hostname)\r\n                return local_ip\r\n            except socket.gaierror: # getaddrinfo error\r\n                return '127.0.0.1' # Ultimate fallback\r\n        except Exception:\r\n            # Catch any other unexpected errors\r\n            return '127.0.0.1'\r\n\r\n\r\n    @staticmethod\r\n    def compress_message(message: Dict[str, Any]) -> bytes:\r\n        \"\"\"Compress a message dictionary to bytes using JSON and zlib.\"\"\"\r\n        is_squid_exit_message = message.get('type') == 'squid_exit'\r\n        \r\n        # Optional: More detailed logging for all messages for debugging structure\r\n        # print(\"--- Debug: Attempting to compress message (NetworkUtilities) ---\")\r\n        # for key, value in message.items():\r\n        #     print(f\"  Key: {key}, Type: {type(value)}\")\r\n        #     if isinstance(value, dict):\r\n        #         for sub_key, sub_value in value.items():\r\n        #             print(f\"    SubKey: {sub_key}, SubType: {type(sub_value)}\")\r\n        # print(\"--- End of debug statements for message content (NetworkUtilities) ---\")\r\n\r\n        if is_squid_exit_message:\r\n            # Using json.dumps for pretty printing complex nested structures for the log\r\n            try:\r\n                message_for_log = json.dumps(message, indent=2)\r\n            except TypeError: # Handle non-serializable items if any for logging\r\n                message_for_log = str(message) # Fallback to string representation\r\n            print(f\"DEBUG_COMPRESS: Compressing SQUID_EXIT. Full message data: {message_for_log}\")\r\n\r\n        serialized_msg = None # Initialize to handle potential early error\r\n        try:\r\n            serialized_msg = json.dumps(message).encode('utf-8')\r\n            if is_squid_exit_message:\r\n                print(f\"DEBUG_COMPRESS: SQUID_EXIT serialized size: {len(serialized_msg)}\")\r\n            \r\n            compressed_msg = zlib.compress(serialized_msg)\r\n            if is_squid_exit_message:\r\n                compression_ratio = len(compressed_msg) / len(serialized_msg) if len(serialized_msg) > 0 else 0\r\n                print(f\"DEBUG_COMPRESS: SQUID_EXIT compressed size: {len(compressed_msg)}. Compression ratio: {compression_ratio:.2f}\")\r\n            return compressed_msg\r\n\r\n        except TypeError as te:\r\n            error_detail = f\"TypeError during JSON serialization for SQUID_EXIT: {te}. Message keys: {list(message.keys())}\" if is_squid_exit_message else f\"TypeError during JSON serialization: {te}. Message keys: {list(message.keys())}\"\r\n            print(f\"DEBUG_COMPRESS_ERROR: {error_detail}\")\r\n            # Fallback: return an error message, still as bytes\r\n            return json.dumps({\"error\": \"json_type_error\", \"details\": str(te), \"original_type\": message.get('type')}).encode('utf-8')\r\n        except zlib.error as ze:\r\n            error_detail = f\"zlib compression error for SQUID_EXIT: {ze}. Sending uncompressed.\" if is_squid_exit_message else f\"zlib compression error: {ze}. Sending uncompressed.\"\r\n            print(f\"DEBUG_COMPRESS_ERROR: {error_detail}\")\r\n            if serialized_msg: # If serialization succeeded before zlib error\r\n                return serialized_msg \r\n            else: # Should not happen if TypeError is caught, but as a safeguard\r\n                return json.dumps({\"error\": \"zlib_error_and_serialization_failed\", \"details\": str(ze), \"original_type\": message.get('type')}).encode('utf-8')\r\n        except Exception as e:\r\n            error_detail = f\"General error compressing SQUID_EXIT: {e}\" if is_squid_exit_message else f\"General error compressing message: {e}\"\r\n            print(f\"DEBUG_COMPRESS_ERROR: {error_detail}\")\r\n            return json.dumps({\"error\": \"compression_failure\", \"details\": str(e), \"original_type\": message.get('type')}).encode('utf-8')\r\n\r\n    @staticmethod\r\n    def decompress_message(compressed_msg: bytes) -> Union[Dict[str, Any], None]:\r\n        \"\"\"Decompress bytes to a message dictionary using zlib and JSON.\"\"\"\r\n        if not compressed_msg:\r\n            print(\"DEBUG_DECOMPRESS_ERROR: Received empty message for decompression.\")\r\n            return None\r\n\r\n        # Crude check on raw/compressed bytes to see if it *might* be a squid_exit message for targeted logging\r\n        # This check is heuristic and might not always be accurate before decompression.\r\n        is_potentially_squid_exit = b'\"type\": \"squid_exit\"' in compressed_msg or \\\r\n                                    b'squid_exit' in compressed_msg # More generic check\r\n        \r\n        if is_potentially_squid_exit:\r\n            print(f\"DEBUG_DECOMPRESS: Potential SQUID_EXIT raw data received (first 100 bytes): {compressed_msg[:100]}\")\r\n\r\n        decompressed_data_str = None\r\n        message_dict = None\r\n\r\n        try:\r\n            # Attempt zlib decompression first\r\n            try:\r\n                decompressed_bytes = zlib.decompress(compressed_msg)\r\n                if is_potentially_squid_exit:\r\n                     print(f\"DEBUG_DECOMPRESS: SQUID_EXIT (potential) successfully zlib decompressed. Decompressed size: {len(decompressed_bytes)}\")\r\n                decompressed_data_str = decompressed_bytes.decode('utf-8')\r\n                message_dict = json.loads(decompressed_data_str)\r\n            except zlib.error as ze_decompress:\r\n                # If zlib fails, assume it's uncompressed JSON\r\n                if is_potentially_squid_exit:\r\n                    print(f\"DEBUG_DECOMPRESS: zlib.error for SQUID_EXIT (potential) ('{ze_decompress}'). Assuming uncompressed JSON.\")\r\n                decompressed_data_str = compressed_msg.decode('utf-8') # Use original msg as string\r\n                message_dict = json.loads(decompressed_data_str)\r\n            except UnicodeDecodeError as ude: # Catch if decode after zlib fails\r\n                print(f\"DEBUG_DECOMPRESS_ERROR: UnicodeDecodeError after zlib success (or if it was uncompressed non-UTF8). Details: {ude}. Data (first 100 bytes of error source): {decompressed_bytes[:100] if 'decompressed_bytes' in locals() else compressed_msg[:100]}\")\r\n                return {\"error\": \"unicode_decode_error_post_zlib\", \"details\": str(ude)}\r\n\r\n            # After successful JSON load, confirm and log if it's a squid_exit\r\n            if isinstance(message_dict, dict) and message_dict.get('type') == 'squid_exit':\r\n                # Using json.dumps for pretty printing complex nested structures for the log\r\n                try:\r\n                    message_for_log = json.dumps(message_dict, indent=2)\r\n                except TypeError:\r\n                    message_for_log = str(message_dict) # Fallback\r\n                print(f\"DEBUG_DECOMPRESS: Successfully decoded SQUID_EXIT message: {message_for_log}\")\r\n            \r\n            return message_dict\r\n\r\n        except UnicodeDecodeError as ude_uncompressed: # If decode of presumed uncompressed fails\r\n            print(f\"DEBUG_DECOMPRESS_ERROR: UnicodeDecodeError (assuming uncompressed). Details: {ude_uncompressed}. Raw data (first 100 bytes): {compressed_msg[:100]}\")\r\n            return {\"error\": \"unicode_decode_error_uncompressed\", \"details\": str(ude_uncompressed)}\r\n        except json.JSONDecodeError as jde:\r\n            # The data fed to json.loads here is `decompressed_data_str`\r\n            print(f\"DEBUG_DECOMPRESS_ERROR: JSONDecodeError. Details: {jde}. Data fed to json.loads (first 100 chars): {decompressed_data_str[:100] if decompressed_data_str else 'N/A'}\")\r\n            return {\"error\": \"json_decode_error\", \"details\": str(jde)}\r\n        except Exception as e: # Catch-all for other unexpected errors\r\n            print(f\"DEBUG_DECOMPRESS_ERROR: General Exception during decompression. Details: {e}. Raw data (first 100 bytes): {compressed_msg[:100]}\")\r\n            return {\"error\": \"general_decompression_failure\", \"details\": str(e)}\r\n\r\n    @staticmethod\r\n    def generate_node_id(prefix: str = \"squid\") -> str:\r\n        \"\"\"Generate a unique node identifier with a given prefix.\"\"\"\r\n        # Generate a UUID and take a portion of its hex representation for brevity\r\n        return f\"{prefix}_{uuid.uuid4().hex[:8]}\"\r\n\r\n    @staticmethod\r\n    def is_node_active(last_seen_time: float, threshold: float = 30.0) -> bool: # Increased threshold\r\n        \"\"\"\r\n        Check if a node is considered active based on its last seen time.\r\n        Args:\r\n            last_seen_time: The timestamp (unix epoch float) when the node was last heard from.\r\n            threshold: The number of seconds without contact after which a node is considered inactive.\r\n        Returns:\r\n            True if the node is active, False otherwise.\r\n        \"\"\"\r\n        if last_seen_time is None:\r\n            return False # Never seen\r\n        return (time.time() - last_seen_time) < threshold\r\n\r\n# Example of a BinaryProtocol class if it were part of this file:\r\n# class BinaryProtocol:\r\n#     @staticmethod\r\n#     def pack_data(*args) -> bytes:\r\n#         # Example: Implement packing logic using struct or similar\r\n#         # This is a placeholder and would need a proper specification\r\n#         packed_bytes = b''\r\n#         for arg in args:\r\n#             if isinstance(arg, int):\r\n#                 packed_bytes += arg.to_bytes(4, 'big', signed=True)\r\n#             elif isinstance(arg, float):\r\n#                 # A more robust solution would use struct.pack\r\n#                 packed_bytes += str(arg).encode('utf-8').ljust(16, b'\\0') # Simplistic\r\n#             elif isinstance(arg, str):\r\n#                 packed_bytes += arg.encode('utf-8').ljust(32, b'\\0') # Simplistic\r\n#         return packed_bytes\r\n\r\n#     @staticmethod\r\n#     def unpack_data(data: bytes) -> tuple:\r\n#         # Example: Implement unpacking logic\r\n#         # This is a placeholder\r\n#         # Assuming a fixed format like: int (4B), float_str (16B), str (32B)\r\n#         num = int.from_bytes(data[0:4], 'big', signed=True)\r\n#         float_val_str = data[4:20].decode('utf-8').strip('\\0')\r\n#         str_val = data[20:52].decode('utf-8').strip('\\0')\r\n#         return num, float(float_val_str), str_val"
  },
  {
    "path": "plugins/multiplayer/packet_validator.py",
    "content": "import re\r\nimport json\r\nimport os\r\nimport time\r\nfrom typing import Dict, Any, Optional, List, Tuple\r\n\r\nclass PacketValidator:\r\n    \"\"\"Utility class to validate network packets for security and integrity\"\"\"\r\n    \r\n    @staticmethod\r\n    def validate_message(message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:\r\n        \"\"\"\r\n        Validate a message for required fields and proper structure\r\n        \r\n        Args:\r\n            message: The message to validate\r\n            \r\n        Returns:\r\n            (is_valid, error_message)\r\n        \"\"\"\r\n        # Check for required fields\r\n        required_fields = ['node_id', 'timestamp', 'type', 'payload']\r\n        for field in required_fields:\r\n            if field not in message:\r\n                return False, f\"Missing required field: {field}\"\r\n        \r\n        # Validate node_id format (alphanumeric)\r\n        if not isinstance(message['node_id'], str) or not re.match(r'^[a-zA-Z0-9_-]+$', message['node_id']):\r\n            return False, \"Invalid node_id format\"\r\n        \r\n        # Check timestamp (should be within 1 hour of current time to prevent replay attacks)\r\n        current_time = time.time()\r\n        msg_time = message['timestamp']\r\n        if not isinstance(msg_time, (int, float)) or abs(current_time - msg_time) > 3600:\r\n            return False, \"Invalid timestamp\"\r\n        \r\n        # Validate message type\r\n        valid_types = [\r\n            'heartbeat', 'squid_move', 'squid_action', 'object_sync', \r\n            'rock_throw', 'player_join', 'player_leave', 'state_update',\r\n            'squid_exit', 'new_squid_arrival'\r\n        ]\r\n        if message['type'] not in valid_types:\r\n            return False, f\"Unknown message type: {message['type']}\"\r\n        \r\n        # Validate payload is a dictionary\r\n        if not isinstance(message['payload'], dict):\r\n            return False, \"Payload must be a dictionary\"\r\n        \r\n        # Type-specific validation\r\n        if message['type'] == 'squid_exit':\r\n            return PacketValidator.validate_squid_exit(message['payload'])\r\n        elif message['type'] == 'object_sync':\r\n            return PacketValidator.validate_object_sync(message['payload'])\r\n        \r\n        # Default to valid for types without specific validation\r\n        return True, None\r\n    \r\n    @staticmethod\r\n    def validate_squid_exit(payload: Dict[str, Any]) -> Tuple[bool, Optional[str]]:\r\n        \"\"\"Validate squid exit payload\"\"\"\r\n        # Check for nested payload structure\r\n        if 'payload' not in payload:\r\n            return False, \"Missing nested payload in squid_exit message\"\r\n        \r\n        exit_data = payload['payload']\r\n        \r\n        # Check required fields\r\n        required_fields = ['node_id', 'direction', 'position', 'color']\r\n        for field in required_fields:\r\n            if field not in exit_data:\r\n                return False, f\"Missing required field in squid_exit: {field}\"\r\n        \r\n        # Validate direction\r\n        valid_directions = ['left', 'right', 'up', 'down']\r\n        if exit_data['direction'] not in valid_directions:\r\n            return False, f\"Invalid exit direction: {exit_data['direction']}\"\r\n        \r\n        # Validate position is a dictionary with x,y\r\n        if not isinstance(exit_data['position'], dict) or not all(k in exit_data['position'] for k in ['x', 'y']):\r\n            return False, \"Invalid position format\"\r\n        \r\n        # Validate color is a tuple or list\r\n        color = exit_data['color']\r\n        if not isinstance(color, (list, tuple)) or len(color) < 3 or not all(isinstance(c, int) for c in color[:3]):\r\n            return False, \"Invalid color format\"\r\n        \r\n        return True, None\r\n    \r\n    @staticmethod\r\n    def validate_object_sync(payload: Dict[str, Any]) -> Tuple[bool, Optional[str]]:\r\n        \"\"\"Validate object sync payload\"\"\"\r\n        # Check for squid data\r\n        if 'squid' not in payload:\r\n            return False, \"Missing squid data in object_sync\"\r\n        \r\n        # Check for objects array\r\n        if 'objects' not in payload:\r\n            return False, \"Missing objects array in object_sync\"\r\n        \r\n        if not isinstance(payload['objects'], list):\r\n            return False, \"Objects must be an array\"\r\n        \r\n        # Validate squid data has required fields\r\n        squid = payload['squid']\r\n        required_squid_fields = ['x', 'y', 'direction']\r\n        for field in required_squid_fields:\r\n            if field not in squid:\r\n                return False, f\"Missing required squid field: {field}\"\r\n        \r\n        # Validate node_info if present\r\n        if 'node_info' in payload:\r\n            node_info = payload['node_info']\r\n            if not isinstance(node_info, dict) or 'id' not in node_info:\r\n                return False, \"Invalid node_info format\"\r\n        \r\n        return True, None\r\n    \r\n    @staticmethod\r\n    def sanitize_object_data(objects: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\r\n        \"\"\"Sanitize object data to ensure no malicious content\"\"\"\r\n        sanitized = []\r\n        \r\n        for obj in objects:\r\n            # Check if required fields exist\r\n            if not all(k in obj for k in ['id', 'type', 'x', 'y']):\r\n                continue\r\n                \r\n            # Sanitize filename to prevent directory traversal\r\n            if 'filename' in obj:\r\n                filename = obj['filename']\r\n                # Remove any path navigation\r\n                filename = re.sub(r'\\.\\./', '', filename)\r\n                filename = re.sub(r'\\.\\.\\\\', '', filename)\r\n                # Use only the basename\r\n                import os\r\n                filename = os.path.basename(filename)\r\n                obj['filename'] = filename\r\n            \r\n            # Ensure numeric values are valid\r\n            obj['x'] = float(obj['x']) if isinstance(obj['x'], (int, float)) else 0\r\n            obj['y'] = float(obj['y']) if isinstance(obj['y'], (int, float)) else 0\r\n            if 'scale' in obj:\r\n                obj['scale'] = float(obj['scale']) if isinstance(obj['scale'], (int, float)) else 1.0\r\n                \r\n            # Limit to valid values\r\n            obj['scale'] = max(0.1, min(5.0, obj['scale']))  # Reasonable scale limits\r\n            \r\n            sanitized.append(obj)\r\n            \r\n        return sanitized"
  },
  {
    "path": "plugins/multiplayer/plugin.txt",
    "content": "NAME=Multiplayer\r\nVERSION=1.10\r\nAUTHOR=ViciousSquid\r\nDESCRIPTION=Enables network sync for squids and objects (Experimental)\r\n\r\nREQUIRES=network_interface\r\n"
  },
  {
    "path": "plugins/multiplayer/remote_entity_manager.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nimport os\r\nimport time\r\nimport math\r\nfrom typing import Dict, Any, Optional, List\r\nimport logging\r\nimport base64\r\n\r\n# AnimatableGraphicsItem class\r\nclass AnimatableGraphicsItem(QtWidgets.QGraphicsPixmapItem, QtCore.QObject):\r\n    def __init__(self, pixmap=None, parent=None):\r\n        QtWidgets.QGraphicsPixmapItem.__init__(self, pixmap, parent)\r\n        QtCore.QObject.__init__(self)\r\n        self._scale = 1.0\r\n    @QtCore.pyqtProperty(float)\r\n    def scale_factor(self): return self._scale\r\n    @scale_factor.setter\r\n    def scale_factor(self, value):\r\n        self._scale = value\r\n        self.setScale(value)\r\n\r\n# ObjectPool class\r\nclass ObjectPool:\r\n    def __init__(self, factory_func, initial_size=10):\r\n        self.factory = factory_func\r\n        self.available = []\r\n        self.in_use = set()\r\n        for _ in range(initial_size): self.available.append(self.factory())\r\n    def acquire(self):\r\n        obj = self.available.pop() if self.available else self.factory()\r\n        self.in_use.add(obj)\r\n        return obj\r\n    def release(self, obj):\r\n        if obj in self.in_use:\r\n            self.in_use.remove(obj)\r\n            self.available.append(obj)\r\n    def clear(self):\r\n        for item_list in [self.available, self.in_use]:\r\n            for item in list(item_list): # Iterate copy if modifying list\r\n                if isinstance(item, QtWidgets.QGraphicsItem) and item.scene(): \r\n                    item.scene().removeItem(item)\r\n                if item_list is self.in_use and item in self.in_use: # If clearing in_use, ensure removed\r\n                     self.in_use.remove(item)\r\n        self.available.clear()\r\n        # self.in_use should be cleared by loop above if items are released,\r\n        # but direct clear if items are not released back to pool by external logic.\r\n        self.in_use.clear()\r\n\r\n\r\nclass RemoteEntityManager:\r\n    def __init__(self, scene, window_width, window_height, debug_mode=False, logger=None):\r\n        self.scene = scene\r\n        self.window_width = window_width\r\n        self.window_height = window_height\r\n        self.debug_mode = debug_mode\r\n\r\n        self.IMAGE_DIMENSIONS = {\r\n            \"left1.png\": (253, 147), \"left2.png\": (253, 147),\r\n            \"right1.png\": (253, 147), \"right2.png\": (253, 147),\r\n            \"up1.png\": (177, 238), \"up2.png\": (177, 238),\r\n        }\r\n        self.DEFAULT_IMAGE_DIMENSION = (253, 147) \r\n\r\n        if logger: self.logger = logger\r\n        else:\r\n            self.logger = logging.getLogger(__name__ + \".RemoteEntityManager\")\r\n            if not self.logger.hasHandlers():\r\n                handler = logging.StreamHandler()\r\n                formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\r\n                handler.setFormatter(formatter)\r\n                self.logger.addHandler(handler)\r\n                self.logger.setLevel(logging.DEBUG if self.debug_mode else logging.INFO)\r\n\r\n        self.remote_squids = {}\r\n        self.remote_objects = {}\r\n        self.connection_lines = {}\r\n        self._last_calculated_entry_details = {}\r\n        self.remote_opacity = 1.0\r\n        self.show_labels = True\r\n        self.show_connections = True\r\n        self.text_pool = ObjectPool(lambda: QtWidgets.QGraphicsTextItem(\"\"), initial_size=20)\r\n        \r\n        self.script_dir = os.path.dirname(os.path.abspath(__file__))\r\n        self.project_root = os.path.join(self.script_dir, '..', '..')\r\n        self.images_folder_root_path = os.path.join(self.project_root, 'images')\r\n\r\n        self.position_update_timer = QtCore.QTimer()\r\n        self.position_update_timer.timeout.connect(self._update_visuals_once_per_second)\r\n        self.MOVEMENT_INTERVAL_MS = 1000  # Update once per second\r\n        self.MAX_PIXELS_PER_JUMP = 90.0\r\n        self.position_update_timer.start(self.MOVEMENT_INTERVAL_MS)\r\n\r\n    def _get_image_file_name_and_direction(self, payload_direction_key: Optional[str], payload_animation_frame: Any, \r\n                                          entry_direction_on_this_screen: Optional[str] = None, \r\n                                          current_image_name_for_fallback_dir: str = \"right1.png\") -> tuple[str, str]:\r\n        \r\n        base_direction = \"right\" \r\n        if payload_direction_key:\r\n            base_direction = payload_direction_key.lower().strip()\r\n        else:\r\n            if self.debug_mode:\r\n                self.logger.warning(f\"_get_image_file_name_and_direction: payload_direction_key is missing. Defaulting to '{base_direction}'. Fallback hint: {current_image_name_for_fallback_dir}\")\r\n        \r\n        facing_direction = base_direction\r\n\r\n        if entry_direction_on_this_screen:\r\n            original_facing_for_log = facing_direction\r\n            if entry_direction_on_this_screen == \"left\":\r\n                facing_direction = \"right\"\r\n            elif entry_direction_on_this_screen == \"right\":\r\n                facing_direction = \"left\"\r\n            elif entry_direction_on_this_screen == \"bottom\": # Enters from bottom, should face up\r\n                facing_direction = \"up\"\r\n            elif entry_direction_on_this_screen == \"top\":    # Enters from top, should face down\r\n                facing_direction = \"down\" # Directly use \"down\" as it's valid since 'down1.png' exists\r\n            \r\n            if self.debug_mode and original_facing_for_log != facing_direction:\r\n                self.logger.debug(f\"_get_image_file_name_and_direction: Arrival adjustment. Entry: {entry_direction_on_this_screen}. Original Payload Facing: {original_facing_for_log}. New Visual Facing: {facing_direction}\")\r\n\r\n        # Valid sprite directions, now including \"down\" as per your confirmation\r\n        valid_sprite_directions = [\"left\", \"right\", \"up\", \"down\"] \r\n        \r\n        if facing_direction not in valid_sprite_directions:\r\n            if self.debug_mode:\r\n                self.logger.warning(f\"_get_image_file_name_and_direction: Attempted facing_direction '{facing_direction}' is not in {valid_sprite_directions}. Defaulting to 'right'.\")\r\n            facing_direction = \"right\"\r\n\r\n        try:\r\n            frame = int(payload_animation_frame)\r\n            if frame not in [1, 2]: # Assuming animation frames are 1 and 2\r\n                if self.debug_mode and payload_animation_frame not in [1,2]: \r\n                    self.logger.warning(f\"_get_image_file_name_and_direction: Invalid animation frame '{payload_animation_frame}'. Defaulting to 1.\")\r\n                frame = 1\r\n        except (ValueError, TypeError):\r\n            if self.debug_mode:\r\n                 self.logger.warning(f\"_get_image_file_name_and_direction: Animation frame '{payload_animation_frame}' is not a valid integer. Defaulting to 1.\")\r\n            frame = 1\r\n        \r\n        image_file_name = f\"{facing_direction}{frame}.png\"\r\n        \r\n        if self.debug_mode:\r\n            self.logger.debug(f\"_get_image_file_name_and_direction: Args(PayloadDirKey='{payload_direction_key}', EntryDir='{entry_direction_on_this_screen}', AnimFrame='{payload_animation_frame}') -> Result(BaseDir='{base_direction}', FinalFacing='{facing_direction}', Image='{image_file_name}')\")\r\n            \r\n        return image_file_name, facing_direction\r\n\r\n    def _get_scaled_pixmap(self, image_file_name: str) -> tuple[QtGui.QPixmap, tuple[int, int]]:\r\n        target_width, target_height = self.IMAGE_DIMENSIONS.get(image_file_name, self.DEFAULT_IMAGE_DIMENSION)\r\n        if (target_width, target_height) == self.DEFAULT_IMAGE_DIMENSION and image_file_name not in self.IMAGE_DIMENSIONS:\r\n            if self.debug_mode: self.logger.warning(f\"Image file '{image_file_name}' not in IMAGE_DIMENSIONS. Using default: {self.DEFAULT_IMAGE_DIMENSION}\")\r\n        \r\n        image_path = os.path.join(self.images_folder_root_path, image_file_name)\r\n        pixmap = QtGui.QPixmap(image_path)\r\n        if pixmap.isNull():\r\n            if self.debug_mode: self.logger.error(f\"Image {image_path} not found. Fallback gray pixmap {target_width}x{target_height}.\")\r\n            fb_pixmap = QtGui.QPixmap(target_width, target_height); fb_pixmap.fill(QtCore.Qt.gray)\r\n            return fb_pixmap, (target_width, target_height) \r\n        return pixmap.scaled(target_width, target_height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation), (target_width, target_height)\r\n\r\n    def _update_dependent_items_position(self, remote_squid_info, new_visual_x, new_visual_y):\r\n        if remote_squid_info.get('status_text'):\r\n            remote_squid_info['status_text'].setPos(new_visual_x, new_visual_y - 30)\r\n        if remote_squid_info.get('id_text'):\r\n            remote_squid_info['id_text'].setPos(new_visual_x, new_visual_y - 45)\r\n\r\n    def _update_visuals_once_per_second(self):\r\n        for node_id, remote_squid_info in list(self.remote_squids.items()):\r\n            visual_item = remote_squid_info.get('visual')\r\n            if not visual_item: continue\r\n\r\n            target_x = remote_squid_info.get('network_target_x')\r\n            target_y = remote_squid_info.get('network_target_y')\r\n            if target_x is None or target_y is None: continue\r\n\r\n            current_pos = visual_item.pos()\r\n            target_pos = QtCore.QPointF(target_x, target_y)\r\n            if current_pos == target_pos: continue\r\n\r\n            vector_to_target = target_pos - current_pos\r\n            distance_to_target = math.sqrt(vector_to_target.x()**2 + vector_to_target.y()**2)\r\n            new_pos: QtCore.QPointF\r\n\r\n            if distance_to_target <= self.MAX_PIXELS_PER_JUMP:\r\n                new_pos = target_pos\r\n            else:\r\n                normalized_x = vector_to_target.x() / distance_to_target\r\n                normalized_y = vector_to_target.y() / distance_to_target\r\n                new_pos = QtCore.QPointF(\r\n                    current_pos.x() + normalized_x * self.MAX_PIXELS_PER_JUMP,\r\n                    current_pos.y() + normalized_y * self.MAX_PIXELS_PER_JUMP\r\n                )\r\n            \r\n            visual_item.setPos(new_pos)\r\n            self._update_dependent_items_position(remote_squid_info, new_pos.x(), new_pos.y())\r\n            # No debug log here by default to avoid spam, enable if needed\r\n\r\n    def _handle_new_squid_arrival(self, node_id, squid_data_payload, entry_x, entry_y, entry_direction_on_this_screen):\r\n        if self.debug_mode:\r\n            self.logger.debug(f\"_handle_new_squid_arrival: NodeID='{node_id}', EntryPos=({entry_x:.1f},{entry_y:.1f}), EntryDir='{entry_direction_on_this_screen}'\")\r\n            self.logger.debug(f\"Payload for new arrival '{node_id}': {squid_data_payload}\")\r\n\r\n        payload_dir_key = squid_data_payload.get('image_direction_key', 'right')\r\n        payload_anim_frame = squid_data_payload.get('current_animation_frame', 1) \r\n        \r\n        squid_image_name, determined_facing_direction = self._get_image_file_name_and_direction(\r\n            payload_dir_key, \r\n            payload_anim_frame, \r\n            entry_direction_on_this_screen=entry_direction_on_this_screen \r\n        )\r\n        \r\n        scaled_pixmap, (current_w, current_h) = self._get_scaled_pixmap(squid_image_name)\r\n        \r\n        if self.debug_mode:\r\n            self.logger.debug(f\"_handle_new_squid_arrival '{node_id}': Image selected='{squid_image_name}', Determined Facing='{determined_facing_direction}', Size=({current_w}x{current_h})\")\r\n\r\n        remote_visual = AnimatableGraphicsItem(scaled_pixmap)\r\n        remote_visual.setPos(entry_x, entry_y)\r\n        remote_visual.setZValue(5)\r\n        remote_visual.setOpacity(self.remote_opacity)\r\n        remote_visual.setScale(1.0)\r\n        self.scene.addItem(remote_visual)\r\n\r\n        id_text = self.text_pool.acquire()\r\n        if id_text.scene() != self.scene: \r\n            if id_text.scene(): id_text.scene().removeItem(id_text)\r\n            self.scene.addItem(id_text)\r\n        id_text.setPlainText(f\"Remote ({node_id[-4:]})\")\r\n        id_text.setDefaultTextColor(QtGui.QColor(200,200,200,200))\r\n        id_text.setFont(QtGui.QFont(\"Arial\", 8))\r\n        id_text.setZValue(6) \r\n        id_text.setVisible(self.show_labels)\r\n\r\n        status_text = self.text_pool.acquire()\r\n        if status_text.scene() != self.scene:\r\n            if status_text.scene(): status_text.scene().removeItem(status_text)\r\n            self.scene.addItem(status_text)\r\n        status_text.setPlainText(\"ENTERING...\") \r\n        status_text.setDefaultTextColor(QtGui.QColor(255,255,0)) \r\n        status_text.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\r\n        status_text.setZValue(6)\r\n        status_text.setVisible(self.show_labels)\r\n        \r\n        self._update_dependent_items_position({'id_text': id_text, 'status_text': status_text}, entry_x, entry_y)\r\n\r\n        self.remote_squids[node_id] = {\r\n            'visual': remote_visual, \r\n            'id_text': id_text, \r\n            'status_text': status_text,\r\n            'view_cone': None, \r\n            'last_update': time.time(),\r\n            'data': squid_data_payload.copy(), \r\n            'current_display_dimensions': (current_w, current_h),\r\n            'current_image_name': squid_image_name,\r\n            'was_arrival_text': True, \r\n            'network_target_x': entry_x, \r\n            'network_target_y': entry_y\r\n        }\r\n        if self.debug_mode:\r\n            self.logger.info(f\"REMOTE_ENTITY_MANAGER: Created NEW remote squid '{node_id}' at ({entry_x:.1f}, {entry_y:.1f}). Image: '{squid_image_name}', Size: {current_w}x{current_h}\")\r\n\r\n    def _handle_re_arriving_squid(self, node_id, squid_data_payload, remote_squid_info, entry_x, entry_y, entry_direction_on_this_screen):\r\n        if self.debug_mode:\r\n            self.logger.debug(f\"_handle_re_arriving_squid: NodeID='{node_id}', EntryPos=({entry_x:.1f},{entry_y:.1f}), EntryDir='{entry_direction_on_this_screen}'\")\r\n            self.logger.debug(f\"Payload for re-arriving '{node_id}': {squid_data_payload}\")\r\n\r\n        visual_item = remote_squid_info['visual']\r\n        visual_item.setPos(entry_x, entry_y)\r\n        visual_item.setOpacity(self.remote_opacity) \r\n        visual_item.setVisible(True)\r\n        visual_item.setScale(1.0) \r\n\r\n        payload_dir_key = squid_data_payload.get('image_direction_key', 'right')\r\n        payload_anim_frame = squid_data_payload.get('current_animation_frame', 1)\r\n        current_img_name_fallback = remote_squid_info.get('current_image_name', 'right1.png')\r\n\r\n        new_squid_image_name, determined_facing_direction = self._get_image_file_name_and_direction(\r\n            payload_dir_key, \r\n            payload_anim_frame, \r\n            entry_direction_on_this_screen=entry_direction_on_this_screen,\r\n            current_image_name_for_fallback_dir=current_img_name_fallback\r\n        )\r\n        scaled_pixmap, (current_w, current_h) = self._get_scaled_pixmap(new_squid_image_name)\r\n        visual_item.setPixmap(scaled_pixmap)\r\n        \r\n        if self.debug_mode:\r\n            self.logger.debug(f\"_handle_re_arriving_squid '{node_id}': Image selected='{new_squid_image_name}', Determined Facing='{determined_facing_direction}', Size=({current_w}x{current_h})\")\r\n\r\n        remote_squid_info['current_display_dimensions'] = (current_w, current_h)\r\n        remote_squid_info['current_image_name'] = new_squid_image_name\r\n        remote_squid_info['network_target_x'] = entry_x \r\n        remote_squid_info['network_target_y'] = entry_y\r\n        \r\n        status_item = remote_squid_info.get('status_text')\r\n        if status_item:\r\n            status_item.setPlainText(\"ENTERING...\")\r\n            status_item.setDefaultTextColor(QtGui.QColor(255,255,0)) \r\n            status_item.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\r\n            status_item.setVisible(self.show_labels) \r\n        \r\n        id_item = remote_squid_info.get('id_text')\r\n        if id_item: \r\n            id_item.setVisible(self.show_labels)\r\n\r\n        self._update_dependent_items_position(remote_squid_info, entry_x, entry_y)\r\n        remote_squid_info['was_arrival_text'] = True \r\n        \r\n        if self.debug_mode:\r\n            self.logger.info(f\"REMOTE_ENTITY_MANAGER: Re-initialized RE-ARRIVING squid '{node_id}' at ({entry_x:.1f}, {entry_y:.1f}). Image: '{new_squid_image_name}', Size: {current_w}x{current_h}\")\r\n\r\n    def _handle_existing_squid_update(self, node_id, squid_data_payload, remote_squid_info):\r\n        if self.debug_mode:\r\n            log_payload = {\r\n                'x': squid_data_payload.get('x'), 'y': squid_data_payload.get('y'),\r\n                'image_direction_key': squid_data_payload.get('image_direction_key'),\r\n                'current_animation_frame': squid_data_payload.get('current_animation_frame'),\r\n                'status': squid_data_payload.get('status'),\r\n                'view_cone_visible': squid_data_payload.get('view_cone_visible')\r\n            }\r\n            self.logger.debug(f\"_handle_existing_squid_update: NodeID='{node_id}'. Payload essentials: {log_payload}\")\r\n\r\n        network_x = squid_data_payload.get('x')\r\n        network_y = squid_data_payload.get('y')\r\n\r\n        if network_x is not None and network_y is not None:\r\n            remote_squid_info['network_target_x'] = network_x\r\n            remote_squid_info['network_target_y'] = network_y\r\n        else:\r\n            if self.debug_mode:\r\n                self.logger.warning(f\"_handle_existing_squid_update '{node_id}': Update missing x or y coordinates. Target position not updated.\")\r\n\r\n        visual_item = remote_squid_info.get('visual')\r\n        if not visual_item:\r\n            if self.debug_mode:\r\n                self.logger.error(f\"_handle_existing_squid_update '{node_id}': Visual item not found! Cannot update.\")\r\n            return False\r\n\r\n        new_status_from_payload = squid_data_payload.get('status', remote_squid_info.get('data',{}).get('status','visiting'))\r\n        status_text_item = remote_squid_info.get('status_text')\r\n        if status_text_item:\r\n            if status_text_item.toPlainText().upper() != new_status_from_payload.upper() or remote_squid_info.get('was_arrival_text', False): # Case-insensitive compare for current text\r\n                status_text_item.setPlainText(new_status_from_payload.upper())\r\n                if remote_squid_info.get('was_arrival_text', False) or \"ENTERING\" in status_text_item.toPlainText(): \r\n                    status_text_item.setDefaultTextColor(QtGui.QColor(200,200,200,230)) \r\n                    status_text_item.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Normal)) \r\n                    remote_squid_info['was_arrival_text'] = False\r\n            status_text_item.setVisible(self.show_labels)\r\n        \r\n        if 'view_cone_visible' in squid_data_payload: \r\n            if squid_data_payload['view_cone_visible']:\r\n                self.update_remote_view_cone(node_id, squid_data_payload) \r\n            elif remote_squid_info.get('view_cone') and remote_squid_info['view_cone'].scene():\r\n                self.scene.removeItem(remote_squid_info['view_cone'])\r\n                remote_squid_info['view_cone'] = None\r\n        \r\n        payload_dir_key = squid_data_payload.get('image_direction_key') \r\n        payload_anim_frame = squid_data_payload.get('current_animation_frame', 1)\r\n        current_img_name_fallback = remote_squid_info.get('current_image_name', 'right1.png')\r\n\r\n        potential_new_image_name, determined_facing_direction = self._get_image_file_name_and_direction(\r\n            payload_dir_key, \r\n            payload_anim_frame,\r\n            entry_direction_on_this_screen=None, \r\n            current_image_name_for_fallback_dir=current_img_name_fallback\r\n        )\r\n        \r\n        if potential_new_image_name != remote_squid_info.get('current_image_name'):\r\n            scaled_pixmap, (current_w, current_h) = self._get_scaled_pixmap(potential_new_image_name)\r\n            visual_item.setPixmap(scaled_pixmap) \r\n            \r\n            remote_squid_info['current_display_dimensions'] = (current_w, current_h)\r\n            remote_squid_info['current_image_name'] = potential_new_image_name\r\n            if self.debug_mode:\r\n                self.logger.debug(f\"_handle_existing_squid_update '{node_id}': Image CHANGED to '{potential_new_image_name}', New Facing='{determined_facing_direction}', Size=({current_w}x{current_h})\")\r\n        \r\n        return True\r\n\r\n    def update_remote_squid(self, node_id, squid_data_payload, is_new_arrival=False):\r\n        if self.debug_mode:\r\n            arrival_status = \"NEW ARRIVAL\" if is_new_arrival else \"EXISTING SQUID UPDATE\"\r\n            self.logger.debug(f\"update_remote_squid CALLED: NodeID='{node_id}', Status='{arrival_status}'\")\r\n            # Avoid logging full payload if too verbose, log key parts or hash if necessary\r\n            # For now, logging essential keys to understand context:\r\n            log_payload_essentials = {\r\n                'x': squid_data_payload.get('x'), 'y': squid_data_payload.get('y'),\r\n                'image_direction_key': squid_data_payload.get('image_direction_key'),\r\n                'status': squid_data_payload.get('status'),\r\n                'node_id_in_payload': squid_data_payload.get('node_id') # verify it matches argument node_id\r\n            }\r\n            self.logger.debug(f\"Payload essentials for '{node_id}': {log_payload_essentials}\")\r\n\r\n        if not squid_data_payload:\r\n            if self.debug_mode: self.logger.warning(f\"No data for remote squid {node_id}\")\r\n            return False\r\n        try:\r\n            if is_new_arrival:\r\n                entry_x, entry_y, entry_dir = self.calculate_entry_position(squid_data_payload)\r\n                if node_id in self.remote_squids:\r\n                    self._handle_re_arriving_squid(node_id, squid_data_payload, self.remote_squids[node_id], entry_x, entry_y, entry_dir)\r\n                else:\r\n                    self._handle_new_squid_arrival(node_id, squid_data_payload, entry_x, entry_y, entry_dir)\r\n                if node_id in self.remote_squids:\r\n                    self._create_arrival_animation(self.remote_squids[node_id]['visual'])\r\n                    if squid_data_payload.get('view_cone_visible', False):\r\n                        self.update_remote_view_cone(node_id, squid_data_payload)\r\n            elif node_id in self.remote_squids:\r\n                 self._handle_existing_squid_update(node_id, squid_data_payload, self.remote_squids[node_id])\r\n            else: \r\n                if self.debug_mode: self.logger.warning(f\"Update for unknown {node_id} (not new arrival). Ignoring.\")\r\n                return False\r\n\r\n            if node_id in self.remote_squids:\r\n                self.remote_squids[node_id]['data'].update(squid_data_payload)\r\n                self.remote_squids[node_id]['last_update'] = time.time()\r\n                return True\r\n            elif is_new_arrival and node_id not in self.remote_squids: # Should have been added\r\n                if self.debug_mode: self.logger.error(f\"New arrival {node_id} not added to remote_squids.\")\r\n                return False\r\n            return False \r\n\r\n        except Exception as e:\r\n            self.logger.error(f\"Error update_remote_squid for {node_id}: {e}\", exc_info=True)\r\n            if node_id in self.remote_squids and self.remote_squids[node_id].get('visual') and \\\r\n               not self.remote_squids[node_id]['visual'].scene():\r\n                 self.logger.info(f\"Cleanup partially processed squid {node_id} after error.\")\r\n                 self.remove_remote_squid(node_id)\r\n            return False\r\n\r\n    def calculate_entry_position(self, exit_data: dict) -> tuple[float, float, str]:\r\n        original_exit_direction = exit_data.get('direction')\r\n        original_exit_pos_x = exit_data.get('position', {}).get('x', 0)\r\n        original_exit_pos_y = exit_data.get('position', {}).get('y', 0)\r\n        squid_width_payload = int(exit_data.get('squid_width', 50))\r\n        squid_height_payload = int(exit_data.get('squid_height', 50))\r\n        current_window_width = self.window_width; current_window_height = self.window_height\r\n        entry_x, entry_y = 0.0, 0.0; entry_direction_on_this_screen = \"unknown\"\r\n        if original_exit_direction == 'right':\r\n            entry_x = -squid_width_payload*0.8; entry_y = original_exit_pos_y; entry_direction_on_this_screen = \"left\"\r\n        elif original_exit_direction == 'left':\r\n            entry_x = current_window_width - squid_width_payload*0.2; entry_y = original_exit_pos_y; entry_direction_on_this_screen = \"right\"\r\n        elif original_exit_direction == 'down':\r\n            entry_y = -squid_height_payload*0.8; entry_x = original_exit_pos_x; entry_direction_on_this_screen = \"top\"\r\n        elif original_exit_direction == 'up':\r\n            entry_y = current_window_height - squid_height_payload*0.2; entry_x = original_exit_pos_x; entry_direction_on_this_screen = \"bottom\"\r\n        else:\r\n            entry_x = current_window_width/2 - squid_width_payload/2; entry_y = current_window_height/2 - squid_height_payload/2; entry_direction_on_this_screen = \"center_fallback\"\r\n        if original_exit_direction in ['right','left']: entry_y = max(0, min(entry_y, current_window_height - squid_height_payload))\r\n        if original_exit_direction in ['up','down']: entry_x = max(0, min(entry_x, current_window_width - squid_width_payload))\r\n        node_id = exit_data.get('node_id')\r\n        if node_id: self._last_calculated_entry_details[node_id] = {'entry_pos': (entry_x, entry_y), 'entry_direction': entry_direction_on_this_screen}\r\n        return entry_x, entry_y, entry_direction_on_this_screen\r\n\r\n    def get_last_calculated_entry_details(self, node_id: str) -> dict | None: return self._last_calculated_entry_details.get(node_id)\r\n    def update_settings(self, opacity=None, show_labels=None, show_connections=None):\r\n        if opacity is not None: self.remote_opacity = opacity\r\n        current_target_opacity = self.remote_opacity\r\n        for squid_data in self.remote_squids.values():\r\n            if squid_data.get('visual'): squid_data['visual'].setOpacity(current_target_opacity)\r\n        if show_labels is not None:\r\n            self.show_labels = show_labels\r\n            for squid_data in self.remote_squids.values():\r\n                if squid_data.get('id_text'): squid_data['id_text'].setVisible(show_labels)\r\n                if squid_data.get('status_text'): squid_data['status_text'].setVisible(show_labels)\r\n        if show_connections is not None:\r\n            self.show_connections = show_connections\r\n            for line in self.connection_lines.values(): # Values, not items() for direct line objects\r\n                if line.scene(): line.setVisible(show_connections)\r\n\r\n    def update_remote_view_cone(self, node_id, squid_data):\r\n        if node_id not in self.remote_squids: return\r\n        remote_squid_info = self.remote_squids[node_id]; visual_item = remote_squid_info.get('visual')\r\n        if not visual_item: return\r\n        if remote_squid_info.get('view_cone') and remote_squid_info['view_cone'].scene(): self.scene.removeItem(remote_squid_info['view_cone'])\r\n        remote_squid_info['view_cone'] = None\r\n        if not squid_data.get('view_cone_visible', False): return\r\n        squid_visual_pos = visual_item.pos() # Uses current visual position\r\n        display_dims = remote_squid_info.get('current_display_dimensions')\r\n        if not display_dims: pixmap = visual_item.pixmap(); display_dims = (pixmap.width(), pixmap.height()) if not pixmap.isNull() else self.DEFAULT_IMAGE_DIMENSION\r\n        current_w, current_h = display_dims; item_scale = visual_item.scale()\r\n        squid_center_x = squid_visual_pos.x()+(current_w/2*item_scale); squid_center_y = squid_visual_pos.y()+(current_h/2*item_scale)\r\n        looking_direction_rad=squid_data.get('looking_direction',0.0); view_cone_angle_rad=squid_data.get('view_cone_angle',math.radians(50))\r\n        cone_length=squid_data.get('view_cone_length',150); cone_half_angle=view_cone_angle_rad/2.0\r\n        p1=QtCore.QPointF(squid_center_x,squid_center_y)\r\n        p2=QtCore.QPointF(squid_center_x+cone_length*math.cos(looking_direction_rad-cone_half_angle),squid_center_y+cone_length*math.sin(looking_direction_rad-cone_half_angle))\r\n        p3=QtCore.QPointF(squid_center_x+cone_length*math.cos(looking_direction_rad+cone_half_angle),squid_center_y+cone_length*math.sin(looking_direction_rad+cone_half_angle))\r\n        cone_poly=QtGui.QPolygonF([p1,p2,p3]); cone_item=QtWidgets.QGraphicsPolygonItem(cone_poly)\r\n        color_tuple=squid_data.get('color',(150,150,255)); q_color=QtGui.QColor(*color_tuple) if isinstance(color_tuple,tuple) else QtGui.QColor(150,150,255)\r\n        cone_item.setPen(QtGui.QPen(QtGui.QColor(q_color.red(),q_color.green(),q_color.blue(),0)))\r\n        cone_item.setBrush(QtGui.QBrush(QtGui.QColor(q_color.red(),q_color.green(),q_color.blue(),25)))\r\n        cone_item.setZValue(visual_item.zValue()-1); self.scene.addItem(cone_item); remote_squid_info['view_cone']=cone_item\r\n\r\n    def _create_arrival_animation(self, visual_item):\r\n        if hasattr(visual_item, 'setOpacity'): visual_item.setOpacity(self.remote_opacity)\r\n        if hasattr(visual_item, 'setScale'): visual_item.setScale(1.0)\r\n    def _reset_remote_squid_style(self, visual_item_or_node_id): # Full method\r\n        node_id=None; squid_display_data=None\r\n        if isinstance(visual_item_or_node_id,str): node_id=visual_item_or_node_id; squid_display_data=self.remote_squids.get(node_id)\r\n        elif isinstance(visual_item_or_node_id,QtWidgets.QGraphicsPixmapItem):\r\n            for nid,s_data in self.remote_squids.items():\r\n                if s_data.get('visual')==visual_item_or_node_id: node_id=nid; squid_display_data=s_data; break\r\n        if not squid_display_data: return\r\n        visual_item=squid_display_data.get('visual'); status_text_item=squid_display_data.get('status_text')\r\n        if visual_item: visual_item.setZValue(5); visual_item.setOpacity(self.remote_opacity); visual_item.setScale(1.0); visual_item.setGraphicsEffect(None)\r\n        if status_text_item:\r\n            current_status=squid_display_data.get('data',{}).get('status','visiting').upper()\r\n            if squid_display_data.get('was_arrival_text',False) and current_status not in [\"ENTERING...\",\"ARRIVING...\",squid_display_data.get('data',{}).get('status','visiting').upper()]:\r\n                status_text_item.setDefaultTextColor(QtGui.QColor(200,200,200,230)); status_text_item.setFont(QtGui.QFont(\"Arial\",10,QtGui.QFont.Normal))\r\n                status_text_item.setPlainText(squid_display_data.get('data',{}).get('status','visiting')); squid_display_data['was_arrival_text']=False\r\n            status_text_item.setZValue(visual_item.zValue()+1 if visual_item else 6)\r\n\r\n    def remove_remote_squid(self, node_id): # Full method\r\n        if node_id not in self.remote_squids: return\r\n        squid_data=self.remote_squids.pop(node_id)\r\n        for key in ['visual','view_cone','id_text','status_text']:\r\n            item=squid_data.get(key)\r\n            if item and item.scene():item.scene().removeItem(item)\r\n            if key in ['id_text','status_text'] and hasattr(self,'text_pool') and item in self.text_pool.in_use:self.text_pool.release(item)\r\n        if node_id in self.connection_lines:\r\n            line=self.connection_lines.pop(node_id)\r\n            if line.scene():line.scene().removeItem(line)\r\n        if self.debug_mode:self.logger.info(f\"Removed remote squid {node_id}.\")\r\n    def cleanup_stale_entities(self, timeout=20.0): # Full method\r\n        now=time.time()\r\n        stale_squids=[nid for nid,data in self.remote_squids.items() if now-data.get('last_update',0)>timeout]\r\n        for nid in stale_squids:self.remove_remote_squid(nid)\r\n        stale_objs=[oid for oid,data in self.remote_objects.items() if now-data.get('last_update',0)>timeout]\r\n        for oid in stale_objs:self.remove_remote_object(oid)\r\n        if stale_squids or stale_objs and self.debug_mode:self.logger.debug(f\"Stale cleanup: Removed {len(stale_squids)} squids, {len(stale_objs)} objects.\")\r\n        return len(stale_squids),len(stale_objs)\r\n\r\n    def remove_remote_object(self, obj_id): # Full method\r\n        if obj_id not in self.remote_objects:return\r\n        obj_data=self.remote_objects.pop(obj_id)\r\n        visual=obj_data.get('visual')\r\n        if visual and visual.scene():visual.scene().removeItem(visual)\r\n        if self.debug_mode:self.logger.debug(f\"Removed remote object {obj_id}\")\r\n    def cleanup_all(self): # Full method\r\n        for nid in list(self.remote_squids.keys()):self.remove_remote_squid(nid)\r\n        for oid in list(self.remote_objects.keys()):self.remove_remote_object(oid)\r\n        for line_id in list(self.connection_lines.keys()):\r\n            line=self.connection_lines.pop(line_id)\r\n            if line.scene():self.scene().removeItem(line)\r\n        if hasattr(self,'text_pool'):self.text_pool.clear()\r\n        if self.debug_mode:self.logger.info(\"RemoteEntityManager: All entities cleaned up.\")\r\n\r\n    def update_connection_lines(self, local_squid_pos_tuple): # Full method\r\n        if not self.show_connections:\r\n            for node_id,line in list(self.connection_lines.items()):\r\n                if line.scene():self.scene.removeItem(line); del self.connection_lines[node_id]\r\n            return\r\n        if not local_squid_pos_tuple or len(local_squid_pos_tuple)!=2:return\r\n        lx,ly=local_squid_pos_tuple; active_nodes=set()\r\n        for node_id,squid_info in self.remote_squids.items():\r\n            visual=squid_info.get('visual')\r\n            if not visual or not visual.isVisible() or not visual.scene():continue\r\n            active_nodes.add(node_id); r_pos=visual.pos()\r\n            dims=squid_info.get('current_display_dimensions')\r\n            if not dims:pixmap=visual.pixmap();dims=(pixmap.width(),pixmap.height()) if not pixmap.isNull() else self.DEFAULT_IMAGE_DIMENSION\r\n            w,h=dims; scale=visual.scale()\r\n            rx=r_pos.x()+(w/2*scale); ry=r_pos.y()+(h/2*scale)\r\n            color_tuple=squid_info.get('data',{}).get('color',(100,100,255)); q_color=QtGui.QColor(*color_tuple) if isinstance(color_tuple,tuple) else QtGui.QColor(100,100,255)\r\n            if node_id in self.connection_lines:\r\n                line=self.connection_lines[node_id]\r\n                if not line.scene():self.scene.addItem(line)\r\n                line.setLine(lx,ly,rx,ry);pen=line.pen();pen.setColor(QtGui.QColor(q_color.red(),q_color.green(),q_color.blue(),100));line.setPen(pen);line.setVisible(True)\r\n            else:\r\n                line=QtWidgets.QGraphicsLineItem(lx,ly,rx,ry)\r\n                pen=QtGui.QPen(QtGui.QColor(q_color.red(),q_color.green(),q_color.blue(),100));pen.setWidth(1);pen.setStyle(QtCore.Qt.SolidLine)\r\n                line.setPen(pen);line.setZValue(-10);line.setVisible(True);self.scene.addItem(line);self.connection_lines[node_id]=line\r\n        for node_id in list(self.connection_lines.keys()):\r\n            if node_id not in active_nodes:\r\n                if self.connection_lines[node_id].scene():self.scene.removeItem(self.connection_lines[node_id]);del self.connection_lines[node_id]"
  },
  {
    "path": "plugins/multiplayer/squid_multiplayer_autopilot.py",
    "content": "import random\r\nimport math\r\nimport sys\r\nimport time\r\nimport os\r\nimport threading\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nclass RemoteSquidController:\r\n    \"\"\"Controls behavior of squids away from their home instance\"\"\"\r\n\r\n    def __init__(self, squid_data, scene, plugin_instance=None, debug_mode=False, remote_entity_manager=None):\r\n        self.squid_data = squid_data.copy() # Ensure it's a copy\r\n        self.scene = scene\r\n        self.plugin_instance = plugin_instance\r\n        self.debug_mode = debug_mode\r\n        self.remote_entity_manager = remote_entity_manager\r\n\r\n        # Store node_id for convenience and consistent use\r\n        self.node_id = self.squid_data.get('node_id', 'UnknownRemoteNode')\r\n        self.short_node_id = self.node_id[-4:] # For concise console logs if needed\r\n\r\n        # Unique log file per remote squid instance\r\n        self.log_file_name = f\"autopilot_decisions_remote_{self.node_id}.txt\"\r\n\r\n        # Clear/initialize the log file for this session\r\n        if self.debug_mode:\r\n            try:\r\n                with open(self.log_file_name, 'w', encoding='utf-8') as f:\r\n                    f.write(f\"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Log for remote squid {self.node_id} (controller instance) started.\\n\")\r\n            except Exception as e:\r\n                print(f\"[AutoPilotSetup] Error clearing/creating log file {self.log_file_name}: {e}\")\r\n\r\n        self.entry_time = squid_data.get('entry_time', time.time())\r\n        self.entry_position = squid_data.get('entry_position', None) # Tuple (x,y) if provided\r\n        self.window_width = squid_data.get('window_width', 1280) # Provided by host context\r\n        self.window_height = squid_data.get('window_height', 900) # Provided by host context\r\n\r\n        entry_dir_on_this_screen = squid_data.get('entry_direction_on_this_screen')\r\n        if entry_dir_on_this_screen:\r\n            opposite_map = {\r\n                'left': 'right', 'right': 'left',\r\n                'up': 'down', 'down': 'up',\r\n                'top': 'down', 'bottom': 'up', # Aliases for clarity\r\n                'center_fallback': random.choice(['left', 'right', 'up', 'down']) # Should not happen if entry_dir valid\r\n            }\r\n            self.home_direction = opposite_map.get(entry_dir_on_this_screen.lower(), random.choice(['left', 'right', 'up', 'down']))\r\n        else:\r\n            # Fallback if entry_direction_on_this_screen is not provided in squid_data\r\n            # This will be called again in return_home if still None, using current position.\r\n            self.home_direction = squid_data.get('home_direction') # Use if provided directly\r\n            if not self.home_direction:\r\n                 # If still None, defer final determination to when return_home is called.\r\n                 # For __init__, it's okay if it's None here, as determine_home_direction() will be called.\r\n                 self._log_decision(f\"__init__: home_direction not determined yet (entry_direction_on_this_screen missing). Will determine later.\")\r\n\r\n\r\n        self.state = \"exploring\"\r\n        self.squid_data['status'] = \"exploring\" # Autopilot sets its own status\r\n\r\n        self.target_object = None\r\n        self.time_away = 0\r\n        self.max_time_away = random.randint(60, 180) # e.g. 1-3 minutes\r\n\r\n        self.food_eaten_count = 0\r\n        self.rock_interaction_count = 0 # Counts interactions with any stealable item\r\n        self.distance_traveled = 0\r\n        \r\n        self.rocks_stolen = 0 # Will reflect len(self.carried_items_data)\r\n        self.max_rocks_to_steal = random.randint(1, 3) # Max items the squid will try to carry\r\n        self.stealing_phase = False # True when actively trying to \"steal\" (interaction part)\r\n        \r\n        self.carried_items_data = [] # Stores detailed data of items being \"physically\" carried\r\n        \r\n        self.move_speed = 4.5 \r\n        self.direction_change_prob = 0.15 \r\n        self.next_decision_time = 0 \r\n        self.decision_interval = 0.5 # Time between major decision evaluations in seconds\r\n\r\n        self.last_update_time = time.time()\r\n\r\n        # Initial console print for immediate feedback (uses short_node_id)\r\n        print(f\"[AutoPilot __init__ {self.short_node_id}] Initialized. State: {self.state}, Status: {self.squid_data['status']}\")\r\n        print(f\"[AutoPilot __init__ {self.short_node_id}] Max Time: {self.max_time_away}s. Max Carry: {self.max_rocks_to_steal}. Home Dir: {self.home_direction if self.home_direction else 'TBD'}\")\r\n        \r\n        # Initial log to the dedicated file\r\n        self._log_decision(f\"Controller Initialized. Start State: {self.state}, Start Status: {self.squid_data['status']}, Max Time: {self.max_time_away:.1f}s, Max Carry: {self.max_rocks_to_steal}, Home Dir: {self.home_direction if self.home_direction else 'To be determined'}, Speed: {self.move_speed}, DirChangeProb: {self.direction_change_prob}\")\r\n\r\n    def _log_decision(self, decision_text: str):\r\n        if not self.debug_mode:\r\n            return\r\n        \r\n        timestamp = time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())\r\n        log_entry = f\"[{timestamp}] [SquidID: {self.node_id}] {decision_text}\\n\"\r\n        \r\n        try:\r\n            with open(self.log_file_name, 'a', encoding='utf-8') as f:\r\n                f.write(log_entry)\r\n        except Exception as e:\r\n            print(f\"[AutoPilotDecisionFileError] Could not write to {self.log_file_name} for SquidID {self.node_id}: {e}\")\r\n            print(f\"[AutoPilotDecisionFallbackLog] {log_entry.strip()}\")\r\n\r\n    def _capture_item_properties(self, game_item_object) -> dict | None:\r\n        if not game_item_object or not self.is_object_valid(game_item_object):\r\n            self._log_decision(f\"CaptureItemAttempt: FAILED - Target item is invalid or None ('{getattr(game_item_object, 'filename', game_item_object)}').\")\r\n            return None\r\n\r\n        item_pos = self.get_object_position(game_item_object)\r\n        item_filename = getattr(game_item_object, 'filename', 'unknown_item.png')\r\n        item_category = getattr(game_item_object, 'category', 'unknown')\r\n        item_scale = game_item_object.scale() if hasattr(game_item_object, 'scale') else 1.0\r\n        item_z_value = game_item_object.zValue() if hasattr(game_item_object, 'zValue') else 0.0\r\n        # item_rotation = game_item_object.rotation() if hasattr(game_item_object, 'rotation') else 0.0\r\n\r\n\r\n        properties = {\r\n            'original_filename': item_filename,\r\n            'original_category': item_category,\r\n            'original_x': item_pos[0], \r\n            'original_y': item_pos[1],\r\n            'scale': item_scale,\r\n            'zValue': item_z_value,\r\n            # 'rotation': item_rotation,\r\n        }\r\n        base_name = os.path.basename(item_filename) if isinstance(item_filename, str) else \"unknown_item_name\"\r\n        self._log_decision(f\"CaptureItemSuccess: Captured properties for '{base_name}': Scale {properties['scale']:.2f}, Category '{properties['original_category']}'.\")\r\n        return properties\r\n\r\n    def update(self, delta_time=None):\r\n\r\n        current_time_autopilot = time.time() \r\n        \r\n        if delta_time is None:\r\n            delta_time = current_time_autopilot - self.last_update_time\r\n        self.last_update_time = current_time_autopilot\r\n\r\n        # Ensure delta_time is non-negative and reasonable\r\n        delta_time = max(0, delta_time)\r\n        if delta_time > 1.0: # Cap delta_time to prevent huge jumps if there was a long pause\r\n            self._log_decision(f\"Warning: Large delta_time detected: {delta_time:.2f}s. Capping to 1.0s for this update.\")\r\n            delta_time = 1.0\r\n\r\n        self.time_away += delta_time\r\n\r\n        self._log_decision(f\"Update Cycle Begin. State='{self.state}', Status='{self.squid_data.get('status', 'N/A')}', TimeAway={self.time_away:.1f}/{self.max_time_away:.1f}s, NextDecisionAt={self.next_decision_time:.3f}, DeltaT={delta_time:.3f}\")\r\n\r\n        if current_time_autopilot < self.next_decision_time:\r\n            self.move_in_direction(self.squid_data['direction'])\r\n            if self.remote_entity_manager:\r\n                self.remote_entity_manager.update_remote_squid(self.node_id, self.squid_data, is_new_arrival=False)\r\n            self._log_decision(f\"Update Cycle: Holding decision. Moving {self.squid_data['direction']}. Pos: ({self.squid_data['x']:.1f}, {self.squid_data['y']:.1f})\")\r\n            return\r\n\r\n        self._log_decision(f\"Update Cycle: Making new decision. Old state: '{self.state}', Old status: '{self.squid_data.get('status', 'N/A')}'\")\r\n        self.next_decision_time = current_time_autopilot + self.decision_interval\r\n        \r\n        if self.time_away > self.max_time_away and self.state != \"returning\" and self.state != \"exited\":\r\n            old_state_before_timeout = self.state\r\n            self.state = \"returning\"\r\n            self.squid_data['status'] = \"returning home (timeout)\"\r\n            self._log_decision(f\"Update Cycle: Max time away ({self.max_time_away:.1f}s) EXCEEDED. Forcing state change: {old_state_before_timeout} -> {self.state}.\")\r\n\r\n        # State machine\r\n        if self.state == \"exploring\":\r\n            self.explore()\r\n        elif self.state == \"feeding\":\r\n            self.seek_food()\r\n        elif self.state == \"interacting\":\r\n            self.interact_with_object()\r\n        elif self.state == \"returning\":\r\n            self.return_home()\r\n        elif self.state == \"exited\":\r\n            self._log_decision(\"Update Cycle: State is 'exited'. No further action.\")\r\n            return \r\n\r\n        # After state logic, update visuals if not exited\r\n        if self.remote_entity_manager and self.state != \"exited\":\r\n            self.remote_entity_manager.update_remote_squid(self.node_id, self.squid_data, is_new_arrival=False)\r\n        self._log_decision(f\"Update Cycle End. New State='{self.state}', New Status='{self.squid_data.get('status', 'N/A')}'\")\r\n\r\n\r\n    def explore(self):\r\n        self._log_decision(f\"Explore State: Current direction: {self.squid_data.get('direction')}. Time away: {self.time_away:.1f}s.\")\r\n        \r\n        if self.squid_data.get('status') != \"exploring\": # Ensure status matches state\r\n            self.squid_data['status'] = \"exploring\"\r\n            self._log_decision(f\"Explore: (Status corrected to 'exploring')\")\r\n\r\n        # This check is now also in update(), but keeping it here as a safeguard for explore's logic\r\n        if self.time_away > self.max_time_away:\r\n            old_state = self.state\r\n            self.state = \"returning\"\r\n            self.squid_data['status'] = \"returning home (explore timeout)\"\r\n            self._log_decision(f\"Explore: Max time away ({self.max_time_away:.1f}s) reached. State change: {old_state} -> {self.state}.\")\r\n            return\r\n\r\n        if random.random() < self.direction_change_prob:\r\n            old_direction = self.squid_data.get('direction', 'N/A')\r\n            new_direction = random.choice(['left', 'right', 'up', 'down'])\r\n            if new_direction == old_direction: # Try to pick a different one\r\n                choices = list(set(['left', 'right', 'up', 'down']) - {old_direction})\r\n                new_direction = random.choice(choices) if choices else new_direction\r\n            self.squid_data['direction'] = new_direction\r\n            self._log_decision(f\"Explore: Random direction change {old_direction} -> {new_direction} (Prob: {self.direction_change_prob:.2f}).\")\r\n        else:\r\n            self._log_decision(f\"Explore: No random direction change (Prob: {self.direction_change_prob:.2f}). Sticking to {self.squid_data.get('direction')}.\")\r\n        \r\n        self.move_in_direction(self.squid_data.get('direction', 'right')) # Move first\r\n\r\n        food_check_prob = 0.20 \r\n        steal_check_prob = 0.30 \r\n\r\n        if random.random() < food_check_prob:\r\n            food = self.find_nearby_food() # This logs internally now\r\n            if food:\r\n                self.target_object = food\r\n                old_state = self.state\r\n                self.state = \"feeding\"\r\n                self.squid_data['status'] = \"heading to food\"\r\n                target_name = os.path.basename(getattr(self.target_object, 'filename', 'UnknownFood'))\r\n                self._log_decision(f\"Explore: Spotted food '{target_name}'. State change: {old_state} -> {self.state}.\")\r\n                return \r\n            # else: find_nearby_food logs if nothing found\r\n        \r\n        if self.state == \"exploring\" and random.random() < steal_check_prob: \r\n            if len(self.carried_items_data) < self.max_rocks_to_steal:\r\n                stealable_item = self.find_nearby_stealable_item() # This logs internally now\r\n                if stealable_item:\r\n                    self.target_object = stealable_item\r\n                    old_state = self.state\r\n                    self.state = \"interacting\"\r\n                    self.squid_data['status'] = \"checking item\"\r\n                    item_type = getattr(self.target_object, 'category', 'item')\r\n                    item_name = os.path.basename(getattr(self.target_object, 'filename', f'Unknown{item_type.capitalize()}'))\r\n                    self._log_decision(f\"Explore: Spotted stealable {item_type} '{item_name}'. State change: {old_state} -> {self.state}.\")\r\n                    return\r\n            else: # Log if wanted to steal but couldn't due to carry limit\r\n                if len(self.carried_items_data) >= self.max_rocks_to_steal:\r\n                    self._log_decision(f\"Explore: Considered stealing item, but already carrying max ({len(self.carried_items_data)}/{self.max_rocks_to_steal}).\")\r\n        \r\n        if self.state == \"exploring\": # If no other action taken\r\n             self._log_decision(f\"Explore: No new targets. Continuing exploration in direction {self.squid_data.get('direction')}.\")\r\n\r\n\r\n    def seek_food(self):\r\n        if not self.target_object or not self.is_object_valid(self.target_object):\r\n            old_target_name = os.path.basename(getattr(self.target_object, 'filename', 'previous food target'))\r\n            old_state = self.state\r\n            self.state = \"exploring\"\r\n            self.squid_data['status'] = \"exploring\"\r\n            self._log_decision(f\"SeekFood: Lost or invalid food target '{old_target_name}'. State change: {old_state} -> {self.state}.\")\r\n            self.target_object = None\r\n            return\r\n\r\n        if self.squid_data.get('status') != \"heading to food\":\r\n            self.squid_data['status'] = \"heading to food\"\r\n            self._log_decision(f\"SeekFood: (Status corrected to 'heading to food')\")\r\n            \r\n        target_pos = self.get_object_position(self.target_object)\r\n        self.move_toward(target_pos[0], target_pos[1])\r\n\r\n        squid_pos = (self.squid_data['x'], self.squid_data['y'])\r\n        distance = self.distance_between(squid_pos, target_pos)\r\n        food_name = os.path.basename(getattr(self.target_object, 'filename', 'UnknownFood'))\r\n\r\n        self._log_decision(f\"SeekFood: Moving towards '{food_name}' at ({target_pos[0]:.1f}, {target_pos[1]:.1f}). Distance: {distance:.1f}.\")\r\n\r\n        if distance < 50: \r\n            self.eat_food(self.target_object) # This method already logs \"Action: Eating food\"\r\n            self.food_eaten_count += 1\r\n            old_state = self.state\r\n            self.state = \"exploring\" \r\n            self.squid_data['status'] = \"exploring\" # Reset status after eating\r\n            self._log_decision(f\"SeekFood: Successfully ate food '{food_name}'. Food count: {self.food_eaten_count}. State change: {old_state} -> {self.state}.\")\r\n            self.target_object = None\r\n\r\n\r\n    def interact_with_object(self):\r\n        if not self.target_object or not self.is_object_valid(self.target_object):\r\n            old_target_name = os.path.basename(getattr(self.target_object, 'filename', 'previous item target'))\r\n            old_state = self.state\r\n            self.state = \"exploring\"\r\n            self.squid_data['status'] = \"exploring\"\r\n            self._log_decision(f\"Interact: Lost or invalid item target '{old_target_name}'. State change: {old_state} -> {self.state}.\")\r\n            self.target_object = None\r\n            self.stealing_phase = False\r\n            return\r\n\r\n        current_status = self.squid_data.get('status', '')\r\n        if current_status != \"checking item\" and not self.stealing_phase: # Only set if not already in a carrying/stealing related status\r\n            self.squid_data['status'] = \"checking item\"\r\n            self._log_decision(f\"Interact: (Status set to 'checking item')\")\r\n\r\n        target_pos = self.get_object_position(self.target_object)\r\n        self.move_toward(target_pos[0], target_pos[1])\r\n\r\n        squid_pos = (self.squid_data['x'], self.squid_data['y'])\r\n        distance = self.distance_between(squid_pos, target_pos)\r\n        item_name_for_log = os.path.basename(getattr(self.target_object, 'filename', 'UnknownItem'))\r\n        self._log_decision(f\"Interact: Moving towards '{item_name_for_log}' at ({target_pos[0]:.1f}, {target_pos[1]:.1f}). Distance: {distance:.1f}.\")\r\n\r\n        if distance < 50: \r\n            self.rock_interaction_count += 1\r\n            attempt_steal_chance = 0.4\r\n\r\n            if (self.is_stealable_target(self.target_object) and\r\n                len(self.carried_items_data) < self.max_rocks_to_steal and\r\n                random.random() < attempt_steal_chance):\r\n\r\n                item_data_to_carry = self._capture_item_properties(self.target_object)\r\n                if item_data_to_carry:\r\n                    self.carried_items_data.append(item_data_to_carry)\r\n                    self.rocks_stolen = len(self.carried_items_data) \r\n                    self.squid_data['carrying_rock'] = True \r\n                    self.stealing_phase = True \r\n\r\n                    item_type_stolen = item_data_to_carry.get('original_category', 'item')\r\n                    item_name_stolen = os.path.basename(item_data_to_carry.get('original_filename', f'UnknownItem'))\r\n                    \r\n                    self.squid_data['status'] = f\"carrying {item_type_stolen.lower()}\"\r\n                    self._log_decision(f\"Interact: SUCCEEDED steal of {item_type_stolen} '{item_name_stolen}'. Status: {self.squid_data['status']}. Carrying {self.rocks_stolen}/{self.max_rocks_to_steal}.\")\r\n                    \r\n                    if self.remote_entity_manager and hasattr(self.remote_entity_manager, 'hide_item_temporarily'):\r\n                        self.remote_entity_manager.hide_item_temporarily(self.target_object)\r\n\r\n                    if self.rocks_stolen >= self.max_rocks_to_steal:\r\n                        old_state = self.state\r\n                        self.state = \"returning\"\r\n                        self.squid_data['status'] = \"returning home\" # Set status for returning\r\n                        self._log_decision(f\"Interact: Met carrying quota ({self.rocks_stolen}/{self.max_rocks_to_steal}). State change: {old_state} -> {self.state}.\")\r\n                        self.target_object = None \r\n                        self.stealing_phase = False # Done with stealing for now\r\n                        return \r\n                else:\r\n                    self._log_decision(f\"Interact: Attempted steal but FAILED to capture properties for target '{item_name_for_log}'.\")\r\n            else: \r\n                reason = \"\"\r\n                if not self.is_stealable_target(self.target_object): reason = \"target not stealable type\"\r\n                elif len(self.carried_items_data) >= self.max_rocks_to_steal: reason = \"carrying quota met\"\r\n                else: reason = f\"failed {attempt_steal_chance*100:.0f}% steal chance\"\r\n                self._log_decision(f\"Interact: Interacted with '{item_name_for_log}'. Did not steal (Reason: {reason}). Total interactions: {self.rock_interaction_count}.\")\r\n            \r\n            old_state = self.state \r\n            self.target_object = None\r\n            self.state = \"exploring\"\r\n            self.squid_data['status'] = \"exploring\" # Reset status\r\n            self.stealing_phase = False # Reset stealing phase\r\n            self._log_decision(f\"Interact: Interaction logic complete for '{item_name_for_log}'. State change: {old_state} -> {self.state}.\")\r\n\r\n\r\n    def return_home(self):\r\n        if self.squid_data.get('status') != \"returning home\" and not self.squid_data.get('status', '').startswith(\"returning home\"): # Check variants\r\n            self.squid_data['status'] = \"returning home\"\r\n            self._log_decision(f\"ReturnHome: (Status set to 'returning home')\")\r\n\r\n        if not self.home_direction: # Should have been set by __init__ or explore timeout\r\n            self.determine_home_direction() # Recalculate if somehow lost\r\n            self._log_decision(f\"ReturnHome: home_direction was None, re-determined: {self.home_direction}.\")\r\n\r\n        self.move_in_direction(self.home_direction) # This method now logs boundary hits and turns\r\n        self._log_decision(f\"ReturnHome: Moving towards {self.home_direction}. Position: ({self.squid_data['x']:.1f}, {self.squid_data['y']:.1f}).\")\r\n\r\n        if self.is_at_boundary(self.home_direction):\r\n            summary = self.get_summary() \r\n            self._log_decision(f\"ReturnHome: Reached home boundary ({self.home_direction}). Exiting. Summary: Ate {summary['food_eaten']}, Interacted {summary['rock_interactions']}, Stole {summary['rocks_stolen']} items.\")\r\n            \r\n            if self.plugin_instance and hasattr(self.plugin_instance, 'handle_remote_squid_return'):\r\n                self.plugin_instance.handle_remote_squid_return(self.node_id, self) \r\n            else:\r\n                self._log_decision(f\"ReturnHome: CRITICAL - plugin_instance or handle_remote_squid_return method missing for {self.node_id}.\")\r\n\r\n            self.state = \"exited\" \r\n            self.squid_data['status'] = \"exited\" # Final status for this controller instance\r\n            self._log_decision(f\"ReturnHome: State set to 'exited'.\")\r\n\r\n\r\n    def move_in_direction(self, direction):\r\n        speed = self.move_speed\r\n        prev_x, prev_y = self.squid_data['x'], self.squid_data['y']\r\n        \r\n        squid_width = self.squid_data.get('squid_width', 50)\r\n        squid_height = self.squid_data.get('squid_height', 50)\r\n        win_width = self.get_window_width()\r\n        win_height = self.get_window_height()\r\n\r\n        new_x, new_y = self.squid_data['x'], self.squid_data['y']\r\n        original_direction = str(direction) # Keep a copy for logging\r\n        current_effective_direction = str(direction) # What direction it will actually end up going\r\n\r\n        if current_effective_direction == 'left': new_x -= speed\r\n        elif current_effective_direction == 'right': new_x += speed\r\n        elif current_effective_direction == 'up': new_y -= speed\r\n        elif current_effective_direction == 'down': new_y += speed\r\n        \r\n        boundary_hit_log_message = \"\"\r\n\r\n        # Horizontal boundary check\r\n        if new_x <= 0:\r\n            new_x = 0\r\n            current_effective_direction = 'right'\r\n            boundary_hit_log_message = f\"Hit left boundary (was going {original_direction}), turning right.\"\r\n        elif new_x + squid_width >= win_width:\r\n            new_x = win_width - squid_width\r\n            current_effective_direction = 'left'\r\n            boundary_hit_log_message = f\"Hit right boundary (was going {original_direction}), turning left.\"\r\n        \r\n        # Vertical boundary check (can override horizontal turn if cornered)\r\n        if new_y <= 0:\r\n            new_y = 0\r\n            # If it also hit a side, the horizontal turn takes precedence for the new 'direction'\r\n            # but we still log the vertical hit.\r\n            if not boundary_hit_log_message: current_effective_direction = 'down' # Only change if not already turning from side\r\n            boundary_hit_log_message += (\" \" if boundary_hit_log_message else \"\") + f\"Hit top boundary (was going {original_direction}), ensuring not moving further up.\"\r\n        elif new_y + squid_height >= win_height:\r\n            new_y = win_height - squid_height\r\n            if not boundary_hit_log_message: current_effective_direction = 'up'\r\n            boundary_hit_log_message += (\" \" if boundary_hit_log_message else \"\") + f\"Hit bottom boundary (was going {original_direction}), ensuring not moving further down.\"\r\n\r\n        if boundary_hit_log_message and boundary_hit_log_message != \"No boundary hit.\": # Log if a boundary was actually hit\r\n             self._log_decision(f\"Move: {boundary_hit_log_message} New effective direction: {current_effective_direction}. Pos: ({new_x:.1f},{new_y:.1f})\")\r\n\r\n        self.squid_data['x'] = new_x\r\n        self.squid_data['y'] = new_y\r\n        self.squid_data['direction'] = current_effective_direction \r\n        \r\n        if current_effective_direction in ['left', 'right', 'up', 'down']:\r\n            self.squid_data['image_direction_key'] = current_effective_direction\r\n        \r\n        moved_dist = math.sqrt((self.squid_data['x'] - prev_x)**2 + (self.squid_data['y'] - prev_y)**2)\r\n        self.distance_traveled += moved_dist\r\n\r\n    def move_toward(self, target_x, target_y):\r\n        current_x = self.squid_data['x'] + self.squid_data.get('squid_width', 50) / 2\r\n        current_y = self.squid_data['y'] + self.squid_data.get('squid_height', 50) / 2\r\n        \r\n        dx, dy = target_x - current_x, target_y - current_y\r\n        chosen_direction = self.squid_data.get('direction', 'right')\r\n\r\n        if abs(dx) > self.move_speed / 2 or abs(dy) > self.move_speed / 2: \r\n            if abs(dx) > abs(dy) * 1.2: # Prioritize horizontal if significantly greater\r\n                chosen_direction = 'right' if dx > 0 else 'left'\r\n            elif abs(dy) > abs(dx) * 1.2: # Prioritize vertical if significantly greater\r\n                chosen_direction = 'down' if dy > 0 else 'up'\r\n            else: # Diagonal-ish: pick dominant or maintain current if aligned\r\n                if abs(dx) > abs(dy):\r\n                    chosen_direction = 'right' if dx > 0 else 'left'\r\n                else:\r\n                    chosen_direction = 'down' if dy > 0 else 'up'\r\n        \r\n        if chosen_direction != self.squid_data.get('direction'):\r\n            self._log_decision(f\"MoveToward: Target ({target_x:.0f},{target_y:.0f}), Current ({current_x:.0f},{current_y:.0f}). Direction changed to {chosen_direction}.\")\r\n        \r\n        self.move_in_direction(chosen_direction)\r\n\r\n    def find_nearby_food(self):\r\n        self._log_decision(f\"FIND_NEARBY_FOOD: Entered method for {self.node_id}.\")\r\n        food_items = self.get_food_items_from_scene()\r\n        if not food_items:\r\n            self._log_decision(f\"FIND_NEARBY_FOOD: No food items from get_food_items_from_scene for {self.node_id}.\")\r\n            return None\r\n        \r\n        self._log_decision(f\"FIND_NEARBY_FOOD: Found {len(food_items)} potential food items for {self.node_id}.\")\r\n        \r\n        squid_pos = (self.squid_data['x'] + self.squid_data.get('squid_width',0)/2, \r\n                    self.squid_data['y'] + self.squid_data.get('squid_height',0)/2)\r\n        closest_food, min_dist = None, float('inf')\r\n\r\n        for i, food in enumerate(food_items):\r\n            self._log_decision(f\"FIND_NEARBY_FOOD: Processing item {i} for {self.node_id}. Filename: {getattr(food, 'filename', 'N/A')}\")\r\n            \r\n            food_center_pos = None \r\n            center_x = 0.0\r\n            center_y = 0.0\r\n            \r\n            try:\r\n                if food and self.is_object_valid(food):\r\n                    self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - Food item IS valid.\")\r\n                    \r\n                    item_rect_local = food.boundingRect()\r\n                    item_pos_scene = food.pos() # Get QPointF\r\n                    \r\n                    pos_x_val = item_pos_scene.x() # Known to be reliable\r\n                    rect_center_obj = item_rect_local.center()\r\n                    rect_center_x_val = rect_center_obj.x() # Known to be reliable\r\n                    center_x = pos_x_val + rect_center_x_val\r\n\r\n                    pos_y_val = None\r\n                    # Prioritize reading the stored Python attribute\r\n                    if hasattr(food, 'has_current_y_for_autopilot') and food.has_current_y_for_autopilot:\r\n                        pos_y_val = food.current_y_for_autopilot\r\n                        self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - Used food.current_y_for_autopilot: {pos_y_val}\")\r\n                    else:\r\n                        self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - food.current_y_for_autopilot not found/set. Attempting item_pos_scene.y()\")\r\n                        if item_pos_scene is not None:\r\n                            try:\r\n                                pos_y_val = item_pos_scene.y()\r\n                                self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - item_pos_scene.y() succeeded: {pos_y_val}\")\r\n                            except Exception as e_pos_y:\r\n                                self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - !!! EXCEPTION during item_pos_scene.y() !!! Type: {type(e_pos_y).__name__}, Msg: {str(e_pos_y)}. Using fallback 0.0.\")\r\n                                pos_y_val = 0.0 # Fallback if direct access fails\r\n                        else:\r\n                            self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - item_pos_scene was None. Using fallback 0.0 for pos_y_val.\")\r\n                            pos_y_val = 0.0\r\n                    \r\n                    rect_center_y_val = rect_center_obj.y() # Known to be reliable\r\n\r\n                    center_y = float(pos_y_val if pos_y_val is not None else 0.0) + \\\r\n                            float(rect_center_y_val if rect_center_y_val is not None else 0.0)\r\n                            \r\n                    food_center_pos = (center_x, center_y)\r\n                    self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - Calculated center: {food_center_pos}\")\r\n                else:\r\n                    self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - Food item NOT valid.\")\r\n\r\n            except Exception as e_outer_inline: \r\n                self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - *** Outer Exception for item {i}: {type(e_outer_inline).__name__}: {e_outer_inline} *** food_center_pos set to None.\")\r\n                food_center_pos = None \r\n                # No 'raise' here to allow autopilot to continue\r\n            \r\n            if food_center_pos is None:\r\n                self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - food_center_pos is None after logic block. Skipping.\")\r\n                continue\r\n\r\n            dist = self.distance_between(squid_pos, food_center_pos)\r\n            self._log_decision(f\"FIND_NEARBY_FOOD: Item {i} - Distance to food ({getattr(food, 'filename', 'N/A')} at {food_center_pos}): {dist:.1f}\")\r\n            if dist < min_dist: \r\n                min_dist = dist\r\n                closest_food = food\r\n        \r\n        detection_radius = 300 \r\n        chosen_food = closest_food if closest_food and min_dist < detection_radius else None\r\n        \r\n        if chosen_food:\r\n            self._log_decision(f\"FindFood: Target acquired for {self.node_id}: {os.path.basename(getattr(chosen_food, 'filename', 'N/A'))} at distance {min_dist:.1f}.\")\r\n        elif food_items: \r\n            min_dist_str = f\"{min_dist:.1f}\" if min_dist != float('inf') else \"N/A\" \r\n            self._log_decision(f\"FindFood: Food items detected for {self.node_id} ({len(food_items)}), but none close/suitable (min_dist: {min_dist_str}, detection_radius: {detection_radius}).\")\r\n        else: \r\n            self._log_decision(f\"FindFood: No food items found in scene for {self.node_id} (list was empty or all invalid).\")\r\n            \r\n        return chosen_food\r\n\r\n    def find_nearby_stealable_item(self):\r\n        items = self.get_stealable_items_from_scene() # This now logs periodically\r\n        if not items: return None\r\n\r\n        squid_pos = (self.squid_data['x'] + self.squid_data.get('squid_width',0)/2, \r\n                     self.squid_data['y'] + self.squid_data.get('squid_height',0)/2)\r\n        closest_item, min_dist = None, float('inf')\r\n\r\n        for item_obj in items:\r\n            # is_object_valid should have been called by get_stealable_items_from_scene implicitly\r\n            item_center_pos = self.get_object_center_position(item_obj)\r\n            if item_center_pos is None: continue\r\n            \r\n            dist = self.distance_between(squid_pos, item_center_pos)\r\n            if dist < min_dist: \r\n                min_dist = dist\r\n                closest_item = item_obj\r\n        \r\n        detection_radius = 200\r\n        chosen_item = closest_item if closest_item and min_dist < detection_radius else None\r\n        if chosen_item:\r\n            self._log_decision(f\"FindStealable: Target acquired: {os.path.basename(getattr(chosen_item, 'filename', 'N/A'))} at distance {min_dist:.1f}.\")\r\n        elif items:\r\n            min_dist_str = f\"{min_dist:.1f}\" if min_dist != float('inf') else \"N/A\"\r\n            self._log_decision(f\"FindStealable: Stealable items detected ({len(items)}), but none close/suitable (min_dist: {min_dist_str}, detection_radius: {detection_radius}).\")\r\n        return chosen_item\r\n\r\n    def get_food_items_from_scene(self):\r\n        food_items = []\r\n        if not self.scene: \r\n            if self.debug_mode: self._log_decision(\"get_food_items: Scene not available.\")\r\n            return food_items\r\n        \r\n        items_checked_count = 0\r\n        scene_items_list = list(self.scene.items()) \r\n\r\n        for item in scene_items_list:\r\n            items_checked_count += 1\r\n            try:\r\n                if not self.is_object_valid(item): # Use the enhanced is_object_valid\r\n                    continue\r\n\r\n                is_food = False\r\n                item_category = str(getattr(item, 'category', '')).lower()\r\n                item_filename = str(getattr(item, 'filename', '')).lower()\r\n\r\n                if item_category == 'food':\r\n                    is_food = True\r\n                elif any(ft_keyword in item_filename for ft_keyword in ['food', 'sushi', 'cheese']):\r\n                    is_food = True\r\n                \r\n                if is_food:\r\n                     food_items.append(item)\r\n            except Exception as e:\r\n                if self.debug_mode: self._log_decision(f\"get_food_items: Error checking item - {type(item)}: {e}\")\r\n        \r\n        if self.debug_mode and (random.random() < 0.05 or not food_items): \r\n             log_food_names = [os.path.basename(getattr(f, 'filename', 'N/A')) for f in food_items]\r\n             self._log_decision(f\"get_food_items: Checked {items_checked_count} scene items. Found {len(food_items)} food: [{', '.join(log_food_names)}].\")\r\n        return food_items\r\n\r\n    def get_stealable_items_from_scene(self):\r\n        stealable_items = []\r\n        if not self.scene: \r\n            if self.debug_mode: self._log_decision(\"get_stealable_items: Scene not available.\")\r\n            return stealable_items\r\n            \r\n        items_checked_count = 0\r\n        scene_items_list = list(self.scene.items())\r\n\r\n        for item_obj in scene_items_list:\r\n            items_checked_count +=1\r\n            try:\r\n                if not self.is_object_valid(item_obj): # Use the enhanced is_object_valid\r\n                    continue\r\n                \r\n                # Crucially, do not attempt to steal items that are already clones from other remote players\r\n                # This check is now also part of is_stealable_target, but good to have consistency\r\n                if getattr(item_obj, 'is_remote_clone', False):\r\n                    continue\r\n\r\n                item_category = str(getattr(item_obj, 'category', '')).lower()\r\n                item_filename = str(getattr(item_obj, 'filename', '')).lower()\r\n                \r\n                is_rock = item_category == 'rock' or ('rock' in item_filename)\r\n                is_urchin = item_category == 'urchin' or ('urchin' in item_filename)\r\n\r\n                if (is_rock or is_urchin):\r\n                    stealable_items.append(item_obj)\r\n            except Exception as e:\r\n                if self.debug_mode: self._log_decision(f\"get_stealable_items: Error checking item - {type(item_obj)}: {e}\")\r\n        \r\n        if self.debug_mode and (random.random() < 0.05 or not stealable_items):\r\n            log_item_names = [os.path.basename(getattr(s, 'filename', 'N/A')) for s in stealable_items]\r\n            self._log_decision(f\"get_stealable_items: Checked {items_checked_count} scene items. Found {len(stealable_items)} stealable: [{', '.join(log_item_names)}].\")\r\n        return stealable_items\r\n\r\n    def is_in_vision_range(self, item): \r\n        if not item or not self.is_object_valid(item): return False\r\n        squid_center_pos = (self.squid_data['x'] + self.squid_data.get('squid_width',0)/2, \r\n                           self.squid_data['y'] + self.squid_data.get('squid_height',0)/2)\r\n        obj_center_pos = self.get_object_center_position(item)\r\n        if obj_center_pos is None: return False\r\n        return self.distance_between(squid_center_pos, obj_center_pos) < 800 # Generic large range\r\n\r\n    def animate_movement(self, squid_data, remote_visual): \r\n        if self.debug_mode: self._log_decision(f\"Animate_movement called (currently advisory). Pos: ({squid_data.get('x',0):.1f}, {squid_data.get('y',0):.1f}) Dir: {squid_data.get('direction')}\")\r\n\r\n    def eat_food(self, food_item): \r\n        food_name = os.path.basename(getattr(food_item, 'filename', 'UnknownFood'))\r\n        self._log_decision(f\"Action: Eating food '{food_name}'. Current hunger: {self.squid_data.get('hunger', 50):.1f}.\")\r\n        \r\n        self.squid_data['hunger'] = max(0, self.squid_data.get('hunger', 50) - 25) # More significant hunger reduction\r\n        self.squid_data['happiness'] = min(100, self.squid_data.get('happiness', 50) + 15)\r\n        \r\n        if self.remote_entity_manager and hasattr(self.remote_entity_manager, 'remove_item_from_scene'):\r\n            self.remote_entity_manager.remove_item_from_scene(food_item)\r\n            self._log_decision(f\"Signaled RemoteEntityManager to remove eaten food '{food_name}'. New hunger: {self.squid_data['hunger']:.1f}\")\r\n        else:\r\n            self._log_decision(f\"EatFood: Could not signal for removal of food '{food_name}'.\")\r\n\r\n    def interact_with_rock(self, rock_item): # Legacy, use interact_with_object\r\n        item_name = os.path.basename(getattr(rock_item, 'filename', 'UnknownItem'))\r\n        self._log_decision(f\"Action: Legacy interact_with_rock called for '{item_name}'.\")\r\n        self.squid_data['happiness'] = min(100, self.squid_data.get('happiness', 50) + 5)\r\n\r\n    def is_stealable_target(self, item_obj): \r\n        if not self.is_object_valid(item_obj): return False\r\n        if getattr(item_obj, 'is_remote_clone', False): # Should not steal clones\r\n            self._log_decision(f\"is_stealable_target: Item '{getattr(item_obj, 'filename', 'N/A')}' is a remote clone. Cannot steal.\")\r\n            return False\r\n\r\n        item_category = str(getattr(item_obj, 'category', '')).lower()\r\n        item_filename = str(getattr(item_obj, 'filename', '')).lower()\r\n        \r\n        is_rock = item_category == 'rock' or ('rock' in item_filename)\r\n        is_urchin = item_category == 'urchin' or ('urchin' in item_filename) # Example of another stealable\r\n        \r\n        # Add more conditions for stealable items if needed\r\n        # e.g. is_plant = item_category == 'plant' or ('plant' in item_filename)\r\n        \r\n        can_steal = is_rock or is_urchin # or is_plant etc.\r\n        if can_steal and self.debug_mode:\r\n            self._log_decision(f\"is_stealable_target: Item '{item_filename}' (cat: {item_category}) IS stealable.\")\r\n        elif not can_steal and self.debug_mode:\r\n            self._log_decision(f\"is_stealable_target: Item '{item_filename}' (cat: {item_category}) is NOT stealable.\")\r\n        return can_steal\r\n\r\n\r\n    def is_object_valid(self, obj):\r\n        if obj is None:\r\n            if self.debug_mode: self._log_decision(\"is_object_valid: FAILED - Object is None.\")\r\n            return False\r\n        \r\n        # Ensure obj is a QGraphicsItem before calling QGraphicsItem-specific methods\r\n        if not isinstance(obj, QtWidgets.QGraphicsItem):\r\n            if self.debug_mode: self._log_decision(f\"is_object_valid: FAILED - Object '{type(obj)}' is not a QGraphicsItem.\")\r\n            return False\r\n\r\n        has_scene_attr = hasattr(obj, 'scene')\r\n        obj_scene_instance = None\r\n        \r\n        if has_scene_attr and callable(obj.scene):\r\n            try:\r\n                obj_scene_instance = obj.scene() # Call with no arguments as per PyQt5\r\n            except TypeError as e: # Catch if called incorrectly (e.g. if API differs unexpectedly)\r\n                if self.debug_mode: \r\n                    self._log_decision(f\"is_object_valid: FAILED - TypeError calling obj.scene() for {getattr(obj, 'filename', type(obj))}: {e}\")\r\n                return False \r\n            except Exception as e_scene: # Catch other potential errors during scene() call\r\n                if self.debug_mode:\r\n                    self._log_decision(f\"is_object_valid: FAILED - Exception calling obj.scene() for {getattr(obj, 'filename', type(obj))}: {e_scene}\")\r\n                return False\r\n        else: # If no scene attribute or not callable\r\n            if self.debug_mode:\r\n                self._log_decision(f\"is_object_valid: FAILED - Object {getattr(obj, 'filename', type(obj))} has no callable 'scene' attribute.\")\r\n            # Depending on logic, this might be an invalid object, or an object not yet added to a scene.\r\n            # For now, if it's supposed to be in *our* scene, this is invalid.\r\n            return False\r\n\r\n\r\n        is_in_correct_scene = obj_scene_instance is self.scene # Direct comparison with the controller's scene\r\n        \r\n        is_visible_attr = hasattr(obj, 'isVisible')\r\n        is_currently_visible = obj.isVisible() if is_visible_attr and callable(obj.isVisible) else True \r\n        \r\n        valid = is_in_correct_scene and is_currently_visible\r\n\r\n        if not valid and self.debug_mode: \r\n            filename_info = getattr(obj, 'filename', str(type(obj))) \r\n            reasons = []\r\n            if not is_in_correct_scene:\r\n                reason_scene = \"Scene mismatch/None\"\r\n                if obj_scene_instance: # If we got a scene instance but it's not self.scene\r\n                    reason_scene += f\" (ItemSceneID: {id(obj_scene_instance)}, AutopilotSceneID: {id(self.scene)})\"\r\n                elif has_scene_attr and callable(obj.scene): # If scene() was callable but returned None\r\n                    reason_scene += f\" (Item's scene() returned None, AutopilotSceneID: {id(self.scene) if self.scene else 'None'})\"\r\n                else: # If obj.scene wasn't even callable or present\r\n                     reason_scene += f\" (Item has no valid scene, AutopilotSceneID: {id(self.scene) if self.scene else 'None'})\"\r\n                reasons.append(reason_scene)\r\n            if not is_currently_visible:\r\n                reasons.append(\"Not visible\")\r\n            self._log_decision(f\"is_object_valid: Item '{filename_info}' FAILED validation. Reasons: {'; '.join(reasons) if reasons else 'Unknown (Logic Error in Validation)'}. Object Valid: {valid}\")\r\n            \r\n        return valid\r\n\r\n    def get_object_position(self, obj): # Gets top-left\r\n        if obj and hasattr(obj, 'pos') and callable(obj.pos):\r\n            pos_qpointf = obj.pos()\r\n            return (pos_qpointf.x(), pos_qpointf.y())\r\n        if self.debug_mode: self._log_decision(f\"Warning: get_object_position failed for object: {getattr(obj, 'filename', type(obj))}\")\r\n        return (self.squid_data.get('x',0), self.squid_data.get('y',0)) # Fallback\r\n\r\n    def get_object_center_position(self, obj):\r\n        if obj and self.is_object_valid(obj): # Ensure it's valid before getting rect\r\n            try:\r\n                # QGraphicsPixmapItem.boundingRect() is in item's local coordinates.\r\n                # We need its sceneBoundingRect for global center, or combine pos() with boundingRect().center()\r\n                item_rect_local = obj.boundingRect() # Local bounds\r\n                item_pos_scene = obj.pos() # Top-left in scene\r\n                center_x = item_pos_scene.x() + item_rect_local.center().x()\r\n                center_y = item_pos_scene.y() + item_rect_local.center().y()\r\n                return (center_x, center_y)\r\n            except Exception as e:\r\n                if self.debug_mode: self._log_decision(f\"Error getting center for {getattr(obj, 'filename', type(obj))}: {e}\")\r\n        return None\r\n\r\n\r\n    def distance_between(self, pos1, pos2):\r\n        try:\r\n            return math.sqrt((pos1[0] - pos2[0])**2 + (pos1[1] - pos2[1])**2)\r\n        except TypeError: # If pos1 or pos2 is None or not subscriptable\r\n            self._log_decision(f\"Error in distance_between: Invalid input. Pos1: {pos1}, Pos2: {pos2}\")\r\n            return float('inf')\r\n\r\n    def get_window_width(self):\r\n        if self.remote_entity_manager and hasattr(self.remote_entity_manager, 'window_width'):\r\n            return self.remote_entity_manager.window_width\r\n        elif self.scene and hasattr(self.scene, 'sceneRect') and self.scene.sceneRect():\r\n            return self.scene.sceneRect().width()\r\n        return self.window_width # Fallback to initial value\r\n\r\n    def get_window_height(self):\r\n        if self.remote_entity_manager and hasattr(self.remote_entity_manager, 'window_height'):\r\n            return self.remote_entity_manager.window_height\r\n        elif self.scene and hasattr(self.scene, 'sceneRect') and self.scene.sceneRect():\r\n            return self.scene.sceneRect().height()\r\n        return self.window_height # Fallback to initial value\r\n\r\n    def is_at_boundary(self, direction_moving_towards: str):\r\n        x, y = self.squid_data['x'], self.squid_data['y']\r\n        squid_w = self.squid_data.get('squid_width', 50)\r\n        squid_h = self.squid_data.get('squid_height', 50)\r\n        # Threshold for being \"at\" the boundary to trigger exit\r\n        # Should be small enough that it's definitely at edge, but not so small it overshoots.\r\n        boundary_exit_threshold = self.move_speed * 1.5 # Approx 1.5 move steps from edge\r\n\r\n        win_width = self.get_window_width()\r\n        win_height = self.get_window_height()\r\n\r\n        if direction_moving_towards == 'left': return x <= boundary_exit_threshold\r\n        elif direction_moving_towards == 'right': return x + squid_w >= win_width - boundary_exit_threshold\r\n        elif direction_moving_towards == 'up': return y <= boundary_exit_threshold\r\n        elif direction_moving_towards == 'down': return y + squid_h >= win_height - boundary_exit_threshold\r\n        return False\r\n\r\n    def determine_home_direction(self):\r\n        # This method determines the \"exit\" direction from the current client's perspective\r\n        # to get \"home\" (back to its original instance).\r\n        entry_dir_on_this_screen = self.squid_data.get('entry_direction_on_this_screen')\r\n        opposite_map = {'left': 'right', 'right': 'left', 'up': 'down', 'down': 'up', 'top': 'down', 'bottom': 'up'}\r\n        \r\n        if entry_dir_on_this_screen and entry_dir_on_this_screen.lower() in opposite_map:\r\n            self.home_direction = opposite_map[entry_dir_on_this_screen.lower()]\r\n            self._log_decision(f\"DetermineHomeDir: Determined home direction '{self.home_direction}' as opposite of entry_direction '{entry_dir_on_this_screen}'.\")\r\n        else:\r\n            # Fallback: if entry direction was unclear, choose the closest edge as the exit.\r\n            # This is less ideal as it might not be the true \"opposite\" of how it entered.\r\n            x, y = self.squid_data.get('x', self.get_window_width()/2), self.squid_data.get('y', self.get_window_height()/2)\r\n            width, height = self.get_window_width(), self.get_window_height()\r\n            \r\n            distances_to_edge = {\r\n                'left': x,\r\n                'right': width - (x + self.squid_data.get('squid_width', 50)),\r\n                'up': y,\r\n                'down': height - (y + self.squid_data.get('squid_height', 50))\r\n            }\r\n            # Choose the edge it is currently closest to as its \"home\" direction.\r\n            self.home_direction = min(distances_to_edge, key=distances_to_edge.get)\r\n            self._log_decision(f\"DetermineHomeDir: Fallback - entry_direction unclear. Closest edge chosen as home_direction: '{self.home_direction}'. Distances: {distances_to_edge}\")\r\n\r\n    def get_summary(self):\r\n        actual_items_carried_count = len(self.carried_items_data)\r\n        if self.rocks_stolen != actual_items_carried_count: # Ensure consistency\r\n            self._log_decision(f\"Summary: Correcting 'rocks_stolen' from {self.rocks_stolen} to actual carried count {actual_items_carried_count}.\")\r\n            self.rocks_stolen = actual_items_carried_count\r\n\r\n        summary_data = {\r\n            'time_away': round(self.time_away, 2),\r\n            'food_eaten': self.food_eaten_count,\r\n            'rock_interactions': self.rock_interaction_count, # Total interactions with stealable types\r\n            'rocks_stolen': self.rocks_stolen, # Count of items successfully \"stolen\" and data captured\r\n            'carried_items_details': list(self.carried_items_data), # Ensure it's a list copy\r\n            'distance_traveled': round(self.distance_traveled, 2),\r\n            'final_state_on_this_client': self.state, \r\n            'node_id': self.node_id # ID of the squid this controller is for\r\n        }\r\n        self._log_decision(f\"GetSummary: Generating summary - Food: {summary_data['food_eaten']}, Interactions: {summary_data['rock_interactions']}, Stolen: {summary_data['rocks_stolen']}, Items: {len(summary_data['carried_items_details'])}\")\r\n        return summary_data"
  },
  {
    "path": "plugins/multiplayer/status_bar_component.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nclass StatusBarComponent:\r\n    def __init__(self, main_window):\r\n        self.main_window = main_window\r\n        \r\n        # Create the status bar if it doesn't exist\r\n        if not main_window.statusBar():\r\n            self.status_bar = QtWidgets.QStatusBar(main_window)\r\n            main_window.setStatusBar(self.status_bar)\r\n        else:\r\n            self.status_bar = main_window.statusBar()\r\n        \r\n        # Create status indicators\r\n        self.create_indicators()\r\n        \r\n        # Message queue for rotating messages\r\n        self.message_queue = []\r\n        self.message_timer = QtCore.QTimer()\r\n        self.message_timer.timeout.connect(self.rotate_messages)\r\n        self.message_timer.start(5000)  # Rotate messages every 5 seconds\r\n    \r\n    def create_indicators(self):\r\n        \"\"\"Create permanent status indicators\"\"\"\r\n        # Plugins indicator\r\n        self.plugins_label = QtWidgets.QLabel(\"Plugins: None\")\r\n        self.plugins_label.setStyleSheet(\"padding: 0 10px;\")\r\n        self.status_bar.addPermanentWidget(self.plugins_label)\r\n        \r\n        # Network status indicator\r\n        self.network_label = QtWidgets.QLabel(\"Network: Disconnected\")\r\n        self.network_label.setStyleSheet(\"padding: 0 10px;\")\r\n        self.status_bar.addPermanentWidget(self.network_label)\r\n        \r\n        # Peers indicator\r\n        self.peers_label = QtWidgets.QLabel(\"Peers: 0\")\r\n        self.peers_label.setStyleSheet(\"padding: 0 10px;\")\r\n        self.status_bar.addPermanentWidget(self.peers_label)\r\n    \r\n    def update_plugins_status(self, plugin_manager):\r\n        \"\"\"Update the plugins status indicator\"\"\"\r\n        if not plugin_manager:\r\n            self.plugins_label.setText(\"Plugins: None\")\r\n            return\r\n        \r\n        enabled_plugins = plugin_manager.get_enabled_plugins()\r\n        if not enabled_plugins:\r\n            self.plugins_label.setText(\"Plugins: None\")\r\n            self.plugins_label.setStyleSheet(\"padding: 0 10px; color: gray;\")\r\n        else:\r\n            plugin_count = len(enabled_plugins)\r\n            self.plugins_label.setText(f\"Plugins: {plugin_count} active\")\r\n            self.plugins_label.setStyleSheet(\"padding: 0 10px; color: green;\")\r\n            \r\n            # Create tooltip with plugin names\r\n            tooltip = \"Active plugins:\\n\" + \"\\n\".join(f\"• {p}\" for p in enabled_plugins)\r\n            self.plugins_label.setToolTip(tooltip)\r\n    \r\n    def update_network_status(self, connected, node_id=None):\r\n        \"\"\"Update the network status indicator\"\"\"\r\n        if connected:\r\n            self.network_label.setText(f\"Network: Connected\")\r\n            self.network_label.setStyleSheet(\"padding: 0 10px; color: green;\")\r\n            if node_id:\r\n                self.network_label.setToolTip(f\"Connected as {node_id}\")\r\n        else:\r\n            self.network_label.setText(\"Network: Disconnected\")\r\n            self.network_label.setStyleSheet(\"padding: 0 10px; color: gray;\")\r\n            self.network_label.setToolTip(\"Network functionality is disconnected\")\r\n    \r\n    def update_peers_count(self, count):\r\n        \"\"\"Update the peers count indicator\"\"\"\r\n        self.peers_label.setText(f\"Peers: {count}\")\r\n        if count > 0:\r\n            self.peers_label.setStyleSheet(\"padding: 0 10px; color: green;\")\r\n        else:\r\n            self.peers_label.setStyleSheet(\"padding: 0 10px; color: gray;\")\r\n    \r\n    def add_message(self, message, duration=5000):\r\n        \"\"\"Add a temporary message to the status bar\"\"\"\r\n        self.status_bar.showMessage(message, duration)\r\n    \r\n    def add_to_message_queue(self, message):\r\n        \"\"\"Add a message to the rotation queue\"\"\"\r\n        if message not in self.message_queue:\r\n            self.message_queue.append(message)\r\n    \r\n    def rotate_messages(self):\r\n        \"\"\"Rotate through queued messages\"\"\"\r\n        if not self.message_queue:\r\n            return\r\n            \r\n        # Show the next message\r\n        message = self.message_queue.pop(0)\r\n        self.status_bar.showMessage(message, 4500)  # Show for slightly less than rotation time\r\n        \r\n        # Add the message back to the end of the queue\r\n        self.message_queue.append(message)"
  },
  {
    "path": "plugins/readme.md",
    "content": "#### [Achievements](https://github.com/ViciousSquid/Dosidicus/wiki/Achievements)\n\n\n#### [Multiplayer](https://github.com/ViciousSquid/Dosidicus/wiki/Multiplayer)\n\n\n#### [STDP](https://github.com/ViciousSquid/Dosidicus/wiki/Spike%E2%80%90Timing%E2%80%90Dependent-Plasticity-(STDP))\n\n\n\n\n-------------------------\n\n#### [Plugin system overview](https://github.com/ViciousSquid/Dosidicus/wiki/Plugin-system)\n"
  },
  {
    "path": "plugins/stdp/__init__.py",
    "content": "# STDP Plugin Package\n"
  },
  {
    "path": "plugins/stdp/main.py",
    "content": "\"\"\"\nSTDP Plugin for Dosidicus\n\nAugments the existing Hebbian learning system with Spike-Timing-Dependent\nPlasticity (STDP). Connections strengthen when the pre-synaptic neuron fires\nBEFORE the post-synaptic neuron (causal), and weaken when the order is\nreversed (acausal). This teaches the squid cause-and-effect rather than\nmere correlation.\n\nIntegration method: monkey-patches BrainWorker._perform_hebbian_learning on\nthe live instance, so it slots in without touching any core files.\n\nReward signals are applied on feed / clean / medicine events via the\nstandard hook system, using STDP eligibility traces as the third learning\nfactor.\n\"\"\"\n\nimport sys\nimport os\nimport time\nimport logging\nimport traceback\nfrom heapq import nlargest\nfrom collections import deque\nfrom typing import Optional, Dict\n\n# ---------------------------------------------------------------------------\n# Ensure the project root is importable (mirrors multiplayer plugin pattern)\n# ---------------------------------------------------------------------------\ntry:\n    _current_dir = os.path.dirname(os.path.abspath(__file__))\n    _project_root = os.path.abspath(os.path.join(_current_dir, '..', '..'))\n    if _project_root not in sys.path:\n        sys.path.insert(0, _project_root)\nexcept Exception as _e:\n    print(f\"STDP Plugin: sys.path setup warning: {_e}\")\n\nfrom PyQt5 import QtCore, QtWidgets\n\ntry:\n    from display_scaling import DisplayScaling\nexcept ImportError:\n    class DisplayScaling:\n        @classmethod\n        def font_size(cls, size): return size\n        @classmethod\n        def scale_css(cls, css): return css\n\n# Import STDP core from this package\ntry:\n    from .stdp_core import STDPLearner, STDPConfig\nexcept ImportError:\n    from stdp_core import STDPLearner, STDPConfig\n\n# ---------------------------------------------------------------------------\n# Plugin metadata\n# ---------------------------------------------------------------------------\nPLUGIN_NAME        = \"STDP\"\nPLUGIN_VERSION     = \"1.0.0\"\nPLUGIN_AUTHOR      = \"ViciousSquid\"\nPLUGIN_DESCRIPTION = \"Spike-Timing-Dependent Plasticity – adds causal learning on top of Hebbian\"\nPLUGIN_REQUIRES    = []\n\n# Neurons that receive external input – never modified by learning\n_PURE_INPUTS = {\n    \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\",\n    \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\",\n    \"plant_proximity\",\n}\n\n\n# ===========================================================================\n# Plugin class\n# ===========================================================================\n\nclass STDPPlugin:\n    \"\"\"\n    Integrates STDP learning into Dosidicus without modifying core files.\n\n    Lifecycle\n    ---------\n    setup()  – called by PluginManager once tamagotchi_logic is ready\n    cleanup() – called on disable / app exit\n    \"\"\"\n\n    def __init__(self):\n        self.logger: Optional[logging.Logger] = None\n        self.plugin_manager = None\n        self.tamagotchi_logic = None\n\n        # References resolved during setup()\n        self._brain_worker  = None   # BrainWorker QThread instance\n        self._brain_widget  = None   # BrainWidget QWidget instance\n\n        # The STDP learning engine\n        self.stdp_learner: Optional[STDPLearner] = None\n\n        # Saved reference so we can restore on cleanup\n        self._original_hebbian_fn = None\n\n        # Timer for periodic spike sampling (main thread)\n        self._spike_timer: Optional[QtCore.QTimer] = None\n\n        # Periodic cleanup timer\n        self._cleanup_timer: Optional[QtCore.QTimer] = None\n\n        # UI banner widget injected into the learning tab\n        self._ui_banner: Optional[QtWidgets.QWidget] = None\n        self._banner_stats_label: Optional[QtWidgets.QLabel] = None\n        self._banner_update_timer: Optional[QtCore.QTimer] = None\n\n        # Control panel (opened on demand from the Plugins menu)\n        self._panel = None\n\n        self.is_setup = False\n        self.enabled  = False\n\n        # Thread-safe queue: worker thread pushes (pair, weight_change, meta),\n        # main-thread spike timer drains it into the learning tab.\n        self._pending_tab_updates: deque = deque()\n\n        # Configurable parameters (can be adjusted at runtime)\n        self.config = STDPConfig(\n            tau_plus=0.15,\n            tau_minus=0.15,\n            A_plus=0.08,\n            A_minus=0.05,\n            time_window=0.5,\n            spike_threshold=60.0,\n            spike_rising_threshold=8.0,\n            refractory_period=0.08,\n            burst_bonus=1.5,\n            stdp_weight=0.4,        # 40 % STDP, 60 % Hebbian\n            eligibility_decay=0.95,\n            eligibility_window=2.0,\n        )\n\n    # -----------------------------------------------------------------------\n    # Setup / teardown\n    # -----------------------------------------------------------------------\n\n    def setup(self, plugin_manager, tamagotchi_logic) -> bool:\n        \"\"\"Called by PluginManager after core components are ready.\"\"\"\n        self.plugin_manager    = plugin_manager\n        self.tamagotchi_logic  = tamagotchi_logic\n\n        # Logger\n        if hasattr(plugin_manager, 'logger'):\n            self.logger = plugin_manager.logger.getChild(PLUGIN_NAME)\n        else:\n            self.logger = logging.getLogger(PLUGIN_NAME)\n            if not self.logger.handlers:\n                h = logging.StreamHandler()\n                h.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))\n                self.logger.addHandler(h)\n            self.logger.setLevel(logging.INFO)\n\n        self.logger.info(f\"Setting up {PLUGIN_NAME} v{PLUGIN_VERSION}...\")\n\n        # Build the learner\n        self.stdp_learner = STDPLearner(self.config)\n\n        # Resolve brain_worker and brain_widget\n        if not self._resolve_brain_references():\n            self.logger.error(\"Could not resolve BrainWorker / BrainWidget – setup aborted.\")\n            return False\n\n        # Patch the worker\n        self._install_hebbian_patch()\n\n        # Spike-sampling timer (runs on main thread, safe to touch brain_widget)\n        self._spike_timer = QtCore.QTimer()\n        self._spike_timer.timeout.connect(self._sample_spikes)\n        self._spike_timer.start(150)   # sample every 150 ms\n\n        # Periodic STDP cleanup timer\n        self._cleanup_timer = QtCore.QTimer()\n        self._cleanup_timer.timeout.connect(self._periodic_cleanup)\n        self._cleanup_timer.start(10_000)  # every 10 s\n\n        # Subscribe to game-event hooks for reward modulation\n        self._subscribe_hooks()\n\n        self.is_setup = True\n\n        # Respect is_enabled_by_default from the plugin registry.\n        # The core app calls setup() on every plugin unconditionally, so we\n        # must read the flag ourselves rather than relying on the caller.\n        plugin_key  = PLUGIN_NAME.lower()\n        plugin_data = plugin_manager.plugins.get(plugin_key, {})\n        self.enabled = plugin_data.get('is_enabled_by_default', False)\n\n        # Only inject the UI banner if the plugin is actually starting enabled.\n        if self.enabled:\n            QtCore.QTimer.singleShot(1200, self._inject_ui_banner)\n\n        self.logger.info(f\"{PLUGIN_NAME} setup complete. \"\n                         f\"Patched BrainWorker._perform_hebbian_learning ✓  \"\n                         f\"(enabled={self.enabled})\")\n\n        print(\"\\n\")\n        print(\"=\" * 60)\n        if self.enabled:\n            print(\"  ⚡  STDP LEARNING IS ACTIVE  ⚡\")\n        else:\n            print(\"  ⚡  STDP plugin loaded (DISABLED – enable via Plugins menu)  ⚡\")\n        print(\"=\" * 60)\n        print(f\"  Blend     : {int((1 - self.config.stdp_weight)*100)}% Hebbian  +  {int(self.config.stdp_weight*100)}% STDP\")\n        print(f\"  Window    : {int(self.config.time_window * 1000)} ms\")\n        print(f\"  Threshold : {self.config.spike_threshold}\")\n        print(f\"  Worker    : {type(self._brain_worker).__name__} patched ✓\")\n        print(\"=\" * 60)\n        print(\"  Watch for [LTP] / [LTD] tags on Hebbian lines\")\n        print(\"=\" * 60)\n        print(\"\\n\")\n\n        return True\n\n    def cleanup(self):\n        \"\"\"Restore original BrainWorker method and stop timers.\"\"\"\n        if self.logger:\n            self.logger.info(f\"{PLUGIN_NAME}: cleanup called\")\n\n        if self._spike_timer:\n            self._spike_timer.stop()\n        if self._cleanup_timer:\n            self._cleanup_timer.stop()\n        if self._banner_update_timer:\n            self._banner_update_timer.stop()\n\n        self._restore_hebbian_patch()\n\n        # Close control panel if open\n        if self._panel is not None:\n            try:\n                self._panel.close()\n            except Exception:\n                pass\n            self._panel = None\n\n        # Remove UI banner if present\n        if self._ui_banner is not None:\n            try:\n                self._ui_banner.setParent(None)\n                self._ui_banner.deleteLater()\n                self._ui_banner = None\n            except Exception:\n                pass\n\n        self.is_setup = False\n\n    def shutdown(self):\n        \"\"\"Called by PluginManager when the plugin is disabled/unloaded.\"\"\"\n        self.cleanup()\n\n    # -----------------------------------------------------------------------\n    # Reference resolution\n    # -----------------------------------------------------------------------\n\n    def _resolve_brain_references(self) -> bool:\n        \"\"\"Walk the object graph to find BrainWorker and BrainWidget.\"\"\"\n        tl = self.tamagotchi_logic\n\n        # brain_window → brain_widget\n        brain_window = getattr(tl, 'brain_window', None)\n        if brain_window is None:\n            # Try plugin_manager path\n            pm = self.plugin_manager\n            brain_window = getattr(pm, 'brain_window', None)\n\n        if brain_window is None:\n            self.logger.error(\"Cannot find brain_window on tamagotchi_logic or plugin_manager\")\n            return False\n\n        self._brain_widget = getattr(brain_window, 'brain_widget', None)\n        if self._brain_widget is None:\n            self.logger.error(\"brain_window has no brain_widget\")\n            return False\n\n        # brain_worker is typically on brain_widget (set by SquidBrainWindow)\n        self._brain_worker = getattr(self._brain_widget, 'brain_worker', None)\n        if self._brain_worker is None:\n            # Fallback: look on brain_window itself\n            self._brain_worker = getattr(brain_window, 'brain_worker', None)\n\n        if self._brain_worker is None:\n            self.logger.error(\"Cannot find brain_worker on brain_widget or brain_window\")\n            return False\n\n        self.logger.info(\n            f\"References resolved: worker={type(self._brain_worker).__name__} \"\n            f\"widget={type(self._brain_widget).__name__}\"\n        )\n        return True\n\n    # -----------------------------------------------------------------------\n    # Monkey-patch\n    # -----------------------------------------------------------------------\n\n    def _install_hebbian_patch(self):\n        \"\"\"Replace _perform_hebbian_learning on the live BrainWorker instance.\"\"\"\n        worker = self._brain_worker\n        self._original_hebbian_fn = worker._perform_hebbian_learning\n\n        stdp_plugin_ref = self   # captured in closure\n\n        def _stdp_perform_hebbian_learning():\n            \"\"\"STDP-augmented drop-in for BrainWorker._perform_hebbian_learning.\"\"\"\n            # Fall back to original if plugin was disabled/unloaded\n            if not stdp_plugin_ref.is_setup or stdp_plugin_ref._original_hebbian_fn is None:\n                if stdp_plugin_ref._original_hebbian_fn:\n                    stdp_plugin_ref._original_hebbian_fn()\n                return\n\n            # Fall back to original if STDP is toggled off by the user\n            if not stdp_plugin_ref.enabled:\n                stdp_plugin_ref._original_hebbian_fn()\n                return\n\n            stdp_plugin_ref._run_stdp_hebbian(worker)\n\n        # Bind to the instance (not the class) so only this worker is affected\n        import types\n        worker._perform_hebbian_learning = types.MethodType(\n            lambda self_w: _stdp_perform_hebbian_learning(), worker\n        )\n        # Simpler: just replace the bound reference directly\n        worker._perform_hebbian_learning = _stdp_perform_hebbian_learning\n\n        self.logger.info(\"Hebbian patch installed on BrainWorker instance\")\n\n    def _restore_hebbian_patch(self):\n        \"\"\"Undo the monkey-patch.\"\"\"\n        if self._brain_worker and self._original_hebbian_fn:\n            self._brain_worker._perform_hebbian_learning = self._original_hebbian_fn\n            self.logger.info(\"Hebbian patch removed – BrainWorker restored\")\n\n    # -----------------------------------------------------------------------\n    # Learning-tab UI banner\n    # -----------------------------------------------------------------------\n\n    def _inject_ui_banner(self):\n        \"\"\"\n        Insert a styled STDP status banner at the top of the Learning tab's\n        card area.  Safe to call from main thread only.\n        \"\"\"\n        if not self.enabled:\n            return\n        try:\n            brain_window = getattr(self.tamagotchi_logic, 'brain_window', None)\n            if brain_window is None:\n                return\n\n            nn_viz_tab = getattr(brain_window, 'nn_viz_tab', None)\n            if nn_viz_tab is None:\n                return\n\n            layout = getattr(nn_viz_tab, 'learning_content_layout', None)\n            if layout is None:\n                return\n\n            # Build the banner widget\n            banner = QtWidgets.QWidget()\n            banner.setObjectName(\"stdp_banner\")\n            banner.setStyleSheet(\"\"\"\n                QWidget#stdp_banner {\n                    background: qlineargradient(\n                        x1:0, y1:0, x2:1, y2:0,\n                        stop:0 #1a237e, stop:1 #283593\n                    );\n                    border-radius: 10px;\n                    border: 2px solid #5c6bc0;\n                }\n            \"\"\")\n\n            row = QtWidgets.QHBoxLayout(banner)\n            row.setContentsMargins(16, 12, 16, 12)\n            row.setSpacing(12)\n\n            # Lightning icon\n            icon_label = QtWidgets.QLabel(\"⚡\")\n            icon_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(28)}px; background: transparent;\")\n            row.addWidget(icon_label)\n\n            # Text block\n            text_col = QtWidgets.QVBoxLayout()\n            text_col.setSpacing(2)\n\n            title = QtWidgets.QLabel(\"STDP Learning Active\")\n            title.setStyleSheet(\n                f\"font-size: {DisplayScaling.font_size(15)}px; font-weight: 700; color: #e8eaf6; background: transparent;\"\n            )\n            text_col.addWidget(title)\n\n            detail = QtWidgets.QLabel(\n                f\"{int((1 - self.config.stdp_weight) * 100)}% Hebbian  +  \"\n                f\"{int(self.config.stdp_weight * 100)}% spike-timing  ·  \"\n                f\"window {int(self.config.time_window * 1000)} ms  ·  \"\n                f\"threshold {self.config.spike_threshold:.0f}\"\n            )\n            detail.setStyleSheet(\n                f\"font-size: {DisplayScaling.font_size(12)}px; color: #9fa8da; background: transparent;\"\n            )\n            text_col.addWidget(detail)\n            row.addLayout(text_col)\n\n            row.addStretch()\n\n            # Live LTP/LTD counters (updated by a timer)\n            self._banner_stats_label = QtWidgets.QLabel(\"LTP — · LTD —\")\n            self._banner_stats_label.setStyleSheet(\n                f\"font-size: {DisplayScaling.font_size(12)}px; font-weight: 600; color: #80cbc4; background: transparent;\"\n            )\n            row.addWidget(self._banner_stats_label)\n\n            # Insert at position 0 (before the stretch at the bottom)\n            layout.insertWidget(0, banner)\n            self._ui_banner = banner\n\n            # Timer to keep the LTP/LTD counter live\n            self._banner_update_timer = QtCore.QTimer()\n            self._banner_update_timer.timeout.connect(self._refresh_banner_stats)\n            self._banner_update_timer.start(3000)   # refresh every 3 s\n\n            self.logger.info(\"STDP banner injected into Learning tab\")\n\n        except Exception as exc:\n            self.logger.warning(f\"Could not inject UI banner: {exc}\")\n\n    def _refresh_banner_stats(self):\n        \"\"\"Update the live counter label on the banner.\"\"\"\n        if self._ui_banner is None or self.stdp_learner is None:\n            return\n        try:\n            stats = self.stdp_learner.get_stats()\n            ltp = stats.get('ltp_events', 0)\n            ltd = stats.get('ltd_events', 0)\n            spikes = stats.get('spike_stats', {}).get('total_spikes', 0)\n            self._banner_stats_label.setText(\n                f\"LTP {ltp}  ·  LTD {ltd}  ·  spikes {spikes}\"\n            )\n        except Exception:\n            pass\n\n    # -----------------------------------------------------------------------\n    # Patched Hebbian learning (runs on BrainWorker thread)\n    # -----------------------------------------------------------------------\n\n    def _run_stdp_hebbian(self, worker):\n        \"\"\"\n        STDP-augmented Hebbian learning.\n\n        Mirrors the original BrainWorker._perform_hebbian_learning logic but\n        replaces the pure-Hebbian delta with the combined STDP+Hebbian signal\n        from STDPLearner.compute_combined_learning().\n        \"\"\"\n        from PyQt5.QtCore import QMutexLocker\n\n        # --- Snapshot cache (thread-safe) ---\n        with QMutexLocker(worker._cache_mutex):\n            state           = worker.cache['state']\n            weights         = worker.cache['weights']\n            neuron_list     = list(worker.cache['positions'].keys())\n            excluded        = worker.cache['excluded_neurons']\n            connector_nrns  = worker.cache['connector_neurons']\n            config          = worker.cache['config']\n            base_lr         = worker.cache['learning_rate']\n            new_neurons     = worker.cache['new_neurons']\n            custom_neurons  = worker.cache.get('custom_neurons', set())\n\n        if not config:\n            self.logger.warning(\"STDP Hebbian skipped – no config in cache\")\n            return\n        if not neuron_list:\n            self.logger.warning(\"STDP Hebbian skipped – no neurons in cache\")\n            return\n\n        # Record spikes from current state into STDP learner\n        # (spike_tracker.record_batch is thread-safe via its own mutex)\n        self.stdp_learner.record_state(state)\n\n        # --- Build candidate list (same exclusion rules as original) ---\n        candidates = [\n            n for n in neuron_list\n            if n not in excluded\n            and n not in connector_nrns\n            and n not in _PURE_INPUTS\n        ]\n\n        if len(candidates) < 2:\n            worker.hebbian_result.emit({'updated_pairs': []})\n            return\n\n        # --- Score pairs ---\n        scored_pairs = []\n        import random\n        for i, n1 in enumerate(candidates):\n            for n2 in candidates[i + 1:]:\n                v1 = self._get_neuron_value(state.get(n1, 50))\n                v2 = self._get_neuron_value(state.get(n2, 50))\n                score = v1 + v2 + random.uniform(0, 40)\n\n                pair_key = tuple(sorted((n1, n2)))\n                if pair_key in worker._last_hebbian_pairs:\n                    score -= 500   # same cooldown penalty as original\n\n                scored_pairs.append((score, n1, n2, v1, v2))\n\n        if not scored_pairs:\n            return\n\n        # --- Select top pairs ---\n        top_k = 2\n        if hasattr(config, 'neurogenesis'):\n            top_k = config.neurogenesis.get('max_hebbian_pairs', 2)\n\n        top_pairs = nlargest(top_k, scored_pairs)\n        worker._last_hebbian_pairs = [tuple(sorted((n1, n2))) for _, n1, n2, _, _ in top_pairs]\n\n        # --- Compute weight updates with STDP ---\n        hebbian_cfg = getattr(config, 'hebbian', {})\n        decay_rate  = hebbian_cfg.get('weight_decay', 0.01)\n\n        weight_updates     = {}\n        updated_pairs_list = []\n        ltp_count = 0\n        ltd_count = 0\n\n        for _, n1, n2, v1, v2 in top_pairs:\n            pair         = (n1, n2)\n            reverse_pair = (n2, n1)\n\n            use_pair = None\n            if pair in weights:\n                use_pair = pair\n            elif reverse_pair in weights:\n                use_pair = reverse_pair\n\n            if not use_pair:\n                use_pair = pair\n                old_w = 0.0\n                weight_updates[use_pair] = {\n                    'old_weight': 0.0,\n                    'new_weight': 0.0,\n                    'is_new_connection': True,\n                }\n            else:\n                old_w = weights[use_pair]\n\n            # Learning rate boost for new neurons\n            lr = base_lr\n            if n1 in new_neurons or n2 in new_neurons:\n                lr *= 2.0\n\n            is_custom = (n1 in custom_neurons or n2 in custom_neurons)\n\n            # Combined STDP + Hebbian delta\n            combined_delta, meta = self.stdp_learner.compute_combined_learning(\n                n1, n2, v1, v2,\n                base_learning_rate=lr,\n                is_custom=is_custom,\n            )\n\n            if meta.get('is_ltp'):\n                ltp_count += 1\n            elif meta.get('is_ltd'):\n                ltd_count += 1\n\n            # Forward to control panel logger (main thread will flush)\n            if self._panel:\n                self._panel.record_encoding(\n                    n1, n2,\n                    meta.get('hebbian_delta', 0.0),\n                    meta.get('stdp_delta', 0.0),\n                    combined_delta,\n                    meta.get('stdp_direction', 'none'),\n                    self.config.stdp_weight,\n                )\n\n            new_w = old_w + combined_delta - (old_w * decay_rate)\n            new_w = max(-1.0, min(1.0, new_w))\n\n            is_new = (\n                use_pair in weight_updates\n                and weight_updates[use_pair].get('is_new_connection', False)\n            )\n            weight_updates[use_pair] = {\n                'old_weight': old_w,\n                'new_weight': new_w,\n                'is_new_connection': is_new,\n            }\n            updated_pairs_list.append(use_pair)\n\n            # Queue a learning-tab update with full STDP metadata (drained on main thread)\n            weight_change = \"increase\" if new_w > old_w else (\"decrease\" if new_w < old_w else None)\n            tab_meta = dict(meta)\n            tab_meta['stdp_weight'] = self.config.stdp_weight\n            self._pending_tab_updates.append((use_pair, weight_change, tab_meta))\n\n        if updated_pairs_list:\n            stdp_tag = \"\"\n            if ltp_count:\n                stdp_tag += f\" [LTP×{ltp_count}]\"\n            if ltd_count:\n                stdp_tag += f\" [LTD×{ltd_count}]\"\n            print(f\"🧠 STDP+Hebbian: updated {len(updated_pairs_list)} pair(s){stdp_tag}\")\n\n        worker.hebbian_result.emit({\n            'updated_pairs': updated_pairs_list,\n            'weight_updates': weight_updates,\n        })\n\n    @staticmethod\n    def _get_neuron_value(raw) -> float:\n        \"\"\"Normalise neuron values (mirrors BrainWorker._get_neuron_value).\"\"\"\n        if isinstance(raw, (int, float)):\n            return float(raw)\n        if isinstance(raw, bool):\n            return 100.0 if raw else 0.0\n        return 50.0\n\n    # -----------------------------------------------------------------------\n    # Spike sampling (main thread timer)\n    # -----------------------------------------------------------------------\n\n    def _sample_spikes(self):\n        \"\"\"\n        Periodically read the live brain state and feed it into the spike\n        tracker.  Runs on the main thread so it's safe to read brain_widget\n        and call into the learning tab UI.\n        \"\"\"\n        if not self.enabled or self.stdp_learner is None:\n            return\n        if self._brain_widget is None:\n            return\n\n        state = getattr(self._brain_widget, 'state', None)\n        if not state:\n            return\n\n        spikes = self.stdp_learner.record_state(state)\n\n        # Forward new spikes to the control panel logger\n        if self._panel and spikes:\n            tracker = self.stdp_learner.spike_tracker\n            for neuron_name, spike_event in spikes:\n                is_burst = tracker._is_bursting_nolock(neuron_name)\n                self._panel.record_spike(\n                    neuron_name,\n                    spike_event.activation_level,\n                    spike_event.was_rising,\n                    is_burst,\n                )\n\n        # Drain pending learning-tab updates (pushed by worker thread)\n        if self._pending_tab_updates:\n            nn_viz_tab = None\n            try:\n                brain_window = getattr(self.tamagotchi_logic, 'brain_window', None)\n                if brain_window:\n                    nn_viz_tab = getattr(brain_window, 'nn_viz_tab', None)\n            except Exception:\n                pass\n\n            while self._pending_tab_updates:\n                try:\n                    pair, weight_change, meta = self._pending_tab_updates.popleft()\n                    if nn_viz_tab is not None:\n                        nn_viz_tab.add_log_entry(\"\", pair, weight_change, stdp_meta=meta)\n                except Exception:\n                    pass\n\n    # -----------------------------------------------------------------------\n    # Reward modulation via eligibility traces\n    # -----------------------------------------------------------------------\n\n    def apply_reward(self, signal: float, reason: str = \"\"):\n        \"\"\"\n        Modulate recent eligibility traces by *signal* and apply the resulting\n        weight deltas directly to brain_widget.weights.\n\n        Positive signal → reinforce recent causal connections.\n        Negative signal → weaken them.\n        \"\"\"\n        if not self.enabled or self.stdp_learner is None:\n            return\n        if self._brain_widget is None:\n            return\n\n        deltas: Dict = self.stdp_learner.apply_reward_modulation(signal)\n        if not deltas:\n            return\n\n        weights = getattr(self._brain_widget, 'weights', {})\n        applied = 0\n        for (pre, post), delta in deltas.items():\n            pair = (pre, post)\n            rev  = (post, pre)\n            use  = pair if pair in weights else (rev if rev in weights else None)\n            if use:\n                old_w = weights[use]\n                weights[use] = max(-1.0, min(1.0, old_w + delta))\n                applied += 1\n\n        if applied:\n            tag = f\" ({reason})\" if reason else \"\"\n            sign = \"+\" if signal > 0 else \"\"\n            self.logger.debug(\n                f\"Reward{tag}: signal={sign}{signal:.2f}, \"\n                f\"modulated {applied} connection(s)\"\n            )\n            print(f\"⚡ STDP reward{tag}: {sign}{signal:.2f} → {applied} weight(s) modulated\")\n\n    # -----------------------------------------------------------------------\n    # Hook subscriptions\n    # -----------------------------------------------------------------------\n\n    def _subscribe_hooks(self):\n        pm = self.plugin_manager\n        if pm is None:\n            return\n\n        subscriptions = [\n            (\"on_feed\",      self._on_feed),\n            (\"on_clean\",     self._on_clean),\n            (\"on_medicine\",  self._on_medicine),\n            (\"on_sleep\",     self._on_sleep),\n            (\"on_wake\",      self._on_wake),\n            (\"on_startle\",   self._on_startle),\n        ]\n\n        for hook_name, cb in subscriptions:\n            try:\n                if hasattr(pm, 'subscribe_to_hook'):\n                    pm.subscribe_to_hook(hook_name, PLUGIN_NAME, cb)\n                    self.logger.debug(f\"Subscribed to {hook_name}\")\n            except Exception as exc:\n                self.logger.warning(f\"Could not subscribe to {hook_name}: {exc}\")\n\n    # Hook callbacks --------------------------------------------------------\n\n    def _on_feed(self, **kwargs):\n        self.apply_reward(+0.5, \"fed\")\n\n    def _on_clean(self, **kwargs):\n        self.apply_reward(+0.3, \"cleaned\")\n\n    def _on_medicine(self, **kwargs):\n        self.apply_reward(+0.4, \"medicine\")\n\n    def _on_sleep(self, **kwargs):\n        self.apply_reward(+0.2, \"sleep\")\n\n    def _on_wake(self, **kwargs):\n        # Waking resets recent context – mild positive\n        self.apply_reward(+0.1, \"wake\")\n\n    def _on_startle(self, **kwargs):\n        self.apply_reward(-0.3, \"startled\")\n\n    # -----------------------------------------------------------------------\n    # Periodic cleanup\n    # -----------------------------------------------------------------------\n\n    def _periodic_cleanup(self):\n        \"\"\"Remove stale spike records and eligibility traces.\"\"\"\n        if self.stdp_learner:\n            self.stdp_learner.cleanup()\n\n    # -----------------------------------------------------------------------\n    # Public API\n    # -----------------------------------------------------------------------\n\n    def get_stats(self) -> dict:\n        \"\"\"Return current STDP statistics (safe to call from any thread).\"\"\"\n        if self.stdp_learner is None:\n            return {}\n        stats = self.stdp_learner.get_stats()\n        stats['enabled'] = self.enabled\n        stats['stdp_weight'] = self.config.stdp_weight\n        return stats\n\n    def set_stdp_weight(self, weight: float):\n        \"\"\"Adjust the STDP/Hebbian blend ratio at runtime (0 = pure Hebbian, 1 = pure STDP).\"\"\"\n        self.config.stdp_weight = max(0.0, min(1.0, weight))\n        if self.stdp_learner:\n            self.stdp_learner.config.stdp_weight = self.config.stdp_weight\n        if self.logger:\n            self.logger.info(f\"STDP weight set to {self.config.stdp_weight:.2f}\")\n\n    def enable(self):\n        \"\"\"Called by PluginManager when the plugin is enabled.\"\"\"\n        self.set_enabled(True)\n        return True\n\n    def disable(self):\n        \"\"\"Called by PluginManager when the plugin is disabled.\"\"\"\n        self.set_enabled(False)\n        return True\n\n    def set_enabled(self, enabled: bool):\n        self.enabled = enabled\n        status = \"enabled\" if enabled else \"disabled (pure Hebbian)\"\n        if self.logger:\n            self.logger.info(f\"STDP {status}\")\n        print(f\"⚡ STDP learning {status}\")\n        if enabled:\n            if self._ui_banner is None:\n                QtCore.QTimer.singleShot(0, self._inject_ui_banner)\n            elif self._ui_banner is not None:\n                self._ui_banner.setVisible(True)\n        else:\n            if self._ui_banner is not None:\n                self._ui_banner.setVisible(False)\n\n    def reset_stats(self):\n        if self.stdp_learner:\n            self.stdp_learner.reset_stats()\n\n    # -----------------------------------------------------------------------\n    # Plugin menu integration\n    # -----------------------------------------------------------------------\n\n    def register_menu_actions(self, main_window: QtWidgets.QMainWindow,\n                              menu: QtWidgets.QMenu):\n        \"\"\"Called by the UI to populate the Plugins > STDP submenu.\"\"\"\n        panel_action = QtWidgets.QAction(\"Control Panel…\", main_window)\n        panel_action.triggered.connect(\n            lambda: self.show_control_panel(main_window)\n        )\n        menu.addAction(panel_action)\n\n        menu.addSeparator()\n\n        toggle_action = QtWidgets.QAction(\"Enabled\", main_window)\n        toggle_action.setCheckable(True)\n        toggle_action.setChecked(self.enabled)\n        toggle_action.toggled.connect(self.set_enabled)\n        menu.addAction(toggle_action)\n\n    def show_control_panel(self, parent=None):\n        \"\"\"Open (or raise) the STDP control panel.\"\"\"\n        if self._panel is None or not self._panel.isVisible():\n            try:\n                from .stdp_control_panel import STDPControlPanel\n            except ImportError:\n                from stdp_control_panel import STDPControlPanel\n            self._panel = STDPControlPanel(self, parent)\n            self._panel.show()\n        else:\n            self._panel.raise_()\n            self._panel.activateWindow()\n\n\n# ===========================================================================\n# Plugin registration (called by PluginManager.load_all_plugins)\n# ===========================================================================\n\ndef initialize(plugin_manager) -> bool:\n    \"\"\"\n    Register the STDP plugin with the PluginManager.\n    The PluginManager will later call instance.setup(pm, tamagotchi_logic).\n    \"\"\"\n    plugin_key = PLUGIN_NAME.lower()\n\n    if plugin_key in plugin_manager.plugins:\n        if hasattr(plugin_manager, 'logger'):\n            plugin_manager.logger.warning(\n                f\"{PLUGIN_NAME} is already registered. Skipping.\"\n            )\n        return True\n\n    try:\n        instance = STDPPlugin()\n        instance.plugin_manager = plugin_manager\n\n        plugin_manager.plugins[plugin_key] = {\n            'instance':            instance,\n            'name':                PLUGIN_NAME,\n            'version':             PLUGIN_VERSION,\n            'author':              PLUGIN_AUTHOR,\n            'description':         PLUGIN_DESCRIPTION,\n            'requires':            PLUGIN_REQUIRES,\n            'is_setup':            False,\n            'is_enabled_by_default': False,\n        }\n\n        print(f\"⚡ {PLUGIN_NAME} v{PLUGIN_VERSION} by {PLUGIN_AUTHOR} registered.\")\n        return True\n\n    except Exception as exc:\n        if hasattr(plugin_manager, 'logger'):\n            plugin_manager.logger.error(\n                f\"Failed to initialize {PLUGIN_NAME}: {exc}\"\n            )\n        traceback.print_exc()\n        return False\n"
  },
  {
    "path": "plugins/stdp/stdp_control_panel.py",
    "content": "\"\"\"\nSTDP Control Panel\n\nA dockable dialog for tuning STDP parameters and optionally recording\nspike and directional-encoding logs to disk.\n\nLog files\n---------\nspikes_YYYYMMDD_HHMMSS.csv   – one row per detected spike\n    timestamp, neuron, activation, was_rising, is_burst\n\nencoding_YYYYMMDD_HHMMSS.csv – one row per Hebbian+STDP pair update\n    timestamp, pre, post, hebbian_delta, stdp_delta, combined_delta,\n    direction, ltp_ltd, stdp_weight\n\"\"\"\n\nimport os\nimport csv\nimport time\nfrom datetime import datetime\nfrom typing import Optional, TYPE_CHECKING\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\nif TYPE_CHECKING:\n    from .main import STDPPlugin\n\n\n# ── palette ────────────────────────────────────────────────────────────────\n_BG       = \"#0f1923\"\n_SURFACE  = \"#16232f\"\n_BORDER   = \"#1e3448\"\n_ACCENT   = \"#00bcd4\"\n_ACCENT2  = \"#7c4dff\"\n_TEXT     = \"#cfd8dc\"\n_TEXT_DIM = \"#546e7a\"\n_LTP      = \"#00e676\"\n_LTD      = \"#ff5252\"\n_WARN     = \"#ffc107\"\n\n\n_PANEL_STYLE = f\"\"\"\nQDialog {{\n    background: {_BG};\n    color: {_TEXT};\n    font-family: \"Consolas\", \"Courier New\", monospace;\n}}\nQGroupBox {{\n    background: {_SURFACE};\n    border: 1px solid {_BORDER};\n    border-radius: 6px;\n    margin-top: 10px;\n    padding: 8px;\n    color: {_ACCENT};\n    font-size: 11px;\n    font-weight: bold;\n    letter-spacing: 1px;\n    text-transform: uppercase;\n}}\nQGroupBox::title {{\n    subcontrol-origin: margin;\n    left: 10px;\n    padding: 0 6px;\n}}\nQLabel {{\n    color: {_TEXT};\n    font-size: 12px;\n    background: transparent;\n}}\nQLabel#dim {{\n    color: {_TEXT_DIM};\n    font-size: 11px;\n}}\nQLabel#ltp {{\n    color: {_LTP};\n    font-weight: bold;\n}}\nQLabel#ltd {{\n    color: {_LTD};\n    font-weight: bold;\n}}\nQLabel#accent {{\n    color: {_ACCENT};\n    font-weight: bold;\n}}\nQSlider::groove:horizontal {{\n    height: 4px;\n    background: {_BORDER};\n    border-radius: 2px;\n}}\nQSlider::handle:horizontal {{\n    width: 14px;\n    height: 14px;\n    margin: -5px 0;\n    border-radius: 7px;\n    background: {_ACCENT};\n}}\nQSlider::sub-page:horizontal {{\n    background: {_ACCENT};\n    border-radius: 2px;\n}}\nQCheckBox {{\n    color: {_TEXT};\n    spacing: 8px;\n    font-size: 12px;\n}}\nQCheckBox::indicator {{\n    width: 16px;\n    height: 16px;\n    border-radius: 3px;\n    border: 1px solid {_BORDER};\n    background: {_SURFACE};\n}}\nQCheckBox::indicator:checked {{\n    background: {_ACCENT};\n    border-color: {_ACCENT};\n    image: none;\n}}\nQPushButton {{\n    background: {_SURFACE};\n    color: {_TEXT};\n    border: 1px solid {_BORDER};\n    border-radius: 5px;\n    padding: 6px 14px;\n    font-size: 12px;\n    font-family: \"Consolas\", \"Courier New\", monospace;\n}}\nQPushButton:hover {{\n    background: {_BORDER};\n    border-color: {_ACCENT};\n    color: {_ACCENT};\n}}\nQPushButton:pressed {{\n    background: {_ACCENT};\n    color: {_BG};\n}}\nQPushButton#danger {{\n    border-color: {_LTD};\n    color: {_LTD};\n}}\nQPushButton#danger:hover {{\n    background: {_LTD};\n    color: {_BG};\n}}\nQPushButton#export {{\n    border-color: {_LTP};\n    color: {_LTP};\n}}\nQPushButton#export:hover {{\n    background: {_LTP};\n    color: {_BG};\n}}\nQLineEdit {{\n    background: {_SURFACE};\n    color: {_TEXT};\n    border: 1px solid {_BORDER};\n    border-radius: 4px;\n    padding: 4px 8px;\n    font-size: 11px;\n    font-family: \"Consolas\", \"Courier New\", monospace;\n}}\nQScrollBar:vertical {{\n    background: {_BG};\n    width: 8px;\n}}\nQScrollBar::handle:vertical {{\n    background: {_BORDER};\n    border-radius: 4px;\n    min-height: 20px;\n}}\nQFrame#separator {{\n    color: {_BORDER};\n}}\n\"\"\"\n\n\nclass _Slider(QtWidgets.QWidget):\n    \"\"\"Label + slider + value label in one row.\"\"\"\n    valueChanged = QtCore.pyqtSignal(float)\n\n    def __init__(self, label: str, min_val: float, max_val: float,\n                 value: float, decimals: int = 2, parent=None):\n        super().__init__(parent)\n        self._scale = 10 ** decimals\n        self._decimals = decimals\n\n        row = QtWidgets.QHBoxLayout(self)\n        row.setContentsMargins(0, 0, 0, 0)\n        row.setSpacing(8)\n\n        lbl = QtWidgets.QLabel(label)\n        lbl.setFixedWidth(130)\n        row.addWidget(lbl)\n\n        self._slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)\n        self._slider.setRange(int(min_val * self._scale),\n                              int(max_val * self._scale))\n        self._slider.setValue(int(value * self._scale))\n        row.addWidget(self._slider, 1)\n\n        self._val_lbl = QtWidgets.QLabel(f\"{value:.{decimals}f}\")\n        self._val_lbl.setFixedWidth(46)\n        self._val_lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)\n        self._val_lbl.setObjectName(\"accent\")\n        row.addWidget(self._val_lbl)\n\n        self._slider.valueChanged.connect(self._on_change)\n\n    def _on_change(self, raw: int):\n        v = raw / self._scale\n        self._val_lbl.setText(f\"{v:.{self._decimals}f}\")\n        self.valueChanged.emit(v)\n\n    def value(self) -> float:\n        return self._slider.value() / self._scale\n\n    def setValue(self, v: float):\n        self._slider.setValue(int(v * self._scale))\n\n\nclass STDPControlPanel(QtWidgets.QDialog):\n    \"\"\"\n    Floating control panel for the STDP plugin.\n    \"\"\"\n\n    def __init__(self, plugin: \"STDPPlugin\", parent=None):\n        super().__init__(parent)\n        self.plugin = plugin\n        self.setWindowTitle(\"STDP Control Panel\")\n        self.setWindowFlags(\n            QtCore.Qt.Window |\n            QtCore.Qt.WindowCloseButtonHint |\n            QtCore.Qt.WindowMinimizeButtonHint\n        )\n        self.setMinimumWidth(520)\n        self.setStyleSheet(_PANEL_STYLE)\n\n        # Logging state\n        self._logging_active    = False\n        self._log_dir           = self._default_log_dir()\n        self._spike_writer      = None\n        self._spike_file        = None\n        self._encoding_writer   = None\n        self._encoding_file     = None\n        self._spike_count       = 0\n        self._encoding_count    = 0\n\n        # Connect to plugin's logging hooks\n        self.plugin._panel = self\n\n        self._build_ui()\n\n        # Refresh stats every 2 s\n        self._refresh_timer = QtCore.QTimer(self)\n        self._refresh_timer.timeout.connect(self._refresh_stats)\n        self._refresh_timer.start(2000)\n\n    # ── UI construction ────────────────────────────────────────────────────\n\n    def _build_ui(self):\n        root = QtWidgets.QVBoxLayout(self)\n        root.setSpacing(10)\n        root.setContentsMargins(14, 14, 14, 14)\n\n        # Title bar\n        title = QtWidgets.QLabel(\"⚡  STDP CONTROL PANEL\")\n        title.setStyleSheet(f\"color:{_ACCENT}; font-size:14px; font-weight:bold;\"\n                            f\" letter-spacing:2px;\")\n        root.addWidget(title)\n\n        sep = QtWidgets.QFrame()\n        sep.setFrameShape(QtWidgets.QFrame.HLine)\n        sep.setObjectName(\"separator\")\n        root.addWidget(sep)\n\n        # ── Parameters ──────────────────────────────────────────────────\n        params_grp = QtWidgets.QGroupBox(\"Parameters\")\n        params_lay = QtWidgets.QVBoxLayout(params_grp)\n        params_lay.setSpacing(8)\n\n        cfg = self.plugin.config\n\n        self._w_blend = _Slider(\"STDP blend\",   0.0, 1.0, cfg.stdp_weight,    2)\n        self._w_taup  = _Slider(\"τ+ (LTP)\",     0.02, 1.0, cfg.tau_plus,      2)\n        self._w_taum  = _Slider(\"τ− (LTD)\",     0.02, 1.0, cfg.tau_minus,     2)\n        self._w_aplus = _Slider(\"A+ amplitude\", 0.01, 0.3, cfg.A_plus,        3)\n        self._w_aminus= _Slider(\"A− amplitude\", 0.01, 0.3, cfg.A_minus,       3)\n        self._w_win   = _Slider(\"Time window\",  0.1,  2.0, cfg.time_window,   2)\n        self._w_thr   = _Slider(\"Spike thresh\", 20.0, 90.0, cfg.spike_threshold, 1)\n\n        for w in (self._w_blend, self._w_taup, self._w_taum,\n                  self._w_aplus, self._w_aminus, self._w_win, self._w_thr):\n            params_lay.addWidget(w)\n\n        self._w_blend .valueChanged.connect(lambda v: self.plugin.set_stdp_weight(v))\n        self._w_taup  .valueChanged.connect(lambda v: self._set_cfg('tau_plus', v))\n        self._w_taum  .valueChanged.connect(lambda v: self._set_cfg('tau_minus', v))\n        self._w_aplus .valueChanged.connect(lambda v: self._set_cfg('A_plus', v))\n        self._w_aminus.valueChanged.connect(lambda v: self._set_cfg('A_minus', v))\n        self._w_win   .valueChanged.connect(lambda v: self._set_cfg('time_window', v))\n        self._w_thr   .valueChanged.connect(lambda v: self._set_cfg('spike_threshold', v))\n\n        root.addWidget(params_grp)\n\n        # ── Live stats ───────────────────────────────────────────────────\n        stats_grp = QtWidgets.QGroupBox(\"Live Statistics\")\n        stats_grid = QtWidgets.QGridLayout(stats_grp)\n        stats_grid.setSpacing(6)\n\n        def _stat_row(grid, row, name):\n            lbl = QtWidgets.QLabel(name)\n            lbl.setObjectName(\"dim\")\n            val = QtWidgets.QLabel(\"—\")\n            grid.addWidget(lbl, row, 0)\n            grid.addWidget(val, row, 1, QtCore.Qt.AlignRight)\n            return val\n\n        self._s_ltp      = _stat_row(stats_grid, 0, \"LTP events\")\n        self._s_ltp.setObjectName(\"ltp\")\n        self._s_ltd      = _stat_row(stats_grid, 1, \"LTD events\")\n        self._s_ltd.setObjectName(\"ltd\")\n        self._s_spikes   = _stat_row(stats_grid, 2, \"Total spikes\")\n        self._s_neurons  = _stat_row(stats_grid, 3, \"Tracked neurons\")\n        self._s_bursting = _stat_row(stats_grid, 4, \"Bursting now\")\n        self._s_traces   = _stat_row(stats_grid, 5, \"Eligibility traces\")\n\n        reset_btn = QtWidgets.QPushButton(\"Reset counters\")\n        reset_btn.setObjectName(\"danger\")\n        reset_btn.clicked.connect(self._reset_stats)\n        stats_grid.addWidget(reset_btn, 6, 0, 1, 2)\n\n        root.addWidget(stats_grp)\n\n        # ── Logging ──────────────────────────────────────────────────────\n        log_grp = QtWidgets.QGroupBox(\"Data Export\")\n        log_lay = QtWidgets.QVBoxLayout(log_grp)\n        log_lay.setSpacing(8)\n\n        # What to log\n        self._chk_spikes   = QtWidgets.QCheckBox(\"Log spike events  (spikes_*.csv)\")\n        self._chk_encoding = QtWidgets.QCheckBox(\"Log directional encoding  (encoding_*.csv)\")\n        self._chk_spikes.setChecked(True)\n        self._chk_encoding.setChecked(True)\n        log_lay.addWidget(self._chk_spikes)\n        log_lay.addWidget(self._chk_encoding)\n\n        # Output directory\n        dir_row = QtWidgets.QHBoxLayout()\n        dir_row.setSpacing(6)\n        dir_lbl = QtWidgets.QLabel(\"Output dir:\")\n        dir_lbl.setObjectName(\"dim\")\n        self._dir_edit = QtWidgets.QLineEdit(self._log_dir)\n        browse_btn = QtWidgets.QPushButton(\"…\")\n        browse_btn.setFixedWidth(32)\n        browse_btn.clicked.connect(self._browse_dir)\n        dir_row.addWidget(dir_lbl)\n        dir_row.addWidget(self._dir_edit, 1)\n        dir_row.addWidget(browse_btn)\n        log_lay.addLayout(dir_row)\n\n        # Start / stop\n        btn_row = QtWidgets.QHBoxLayout()\n        self._start_btn = QtWidgets.QPushButton(\"▶  Start logging\")\n        self._start_btn.setObjectName(\"export\")\n        self._start_btn.clicked.connect(self._toggle_logging)\n        self._export_now_btn = QtWidgets.QPushButton(\"⬇  Export snapshot now\")\n        self._export_now_btn.clicked.connect(self._export_snapshot)\n        btn_row.addWidget(self._start_btn)\n        btn_row.addWidget(self._export_now_btn)\n        log_lay.addLayout(btn_row)\n\n        # Status line\n        self._log_status = QtWidgets.QLabel(\"Logging inactive\")\n        self._log_status.setObjectName(\"dim\")\n        self._log_status.setAlignment(QtCore.Qt.AlignCenter)\n        log_lay.addWidget(self._log_status)\n\n        root.addWidget(log_grp)\n\n        # ── Close ────────────────────────────────────────────────────────\n        close_btn = QtWidgets.QPushButton(\"Close\")\n        close_btn.clicked.connect(self.close)\n        root.addWidget(close_btn, alignment=QtCore.Qt.AlignRight)\n\n        self._refresh_stats()\n\n    # ── Config helpers ─────────────────────────────────────────────────────\n\n    def _set_cfg(self, attr: str, value: float):\n        setattr(self.plugin.config, attr, value)\n        if self.plugin.stdp_learner:\n            setattr(self.plugin.stdp_learner.config, attr, value)\n\n    def _reset_stats(self):\n        self.plugin.reset_stats()\n        self._spike_count    = 0\n        self._encoding_count = 0\n        self._refresh_stats()\n\n    # ── Stats refresh ──────────────────────────────────────────────────────\n\n    def _refresh_stats(self):\n        stats = self.plugin.get_stats()\n        ss    = stats.get('spike_stats', {})\n        self._s_ltp     .setText(str(stats.get('ltp_events', 0)))\n        self._s_ltd     .setText(str(stats.get('ltd_events', 0)))\n        self._s_spikes  .setText(str(ss.get('total_spikes', 0)))\n        self._s_neurons .setText(str(ss.get('tracked_neurons', 0)))\n        self._s_bursting.setText(str(ss.get('bursting_neurons', 0)))\n        self._s_traces  .setText(str(stats.get('active_eligibility_traces', 0)))\n\n        if self._logging_active:\n            self._log_status.setText(\n                f\"● Recording  —  \"\n                f\"spikes: {self._spike_count}  |  \"\n                f\"pairs: {self._encoding_count}\"\n            )\n            self._log_status.setStyleSheet(f\"color:{_LTP}; font-size:11px;\")\n\n    # ── Directory browse ───────────────────────────────────────────────────\n\n    def _browse_dir(self):\n        d = QtWidgets.QFileDialog.getExistingDirectory(\n            self, \"Select output directory\", self._dir_edit.text()\n        )\n        if d:\n            self._dir_edit.setText(d)\n\n    # ── Continuous logging toggle ──────────────────────────────────────────\n\n    def _toggle_logging(self):\n        if not self._logging_active:\n            self._start_logging()\n        else:\n            self._stop_logging()\n\n    def _start_logging(self):\n        self._log_dir = self._dir_edit.text().strip() or self._default_log_dir()\n        os.makedirs(self._log_dir, exist_ok=True)\n        ts = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n\n        if self._chk_spikes.isChecked():\n            path = os.path.join(self._log_dir, f\"spikes_{ts}.csv\")\n            self._spike_file = open(path, 'w', newline='', encoding='utf-8')\n            self._spike_writer = csv.writer(self._spike_file)\n            self._spike_writer.writerow(\n                [\"timestamp\", \"neuron\", \"activation\", \"was_rising\", \"is_burst\"]\n            )\n\n        if self._chk_encoding.isChecked():\n            path = os.path.join(self._log_dir, f\"encoding_{ts}.csv\")\n            self._encoding_file = open(path, 'w', newline='', encoding='utf-8')\n            self._encoding_writer = csv.writer(self._encoding_file)\n            self._encoding_writer.writerow([\n                \"timestamp\", \"pre\", \"post\",\n                \"hebbian_delta\", \"stdp_delta\", \"combined_delta\",\n                \"direction\", \"ltp_ltd\", \"stdp_weight\"\n            ])\n\n        self._logging_active = True\n        self._spike_count    = 0\n        self._encoding_count = 0\n        self._start_btn.setText(\"■  Stop logging\")\n        self._start_btn.setObjectName(\"danger\")\n        self._start_btn.setStyleSheet(\n            f\"border-color:{_LTD}; color:{_LTD};\"\n            f\"background:{_SURFACE}; border-radius:5px; padding:6px 14px;\"\n        )\n        self._log_status.setText(\"● Recording …\")\n        self._log_status.setStyleSheet(f\"color:{_LTP}; font-size:11px;\")\n        print(f\"⚡ STDP logging started → {self._log_dir}\")\n\n    def _stop_logging(self):\n        self._logging_active = False\n        for f in (self._spike_file, self._encoding_file):\n            if f:\n                try:\n                    f.close()\n                except Exception:\n                    pass\n        self._spike_file      = None\n        self._encoding_file   = None\n        self._spike_writer    = None\n        self._encoding_writer = None\n        self._start_btn.setText(\"▶  Start logging\")\n        self._start_btn.setObjectName(\"export\")\n        self._start_btn.setStyleSheet(\"\")   # revert to stylesheet default\n        self._log_status.setText(\n            f\"Stopped.  Wrote {self._spike_count} spike rows, \"\n            f\"{self._encoding_count} encoding rows.\"\n        )\n        self._log_status.setStyleSheet(f\"color:{_TEXT_DIM}; font-size:11px;\")\n        print(f\"⚡ STDP logging stopped — \"\n              f\"{self._spike_count} spikes, {self._encoding_count} pairs written.\")\n\n    # ── Snapshot export ────────────────────────────────────────────────────\n\n    def _export_snapshot(self):\n        \"\"\"Export a one-shot CSV of current spike history and recent pairs.\"\"\"\n        log_dir = self._dir_edit.text().strip() or self._default_log_dir()\n        os.makedirs(log_dir, exist_ok=True)\n        ts = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n\n        learner = self.plugin.stdp_learner\n        if learner is None:\n            QtWidgets.QMessageBox.warning(self, \"STDP\", \"Plugin not active.\")\n            return\n\n        written = []\n\n        # Spikes snapshot\n        spike_path = os.path.join(log_dir, f\"snapshot_spikes_{ts}.csv\")\n        with open(spike_path, 'w', newline='', encoding='utf-8') as f:\n            w = csv.writer(f)\n            w.writerow([\"neuron\", \"timestamp\", \"activation\", \"was_rising\"])\n            tracker = learner.spike_tracker\n            with __import__('PyQt5.QtCore', fromlist=['QMutexLocker']).QMutexLocker(tracker._mutex):\n                for name, history in tracker._spike_history.items():\n                    for spike in history:\n                        w.writerow([\n                            name,\n                            f\"{spike.timestamp:.4f}\",\n                            f\"{spike.activation_level:.2f}\",\n                            spike.was_rising,\n                        ])\n        written.append(spike_path)\n\n        # Eligibility traces snapshot (best proxy for recent directional encoding)\n        enc_path = os.path.join(log_dir, f\"snapshot_encoding_{ts}.csv\")\n        with open(enc_path, 'w', newline='', encoding='utf-8') as f:\n            w = csv.writer(f)\n            w.writerow([\"pre\", \"post\", \"eligibility_trace\", \"last_update\"])\n            with __import__('PyQt5.QtCore', fromlist=['QMutexLocker']).QMutexLocker(learner._mutex):\n                for (pre, post), (trace, t) in learner._eligibility_traces.items():\n                    w.writerow([pre, post, f\"{trace:.4f}\", f\"{t:.4f}\"])\n        written.append(enc_path)\n\n        msg = \"Exported:\\n\" + \"\\n\".join(written)\n        QtWidgets.QMessageBox.information(self, \"STDP snapshot saved\", msg)\n        print(f\"⚡ STDP snapshot exported → {log_dir}\")\n\n    # ── Called by plugin to record events ─────────────────────────────────\n\n    def record_spike(self, neuron: str, activation: float,\n                     was_rising: bool, is_burst: bool):\n        \"\"\"Called from the spike sampling timer in the plugin.\"\"\"\n        if not self._logging_active or self._spike_writer is None:\n            return\n        self._spike_writer.writerow([\n            f\"{time.time():.4f}\", neuron, f\"{activation:.2f}\",\n            was_rising, is_burst\n        ])\n        self._spike_count += 1\n        # Flush periodically so the file is readable while recording\n        if self._spike_count % 50 == 0:\n            self._spike_file.flush()\n\n    def record_encoding(self, pre: str, post: str,\n                        hebbian_delta: float, stdp_delta: float,\n                        combined_delta: float, direction: str,\n                        stdp_weight: float):\n        \"\"\"Called from _run_stdp_hebbian after each pair update.\"\"\"\n        if not self._logging_active or self._encoding_writer is None:\n            return\n        ltp_ltd = (\"LTP\" if stdp_delta > 0 else\n                   \"LTD\" if stdp_delta < 0 else \"neutral\")\n        self._encoding_writer.writerow([\n            f\"{time.time():.4f}\", pre, post,\n            f\"{hebbian_delta:.5f}\", f\"{stdp_delta:.5f}\", f\"{combined_delta:.5f}\",\n            direction, ltp_ltd, f\"{stdp_weight:.2f}\"\n        ])\n        self._encoding_count += 1\n        if self._encoding_count % 20 == 0:\n            self._encoding_file.flush()\n\n    # ── Helpers ────────────────────────────────────────────────────────────\n\n    @staticmethod\n    def _default_log_dir() -> str:\n        return os.path.join(os.getcwd(), \"logs\", \"stdp\")\n\n    def closeEvent(self, event):\n        if self._logging_active:\n            self._stop_logging()\n        self._refresh_timer.stop()\n        # Detach panel reference from plugin\n        if hasattr(self.plugin, '_panel') and self.plugin._panel is self:\n            self.plugin._panel = None\n        super().closeEvent(event)\n"
  },
  {
    "path": "plugins/stdp/stdp_core.py",
    "content": "\"\"\"\nSTDP (Spike-Timing-Dependent Plasticity) Module for Dosidicus-2\n\nThis module implements biologically-inspired STDP learning rules that complement\nthe existing Hebbian learning system. STDP adds temporal causality to learning:\nconnections strengthen when the pre-synaptic neuron fires BEFORE the post-synaptic\nneuron (causal relationship), and weaken when the order is reversed.\n\nKey Features:\n- Asymmetric learning window with configurable time constants\n- Thread-safe spike recording for use with BrainWorker\n- Burst detection for handling rapid activations\n- Integration mode for combining with rate-based Hebbian learning\n- Eligibility traces for handling delayed rewards\n\nBiological Basis:\n- Pre → Post (Δt > 0): Long-Term Potentiation (LTP) - strengthening\n- Post → Pre (Δt < 0): Long-Term Depression (LTD) - weakening\n- Learning magnitude decreases exponentially with time difference\n\nAdapted for Dosidicus-2's timescales:\n- Real neurons: ~10-50ms learning windows\n- Dosidicus-2: ~100-500ms learning windows (game runs slower than biology)\n\"\"\"\n\nimport time\nimport math\nfrom collections import deque\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Tuple, Optional, Set\nfrom PyQt5.QtCore import QMutex, QMutexLocker\n\n\n@dataclass\nclass SpikeEvent:\n    \"\"\"Records a single spike event for a neuron.\"\"\"\n    timestamp: float\n    activation_level: float  # How strongly it fired (0-100)\n    was_rising: bool = True  # True if activation was increasing when threshold crossed\n    \n    \n@dataclass \nclass STDPConfig:\n    \"\"\"Configuration for STDP learning parameters.\"\"\"\n    \n    # Time constants (in seconds) - controls how fast learning decays with time difference\n    tau_plus: float = 0.15       # LTP time constant (pre before post)\n    tau_minus: float = 0.15      # LTD time constant (post before pre)\n    \n    # Learning amplitudes\n    A_plus: float = 0.08         # Maximum LTP amplitude\n    A_minus: float = 0.05        # Maximum LTD amplitude (often smaller for stability)\n    \n    # Timing window\n    time_window: float = 0.5     # Maximum time difference to consider (seconds)\n    \n    # Spike detection\n    spike_threshold: float = 60.0       # Activation level to consider a \"spike\"\n    spike_rising_threshold: float = 8.0  # Minimum increase to detect rising edge\n    refractory_period: float = 0.08     # Minimum time between spikes (seconds)\n    \n    # Burst handling\n    burst_window: float = 0.3           # Time window to detect bursts\n    burst_threshold: int = 3            # Number of spikes to consider a burst\n    burst_bonus: float = 1.5            # Multiplier for burst-associated learning\n    \n    # Integration with Hebbian\n    stdp_weight: float = 0.4            # How much STDP contributes vs rate-based (0-1)\n    \n    # Eligibility traces (for delayed reward learning)\n    eligibility_decay: float = 0.95     # How fast eligibility traces decay\n    eligibility_window: float = 2.0     # How long eligibility persists (seconds)\n    \n    # Connection-specific learning rate modulation\n    new_connection_boost: float = 2.0   # Boost for newly formed connections\n    custom_neuron_boost: float = 1.3    # Boost for custom (user-created) neurons\n\n\nclass SpikeTracker:\n    \"\"\"\n    Thread-safe tracker for neuron activation spikes.\n    \n    Records when neurons cross activation thresholds, enabling STDP\n    to compute timing-dependent weight changes.\n    \"\"\"\n    \n    def __init__(self, config: Optional[STDPConfig] = None):\n        self.config = config or STDPConfig()\n        self._mutex = QMutex()\n        \n        # Spike history: neuron_name -> deque of SpikeEvent\n        self._spike_history: Dict[str, deque] = {}\n        self._max_history_per_neuron = 20\n        \n        # Previous activation values for edge detection\n        self._previous_activations: Dict[str, float] = {}\n        \n        # Burst tracking\n        self._burst_counts: Dict[str, int] = {}  # Recent spike counts\n        self._last_burst_check: float = 0.0\n        \n    def record_activation(self, neuron_name: str, activation: float, \n                          timestamp: Optional[float] = None) -> Optional[SpikeEvent]:\n        \"\"\"\n        Record a neuron's activation level and detect spikes.\n        \n        A spike is detected when:\n        1. Activation crosses above the threshold\n        2. Activation was rising (not just staying high)\n        3. Sufficient time has passed since last spike (refractory period)\n        \n        Args:\n            neuron_name: Name of the neuron\n            activation: Current activation level (0-100)\n            timestamp: Optional timestamp (uses current time if not provided)\n            \n        Returns:\n            SpikeEvent if a spike was detected, None otherwise\n        \"\"\"\n        if timestamp is None:\n            timestamp = time.time()\n            \n        with QMutexLocker(self._mutex):\n            # Get previous activation\n            prev_activation = self._previous_activations.get(neuron_name, 50.0)\n            self._previous_activations[neuron_name] = activation\n            \n            # Check if this is a spike (threshold crossing with rising edge)\n            is_above_threshold = activation >= self.config.spike_threshold\n            was_below_threshold = prev_activation < self.config.spike_threshold\n            is_rising = (activation - prev_activation) >= self.config.spike_rising_threshold\n            \n            # Alternative: strong activation even if not crossing threshold\n            is_strong_activation = activation >= 80 and is_rising\n            \n            if not ((is_above_threshold and (was_below_threshold or is_rising)) or is_strong_activation):\n                return None\n            \n            # Check refractory period\n            if neuron_name in self._spike_history and len(self._spike_history[neuron_name]) > 0:\n                last_spike = self._spike_history[neuron_name][-1]\n                if (timestamp - last_spike.timestamp) < self.config.refractory_period:\n                    return None\n            \n            # Create and record spike event\n            spike = SpikeEvent(\n                timestamp=timestamp,\n                activation_level=activation,\n                was_rising=is_rising\n            )\n            \n            # Initialize history deque if needed\n            if neuron_name not in self._spike_history:\n                self._spike_history[neuron_name] = deque(maxlen=self._max_history_per_neuron)\n            \n            self._spike_history[neuron_name].append(spike)\n            \n            # Update burst count\n            self._burst_counts[neuron_name] = self._burst_counts.get(neuron_name, 0) + 1\n            \n            return spike\n    \n    def record_batch(self, state: Dict[str, float], timestamp: Optional[float] = None) -> List[Tuple[str, SpikeEvent]]:\n        \"\"\"\n        Record activations for multiple neurons at once.\n        \n        Args:\n            state: Dictionary of neuron_name -> activation_level\n            timestamp: Optional shared timestamp for all recordings\n            \n        Returns:\n            List of (neuron_name, SpikeEvent) tuples for detected spikes\n        \"\"\"\n        if timestamp is None:\n            timestamp = time.time()\n            \n        spikes = []\n        for neuron_name, activation in state.items():\n            if isinstance(activation, (int, float)):\n                spike = self.record_activation(neuron_name, float(activation), timestamp)\n                if spike:\n                    spikes.append((neuron_name, spike))\n        \n        return spikes\n    \n    def get_last_spike_time(self, neuron_name: str) -> Optional[float]:\n        \"\"\"Get the timestamp of the most recent spike for a neuron.\"\"\"\n        with QMutexLocker(self._mutex):\n            if neuron_name in self._spike_history and len(self._spike_history[neuron_name]) > 0:\n                return self._spike_history[neuron_name][-1].timestamp\n            return None\n    \n    def get_recent_spikes(self, neuron_name: str, window: Optional[float] = None) -> List[SpikeEvent]:\n        \"\"\"Get all spikes within a time window for a neuron.\"\"\"\n        if window is None:\n            window = self.config.time_window\n        with QMutexLocker(self._mutex):\n            return self._get_recent_spikes_nolock(neuron_name, window)\n\n    def _get_recent_spikes_nolock(self, neuron_name: str, window: float) -> List[SpikeEvent]:\n        \"\"\"Lock-free version — caller must already hold _mutex.\"\"\"\n        cutoff = time.time() - window\n        if neuron_name not in self._spike_history:\n            return []\n        return [s for s in self._spike_history[neuron_name] if s.timestamp >= cutoff]\n\n    def is_bursting(self, neuron_name: str) -> bool:\n        \"\"\"Check if a neuron is currently in a burst state.\"\"\"\n        with QMutexLocker(self._mutex):\n            return self._is_bursting_nolock(neuron_name)\n\n    def _is_bursting_nolock(self, neuron_name: str) -> bool:\n        \"\"\"Lock-free version — caller must already hold _mutex.\"\"\"\n        recent = self._get_recent_spikes_nolock(neuron_name, self.config.burst_window)\n        return len(recent) >= self.config.burst_threshold\n    \n    def cleanup_old_spikes(self, max_age: Optional[float] = None):\n        \"\"\"Remove spike records older than max_age seconds.\"\"\"\n        if max_age is None:\n            max_age = self.config.time_window * 3\n            \n        current_time = time.time()\n        cutoff = current_time - max_age\n        \n        with QMutexLocker(self._mutex):\n            for neuron_name in list(self._spike_history.keys()):\n                # Filter to keep only recent spikes\n                self._spike_history[neuron_name] = deque(\n                    (s for s in self._spike_history[neuron_name] if s.timestamp >= cutoff),\n                    maxlen=self._max_history_per_neuron\n                )\n                \n                # Remove empty histories\n                if len(self._spike_history[neuron_name]) == 0:\n                    del self._spike_history[neuron_name]\n            \n            # Decay burst counts periodically\n            current_time = time.time()\n            if current_time - self._last_burst_check > self.config.burst_window:\n                self._burst_counts = {k: max(0, v - 1) for k, v in self._burst_counts.items()}\n                self._last_burst_check = current_time\n    \n    def get_spike_stats(self) -> Dict:\n        \"\"\"Get statistics about spike activity.\"\"\"\n        with QMutexLocker(self._mutex):\n            return {\n                'tracked_neurons': len(self._spike_history),\n                'total_spikes': sum(len(h) for h in self._spike_history.values()),\n                'bursting_neurons': sum(\n                    1 for n in self._spike_history.keys()\n                    if self._is_bursting_nolock(n)          # no re-lock\n                ),\n                'recent_spikes_by_neuron': {\n                    k: len(self._get_recent_spikes_nolock(k, self.config.time_window))  # no re-lock\n                    for k in self._spike_history.keys()\n                }\n            }\n    \n    def to_dict(self) -> Dict:\n        \"\"\"Serialize spike tracker state for saving.\"\"\"\n        with QMutexLocker(self._mutex):\n            return {\n                'spike_history': {\n                    name: [\n                        {'timestamp': s.timestamp, 'activation': s.activation_level, 'rising': s.was_rising}\n                        for s in spikes\n                    ]\n                    for name, spikes in self._spike_history.items()\n                },\n                'previous_activations': dict(self._previous_activations),\n                'burst_counts': dict(self._burst_counts)\n            }\n    \n    def from_dict(self, data: Dict):\n        \"\"\"Restore spike tracker state from saved data.\"\"\"\n        with QMutexLocker(self._mutex):\n            self._spike_history.clear()\n            for name, spikes in data.get('spike_history', {}).items():\n                self._spike_history[name] = deque(\n                    (SpikeEvent(s['timestamp'], s['activation'], s.get('rising', True)) for s in spikes),\n                    maxlen=self._max_history_per_neuron\n                )\n            self._previous_activations = dict(data.get('previous_activations', {}))\n            self._burst_counts = dict(data.get('burst_counts', {}))\n\n\nclass STDPLearner:\n    \"\"\"\n    Implements STDP learning rules for weight updates.\n    \n    Computes weight changes based on the relative timing of pre-synaptic\n    and post-synaptic neuron spikes.\n    \"\"\"\n    \n    def __init__(self, config: Optional[STDPConfig] = None):\n        self.config = config or STDPConfig()\n        self.spike_tracker = SpikeTracker(self.config)\n        self._mutex = QMutex()\n        \n        # Eligibility traces: (pre, post) -> (trace_value, last_update_time)\n        self._eligibility_traces: Dict[Tuple[str, str], Tuple[float, float]] = {}\n        \n        # Statistics tracking\n        self._ltp_count = 0  # Long-term potentiation events\n        self._ltd_count = 0  # Long-term depression events\n        self._total_delta = 0.0\n        \n    def record_activation(self, neuron_name: str, activation: float, \n                          timestamp: Optional[float] = None) -> Optional[SpikeEvent]:\n        \"\"\"Record activation and detect spikes. Delegates to spike tracker.\"\"\"\n        return self.spike_tracker.record_activation(neuron_name, activation, timestamp)\n    \n    def record_state(self, state: Dict[str, float], timestamp: Optional[float] = None):\n        \"\"\"Record full brain state for spike detection.\"\"\"\n        return self.spike_tracker.record_batch(state, timestamp)\n    \n    def compute_stdp_delta(self, pre_neuron: str, post_neuron: str,\n                           connection_age: float = 1.0,\n                           is_custom_neuron: bool = False) -> float:\n        \"\"\"\n        Compute the STDP weight change for a directed connection pre → post.\n        \n        The classic STDP rule:\n        - If pre fires before post (Δt > 0): LTP (positive change)\n        - If post fires before pre (Δt < 0): LTD (negative change)\n        - Magnitude decreases exponentially with |Δt|\n        \n        Args:\n            pre_neuron: Name of pre-synaptic neuron\n            post_neuron: Name of post-synaptic neuron\n            connection_age: Age multiplier (newer connections learn faster)\n            is_custom_neuron: Whether either neuron is user-created\n            \n        Returns:\n            Weight change (positive for LTP, negative for LTD, 0 if no timing data)\n        \"\"\"\n        pre_time = self.spike_tracker.get_last_spike_time(pre_neuron)\n        post_time = self.spike_tracker.get_last_spike_time(post_neuron)\n        \n        # Need both neurons to have spiked recently\n        if pre_time is None or post_time is None:\n            return 0.0\n        \n        # Compute time difference: positive means pre fired first\n        dt = post_time - pre_time\n        \n        # Check if within learning window\n        if abs(dt) > self.config.time_window:\n            return 0.0\n        \n        # Compute STDP delta using exponential decay\n        if dt > 0:\n            # Pre before post: LTP (causal, strengthen)\n            delta = self.config.A_plus * math.exp(-dt / self.config.tau_plus)\n            self._ltp_count += 1\n        elif dt < 0:\n            # Post before pre: LTD (acausal, weaken)\n            delta = -self.config.A_minus * math.exp(dt / self.config.tau_minus)\n            self._ltd_count += 1\n        else:\n            # Simultaneous (very rare): small potentiation\n            delta = self.config.A_plus * 0.5\n        \n        # Apply modifiers\n        # 1. New connection boost\n        if connection_age < 1.0:\n            delta *= self.config.new_connection_boost * (1.0 - connection_age * 0.5)\n        \n        # 2. Custom neuron boost\n        if is_custom_neuron:\n            delta *= self.config.custom_neuron_boost\n        \n        # 3. Burst bonus (if either neuron is bursting)\n        if self.spike_tracker.is_bursting(pre_neuron) or self.spike_tracker.is_bursting(post_neuron):\n            delta *= self.config.burst_bonus\n        \n        self._total_delta += abs(delta)\n        return delta\n    \n    def compute_symmetric_stdp(self, neuron1: str, neuron2: str,\n                                connection_age: float = 1.0,\n                                is_custom: bool = False) -> Tuple[float, str]:\n        \"\"\"\n        Compute STDP for an undirected connection (checks both directions).\n        \n        For networks with bidirectional or undirected connections, this computes\n        the stronger of the two possible STDP signals.\n        \n        Args:\n            neuron1, neuron2: The two neurons\n            connection_age: Age multiplier\n            is_custom: Whether either is a custom neuron\n            \n        Returns:\n            Tuple of (delta, direction) where direction is 'n1_to_n2', 'n2_to_n1', or 'none'\n        \"\"\"\n        delta_1_to_2 = self.compute_stdp_delta(neuron1, neuron2, connection_age, is_custom)\n        delta_2_to_1 = self.compute_stdp_delta(neuron2, neuron1, connection_age, is_custom)\n        \n        # Return the stronger signal\n        if abs(delta_1_to_2) >= abs(delta_2_to_1):\n            if delta_1_to_2 != 0:\n                return delta_1_to_2, 'n1_to_n2'\n        else:\n            if delta_2_to_1 != 0:\n                return delta_2_to_1, 'n2_to_n1'\n        \n        return 0.0, 'none'\n    \n    def update_eligibility_trace(self, pre_neuron: str, post_neuron: str, \n                                  stdp_delta: float, current_time: Optional[float] = None):\n        \"\"\"\n        Update eligibility trace for a connection.\n        \n        Eligibility traces allow STDP effects to be modulated by delayed rewards,\n        implementing a form of three-factor learning rule.\n        \n        Args:\n            pre_neuron, post_neuron: Connection endpoints\n            stdp_delta: The STDP delta computed for this pair\n            current_time: Optional timestamp\n        \"\"\"\n        if current_time is None:\n            current_time = time.time()\n            \n        key = (pre_neuron, post_neuron)\n        \n        with QMutexLocker(self._mutex):\n            # Get existing trace, decayed to current time\n            if key in self._eligibility_traces:\n                old_trace, old_time = self._eligibility_traces[key]\n                elapsed = current_time - old_time\n                \n                # Exponential decay\n                decay_factor = self.config.eligibility_decay ** (elapsed / 0.1)\n                decayed_trace = old_trace * decay_factor\n            else:\n                decayed_trace = 0.0\n            \n            # Add new STDP signal to trace\n            new_trace = decayed_trace + stdp_delta\n            \n            # Clamp trace magnitude\n            new_trace = max(-1.0, min(1.0, new_trace))\n            \n            self._eligibility_traces[key] = (new_trace, current_time)\n    \n    def get_eligibility_trace(self, pre_neuron: str, post_neuron: str,\n                               current_time: Optional[float] = None) -> float:\n        \"\"\"Get the current eligibility trace for a connection.\"\"\"\n        if current_time is None:\n            current_time = time.time()\n            \n        key = (pre_neuron, post_neuron)\n        \n        with QMutexLocker(self._mutex):\n            if key not in self._eligibility_traces:\n                return 0.0\n            \n            trace, last_time = self._eligibility_traces[key]\n            elapsed = current_time - last_time\n            \n            # Expired trace\n            if elapsed > self.config.eligibility_window:\n                return 0.0\n            \n            # Apply decay\n            decay_factor = self.config.eligibility_decay ** (elapsed / 0.1)\n            return trace * decay_factor\n    \n    def apply_reward_modulation(self, reward_signal: float) -> Dict[Tuple[str, str], float]:\n        \"\"\"\n        Apply reward modulation to all active eligibility traces.\n        \n        This implements the third factor in three-factor learning rules:\n        connections with positive eligibility traces are strengthened by\n        positive rewards and weakened by negative rewards (and vice versa).\n        \n        Args:\n            reward_signal: Reward value (positive = good outcome, negative = bad)\n            \n        Returns:\n            Dictionary of (pre, post) -> weight_delta for all affected connections\n        \"\"\"\n        current_time = time.time()\n        weight_deltas = {}\n        \n        with QMutexLocker(self._mutex):\n            for (pre, post), (trace, last_time) in list(self._eligibility_traces.items()):\n                # Skip expired traces\n                if current_time - last_time > self.config.eligibility_window:\n                    continue\n                \n                # Apply decay\n                elapsed = current_time - last_time\n                decay_factor = self.config.eligibility_decay ** (elapsed / 0.1)\n                current_trace = trace * decay_factor\n                \n                if abs(current_trace) < 0.01:\n                    continue\n                \n                # Weight delta = eligibility * reward\n                delta = current_trace * reward_signal * 0.1\n                weight_deltas[(pre, post)] = delta\n                \n                # Clear the trace after applying\n                self._eligibility_traces[(pre, post)] = (0.0, current_time)\n        \n        return weight_deltas\n    \n    def compute_combined_learning(self, neuron1: str, neuron2: str,\n                                   v1: float, v2: float,\n                                   base_learning_rate: float = 0.1,\n                                   connection_age: float = 1.0,\n                                   is_custom: bool = False) -> Tuple[float, Dict]:\n        \"\"\"\n        Compute combined Hebbian + STDP learning signal.\n        \n        This integrates rate-based Hebbian learning with timing-based STDP,\n        weighted by config.stdp_weight.\n        \n        Args:\n            neuron1, neuron2: The two neurons in the connection\n            v1, v2: Current activation values (0-100)\n            base_learning_rate: Base learning rate for Hebbian component\n            connection_age: Age of connection (0-1, where 0 is new)\n            is_custom: Whether either neuron is custom\n            \n        Returns:\n            Tuple of (combined_delta, metadata_dict)\n        \"\"\"\n        # 1. Rate-based Hebbian component\n        hebbian_delta = base_learning_rate * (v1 / 100.0) * (v2 / 100.0)\n        \n        # 2. STDP component\n        stdp_delta, direction = self.compute_symmetric_stdp(\n            neuron1, neuron2, connection_age, is_custom\n        )\n        \n        # 3. Combine with weighting\n        stdp_w = self.config.stdp_weight\n        combined_delta = (1 - stdp_w) * hebbian_delta + stdp_w * stdp_delta\n        \n        # 4. Apply connection age boost to combined signal\n        if connection_age < 0.5:\n            combined_delta *= self.config.new_connection_boost\n        \n        metadata = {\n            'hebbian_delta': hebbian_delta,\n            'stdp_delta': stdp_delta,\n            'stdp_direction': direction,\n            'combined_delta': combined_delta,\n            'is_ltp': stdp_delta > 0,\n            'is_ltd': stdp_delta < 0\n        }\n        \n        # Update eligibility trace\n        if stdp_delta != 0:\n            self.update_eligibility_trace(neuron1, neuron2, stdp_delta)\n        \n        return combined_delta, metadata\n    \n    def cleanup(self):\n        \"\"\"Periodic cleanup of old data.\"\"\"\n        self.spike_tracker.cleanup_old_spikes()\n        \n        # Cleanup old eligibility traces\n        current_time = time.time()\n        cutoff = current_time - self.config.eligibility_window * 2\n        \n        with QMutexLocker(self._mutex):\n            self._eligibility_traces = {\n                k: v for k, v in self._eligibility_traces.items()\n                if v[1] > cutoff\n            }\n    \n    def get_stats(self) -> Dict:\n        \"\"\"Get learning statistics.\"\"\"\n        spike_stats = self.spike_tracker.get_spike_stats()\n        \n        with QMutexLocker(self._mutex):\n            return {\n                'ltp_events': self._ltp_count,\n                'ltd_events': self._ltd_count,\n                'total_delta_magnitude': self._total_delta,\n                'active_eligibility_traces': len(self._eligibility_traces),\n                'spike_stats': spike_stats\n            }\n    \n    def reset_stats(self):\n        \"\"\"Reset learning statistics.\"\"\"\n        with QMutexLocker(self._mutex):\n            self._ltp_count = 0\n            self._ltd_count = 0\n            self._total_delta = 0.0\n    \n    def to_dict(self) -> Dict:\n        \"\"\"Serialize STDP learner state.\"\"\"\n        with QMutexLocker(self._mutex):\n            return {\n                'spike_tracker': self.spike_tracker.to_dict(),\n                'eligibility_traces': {\n                    f\"{k[0]}|{k[1]}\": {'trace': v[0], 'time': v[1]}\n                    for k, v in self._eligibility_traces.items()\n                },\n                'stats': {\n                    'ltp_count': self._ltp_count,\n                    'ltd_count': self._ltd_count,\n                    'total_delta': self._total_delta\n                }\n            }\n    \n    def from_dict(self, data: Dict):\n        \"\"\"Restore STDP learner state.\"\"\"\n        if 'spike_tracker' in data:\n            self.spike_tracker.from_dict(data['spike_tracker'])\n        \n        with QMutexLocker(self._mutex):\n            self._eligibility_traces.clear()\n            for key_str, val in data.get('eligibility_traces', {}).items():\n                parts = key_str.split('|')\n                if len(parts) == 2:\n                    self._eligibility_traces[(parts[0], parts[1])] = (val['trace'], val['time'])\n            \n            stats = data.get('stats', {})\n            self._ltp_count = stats.get('ltp_count', 0)\n            self._ltd_count = stats.get('ltd_count', 0)\n            self._total_delta = stats.get('total_delta', 0.0)\n\n\n# Convenience function for creating a configured STDP system\ndef create_stdp_learner(\n    time_window_ms: float = 500,\n    stdp_weight: float = 0.4,\n    spike_threshold: float = 60.0\n) -> STDPLearner:\n    \"\"\"\n    Create an STDP learner with common configuration.\n    \n    Args:\n        time_window_ms: Learning time window in milliseconds\n        stdp_weight: Weight of STDP vs Hebbian (0-1)\n        spike_threshold: Activation level to consider a spike\n        \n    Returns:\n        Configured STDPLearner instance\n    \"\"\"\n    config = STDPConfig(\n        time_window=time_window_ms / 1000.0,\n        tau_plus=time_window_ms / 3000.0,\n        tau_minus=time_window_ms / 3000.0,\n        stdp_weight=stdp_weight,\n        spike_threshold=spike_threshold\n    )\n    return STDPLearner(config)\n"
  },
  {
    "path": "plugins/whitelist.txt",
    "content": "# Only these plugins load at startup.\r\n# Add one plugin name per line. Lines starting with # are ignored.\r\nachievements"
  },
  {
    "path": "requirements.txt",
    "content": "PyQt5>=5.15\nnumpy>=1.21\n"
  },
  {
    "path": "src/__init__.py",
    "content": ""
  },
  {
    "path": "src/animation_styles.py",
    "content": "from dataclasses import dataclass, field\r\nfrom typing import Tuple\r\nfrom enum import Enum\r\n\r\n\r\nclass AnimationStyleName(Enum):\r\n    \"\"\"Available animation style names.\"\"\"\r\n    VIBRANT = \"vibrant\"\r\n    SUBTLE = \"subtle\"\r\n    #NEURAL = \"neural\"\r\n    DESIGNER = \"designer\"\r\n    NONE = \"none\"\r\n\r\n\r\n@dataclass\r\nclass AnimationStyle:\r\n    \"\"\"\r\n    Base animation style configuration with vibrant defaults.\r\n    Contains all visual parameters for connection and neuron animations.\r\n    \r\n    DEFAULTS: Matches the 'Vibrant' style.\r\n    \"\"\"\r\n    # ===== STYLE METADATA =====\r\n    name: str = \"vibrant\"\r\n    display_name: str = \"Thick\"\r\n    description: str = \"Living connections that breathe and pulse organically\"\r\n    \r\n    # ===== CONNECTION LINE APPEARANCE =====\r\n    # Vibrant defaults\r\n    line_base_width: float = 1.3\r\n    line_colour_positive: Tuple[int, int, int] = (40, 200, 80)      # Lush green\r\n    line_colour_negative: Tuple[int, int, int] = (220, 70, 70)      # Warm red\r\n    line_alpha: int = 180                                          # Base alpha (modulated by pulse)\r\n    use_thick_lines: bool = False                                 # False for vibrant (uses glow instead)\r\n    \r\n    # ===== STRESS-ANXIETY CONNECTION (special red dashed line) =====\r\n    stress_anxiety_width: float = 3.5\r\n    stress_anxiety_colour: Tuple[int, int, int] = (255, 50, 50)\r\n    stress_anxiety_dashed: bool = True\r\n    \r\n    # ===== PULSE / TRAVELLING DOT ANIMATION =====\r\n    # Disabled in Vibrant (replaced by ambient pulse)\r\n    pulse_enabled: bool = False                                     \r\n    pulse_colour: Tuple[int, int, int] = (255, 255, 0)             # Yellow dot\r\n    pulse_alpha: int = 200\r\n    pulse_duration: float = 3.0                                     # Tuned for 10 FPS (slower)\r\n    pulse_speed: float = 1.0                                        # 0-1 range for travel\r\n    pulse_diameter: float = 6.0                                     # pixels (before scale)\r\n    \r\n    # ===== GLOW EFFECT (during weight change animations) =====\r\n    glow_enabled: bool = True\r\n    glow_colour: Tuple[int, int, int] = (255, 255, 150)\r\n    glow_alpha: int = 60\r\n    glow_fade_threshold: float = 0.7                                # fade out after this progress\r\n    \r\n    # ===== NEURON HOVER EFFECTS =====\r\n    hover_enabled: bool = True\r\n    hover_scale: float = 1.25                                       # expand on hover\r\n    hover_animation_duration: float = 0.5                           # Slower for 10 FPS smoothness\r\n    \r\n    # ===== NEURON ACTIVITY HIGHLIGHT =====\r\n    activity_highlight_enabled: bool = True\r\n    activity_highlight_colour: Tuple[int, int, int] = (255, 255, 0)\r\n    activity_highlight_alpha: int = 150\r\n    activity_pulse_speed: float = 1.5                              # Slower Hz for 10 FPS\r\n    \r\n    # ===== NEUROGENESIS HIGHLIGHT =====\r\n    neurogenesis_highlight_colour: Tuple[int, int, int] = (255, 215, 0)  # Gold\r\n    neurogenesis_highlight_alpha: int = 200\r\n    neurogenesis_highlight_duration: float = 5.0                    # seconds\r\n    \r\n    # ===== VIBRANT STYLE: AMBIENT PULSING =====\r\n    # When enabled, connections gently \"breathe\" at random individual rates\r\n    ambient_pulse_enabled: bool = True\r\n    ambient_pulse_width_range: Tuple[float, float] = (0.7, 1.5)    # Width oscillates 70%-150%\r\n    ambient_pulse_alpha_range: Tuple[int, int] = (120, 220)        # Alpha oscillates\r\n    ambient_pulse_freq_range: Tuple[float, float] = (0.1, 0.25)    # Very slow breathing for 10 FPS\r\n    ambient_pulse_phase_drift: float = 0.05                         # Subtle phase wandering\r\n    \r\n    # ===== SUBTLE STYLE: COMMUNICATION GLOWS =====\r\n    # Enabled in Vibrant\r\n    comm_glow_enabled: bool = True\r\n    comm_glow_colour: Tuple[int, int, int] = (247, 181, 57)        # Warm gold/orange for Vibrant\r\n    comm_glow_alpha: int = 200\r\n    comm_glow_size: float = 8.0                                     \r\n    comm_glow_tail_length: float = 0.2                             \r\n    comm_glow_speed_range: Tuple[float, float] = (2.0, 3.5)        # 2-3.5s duration (~80px/s)\r\n    comm_glow_fade_in: float = 0.1                                  # 0-1, fade in portion\r\n    comm_glow_fade_out: float = 0.25                                # 0-1, fade out portion\r\n    comm_glow_spawn_on_activity: bool = True                        # spawn when neurons active\r\n    comm_glow_spawn_on_weight_change: bool = True                   # spawn on weight changes\r\n    comm_glow_max_per_connection: int = 2                           # max simultaneous glows per line\r\n    \r\n    # ===== NEURAL STYLE: ACTIVATION PULSES =====\r\n    # Disabled in Vibrant\r\n    neural_pulse_enabled: bool = False\r\n    neural_pulse_duration: float = 2.0                              # Slower: 2.0s duration\r\n    neural_pulse_width: float = 8.0                                 # Thicker pulse for visibility\r\n    neural_pulse_colour_positive: Tuple[int, int, int] = (180, 230, 255)  # Soft cyan glow\r\n    neural_pulse_colour_negative: Tuple[int, int, int] = (255, 180, 150)  # Soft warm glow\r\n    neural_weight_thickness: bool = False                           # scale line width by weight\r\n    neural_weight_thickness_mult: float = 3.5                       # weight multiplier for thickness\r\n    neural_base_colour_positive: Tuple[int, int, int] = (100, 200, 255)   # Cyan for excitatory\r\n    neural_base_colour_negative: Tuple[int, int, int] = (255, 120, 100)   # Red for inhibitory\r\n    neural_base_alpha: int = 180                                    # Base connection alpha\r\n    \r\n    # ===== BACKGROUND COLOUR =====\r\n    background_colour: Tuple[int, int, int] = (248, 248, 245)      # Warmer Vibrant background\r\n\r\n    # ===== WEIGHT-BASED THICKNESS (Used by Subtle) =====\r\n    weight_thickness_enabled: bool = False\r\n    weight_thickness_min: float = 1.0\r\n    weight_thickness_max: float = 9.0\r\n    weight_thickness_power: float = 1.0\r\n    \r\n    # ===== SCROLLING DOTS (Used by Subtle) =====\r\n    scroll_enabled: bool = False\r\n    scroll_dot_count: int = 3\r\n    scroll_dot_size: float = 6.0\r\n    scroll_dot_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    scroll_dot_alpha: int = 200\r\n    scroll_speed_range: Tuple[float, float] = (1.5, 4.0)\r\n    scroll_random_offsets: bool = True\r\n\r\n\r\n@dataclass\r\nclass VibrantStyle(AnimationStyle):\r\n    \"\"\"\r\n    Style 1: Vibrant - Living, breathing connections.\r\n    Each connection pulses at its own organic rhythm, creating an \r\n    \"alive\" feel like a biological neural network. Tuned for 10 FPS.\r\n    \"\"\"\r\n    name: str = \"vibrant\"\r\n    display_name: str = \"Thick\"\r\n    description: str = \"Living connections that breathe and pulse organically\"\r\n    \r\n    # Explicitly matches defaults, listed here for clarity\r\n    line_base_width: float = 1.3\r\n    line_colour_positive: Tuple[int, int, int] = (40, 200, 80)\r\n    line_colour_negative: Tuple[int, int, int] = (220, 70, 70)\r\n    \r\n    # Vibrant features\r\n    ambient_pulse_enabled: bool = True\r\n    comm_glow_enabled: bool = True\r\n    \r\n    # Disable conflicting styles\r\n    pulse_enabled: bool = False\r\n    neural_pulse_enabled: bool = False\r\n    scroll_enabled: bool = False\r\n\r\n\r\n@dataclass\r\nclass SubtleStyle(AnimationStyle):\r\n    \"\"\"\r\n    Style 2: Flow - Scrolling dotted connections with weight-based thickness.\r\n    Features green (positive) and red (negative) dotted lines with \r\n    continuously scrolling white dots. Line thickness scales with connection strength.\r\n    \"\"\"\r\n    name: str = \"subtle\"\r\n    display_name: str = \"Flow\"\r\n    description: str = \"Scrolling dotted lines with weight-based thickness\"\r\n    \r\n    background_colour: Tuple[int, int, int] = (255, 255, 255)     # Pure white\r\n    \r\n    # Dotted lines\r\n    line_base_width: float = 1.0\r\n    line_colour_positive: Tuple[int, int, int] = (0, 200, 0)\r\n    line_colour_negative: Tuple[int, int, int] = (200, 0, 0)\r\n    line_alpha: int = 255\r\n    line_style: int = 1                                            # Qt.DotLine\r\n    \r\n    # Weight-based thickness\r\n    weight_thickness_enabled: bool = True\r\n    \r\n    # Scrolling dots\r\n    scroll_enabled: bool = True\r\n    scroll_dot_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    \r\n    # Disable Pulse/Glow\r\n    pulse_enabled: bool = False\r\n    glow_enabled: bool = False\r\n    ambient_pulse_enabled: bool = False\r\n    comm_glow_enabled: bool = False\r\n    neural_pulse_enabled: bool = False\r\n\r\n\r\n@dataclass\r\nclass NeuralStyle(AnimationStyle):\r\n    \"\"\"\r\n    Style 3: Neural - Synaptic activation visualization.\r\n    Features weight-based line thickness and traveling activation pulses.\r\n    \"\"\"\r\n    name: str = \"neural\"\r\n    display_name: str = \"Pink & Blue\"\r\n    description: str = \"Highlights most active connections\"\r\n    \r\n    line_base_width: float = 1.0\r\n    line_colour_positive: Tuple[int, int, int] = (100, 200, 255)\r\n    line_colour_negative: Tuple[int, int, int] = (255, 120, 100)\r\n    line_alpha: int = 180\r\n    use_thick_lines: bool = False\r\n    \r\n    #background_colour: Tuple[int, int, int] = (245, 247, 250)\r\n    \r\n    # Disable Pulse/Glow\r\n    ambient_pulse_enabled: bool = False\r\n    comm_glow_enabled: bool = False\r\n    \r\n    # Enable Neural Pulse\r\n    neural_pulse_enabled: bool = True\r\n    neural_weight_thickness: bool = True\r\n\r\n\r\n@dataclass\r\nclass DesignerStyle(AnimationStyle):\r\n    \"\"\"\r\n    Style 4: Structural - Heavy schematic view (Monochrome).\r\n    Features extremely thick, weight-scaled lines with no movement.\r\n    Connections are strictly Black (positive) or Grey (negative).\r\n    \"\"\"\r\n    name: str = \"designer\"\r\n    display_name: str = \"Structural\"\r\n    description: str = \"Heavy, static black & white schematic view\"\r\n    \r\n    # ===== HEAVY, SOLID MONOCHROME LINES =====\r\n    line_base_width: float = 4.0\r\n    line_colour_positive: Tuple[int, int, int] = (0, 0, 0)          # Black\r\n    line_colour_negative: Tuple[int, int, int] = (160, 160, 160)    # Grey\r\n    line_alpha: int = 255                                           # Full opacity\r\n    use_thick_lines: bool = True\r\n    \r\n    # Aggressive thickness scaling based on weight\r\n    neural_weight_thickness: bool = True\r\n    neural_weight_thickness_mult: float = 15.0                      # +15px at max weight\r\n    neural_base_colour_positive: Tuple[int, int, int] = (0, 0, 0)   # Black\r\n    neural_base_colour_negative: Tuple[int, int, int] = (160, 160, 160) # Grey\r\n    neural_base_alpha: int = 255\r\n    \r\n    # Stress connection (Solid Black block)\r\n    stress_anxiety_width: float = 6.0\r\n    stress_anxiety_colour: Tuple[int, int, int] = (0, 0, 0)\r\n    stress_anxiety_dashed: bool = False                             \r\n    \r\n    # ===== ZERO ANIMATION =====\r\n    ambient_pulse_enabled: bool = False\r\n    pulse_enabled: bool = False\r\n    comm_glow_enabled: bool = False\r\n    neural_pulse_enabled: bool = False\r\n    glow_enabled: bool = False\r\n    \r\n    # Technical background\r\n    #background_colour: Tuple[int, int, int] = (240, 240, 240)       # Light Grey\r\n    \r\n    # Minimal interactivity\r\n    hover_enabled: bool = True\r\n    hover_scale: float = 1.05\r\n    hover_animation_duration: float = 0.2\r\n    \r\n    # Minimal highlighting (Greyscale)\r\n    activity_highlight_enabled: bool = False\r\n    neurogenesis_highlight_colour: Tuple[int, int, int] = (100, 100, 100) # Dark Grey\r\n    neurogenesis_highlight_alpha: int = 50\r\n\r\n\r\n@dataclass\r\nclass NoneStyle(AnimationStyle):\r\n    '''\r\n    Style 5: None - No animations - maximum performance.\r\n    Static connections with weight-based thickness capped at 5px.\r\n    '''\r\n    name: str = 'none'\r\n    display_name: str = 'Thin'\r\n    description: str = 'No animations - maximum performance'\r\n    \r\n    # Disable ALL animation features\r\n    pulse_enabled: bool = False\r\n    glow_enabled: bool = False\r\n    hover_enabled: bool = False\r\n    activity_highlight_enabled: bool = False\r\n    ambient_pulse_enabled: bool = False\r\n    comm_glow_enabled: bool = False\r\n    neural_pulse_enabled: bool = False\r\n    \r\n    # Basic visual settings\r\n    line_base_width: float = 1.0\r\n    \r\n    # Matches Vibrant/Default colors (Green/Red)\r\n    line_colour_positive: Tuple[int, int, int] = (40, 200, 80)\r\n    line_colour_negative: Tuple[int, int, int] = (220, 70, 70)\r\n    line_alpha: int = 180\r\n    use_thick_lines: bool = False\r\n    \r\n    # ===== ENABLE WEIGHT-BASED THICKNESS (Max 5px) =====\r\n    weight_thickness_enabled: bool = True\r\n    weight_thickness_min: float = 1.0\r\n    weight_thickness_max: float = 5.0  # Caps total thickness strictly at 5px\r\n    weight_thickness_power: float = 1.0\r\n    \r\n    stress_anxiety_width: float = 2.0\r\n    stress_anxiety_colour: Tuple[int, int, int] = (255, 100, 100)\r\n    stress_anxiety_dashed: bool = False\r\n    \r\n    #background_colour: Tuple[int, int, int] = (240, 240, 245)\r\n    \r\n    # Zeroing logic for animations\r\n    pulse_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    pulse_alpha: int = 0\r\n    pulse_duration: float = 0\r\n    pulse_speed: float = 0\r\n    pulse_diameter: float = 0\r\n    \r\n    glow_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    glow_alpha: int = 0\r\n    glow_fade_threshold: float = 0\r\n    \r\n    hover_scale: float = 1.0\r\n    hover_animation_duration: float = 0\r\n    \r\n    activity_highlight_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    activity_highlight_alpha: int = 0\r\n    activity_pulse_speed: float = 0\r\n    \r\n    neurogenesis_highlight_colour: Tuple[int, int, int] = (255, 200, 0)\r\n    neurogenesis_highlight_alpha: int = 200\r\n    neurogenesis_highlight_duration: float = 3.0  \r\n    \r\n    ambient_pulse_width_range: Tuple[float, float] = (0, 0)\r\n    ambient_pulse_alpha_range: Tuple[int, int] = (0, 0)\r\n    ambient_pulse_freq_range: Tuple[float, float] = (0, 0)\r\n    ambient_pulse_phase_drift: float = 0\r\n    \r\n    comm_glow_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    comm_glow_alpha: int = 0\r\n    comm_glow_size: float = 0\r\n    comm_glow_tail_length: float = 0\r\n    comm_glow_speed_range: Tuple[float, float] = (0, 0)\r\n    comm_glow_fade_in: float = 0\r\n    comm_glow_fade_out: float = 0\r\n    comm_glow_spawn_on_activity: bool = False\r\n    comm_glow_spawn_on_weight_change: bool = False\r\n    comm_glow_max_per_connection: int = 0\r\n    \r\n    neural_pulse_duration: float = 0\r\n    neural_pulse_width: float = 0\r\n    neural_pulse_colour_positive: Tuple[int, int, int] = (255, 255, 255)\r\n    neural_pulse_colour_negative: Tuple[int, int, int] = (255, 255, 255)\r\n    neural_weight_thickness: bool = False\r\n    neural_weight_thickness_mult: float = 1.0\r\n    neural_base_colour_positive: Tuple[int, int, int] = (100, 200, 100)\r\n    neural_base_colour_negative: Tuple[int, int, int] = (200, 100, 100)\r\n    neural_base_alpha: int = 180\r\n\r\n\r\n# ===== STYLE REGISTRY =====\r\n\r\nANIMATION_STYLES = {\r\n    'none': NoneStyle,\r\n    'vibrant': VibrantStyle,\r\n    #'subtle': SubtleStyle,\r\n    'neural': NeuralStyle,\r\n    'designer': DesignerStyle,\r\n}\r\n\r\ndef get_animation_style(name: str) -> AnimationStyle:\r\n    \"\"\"\r\n    Get an animation style by name.\r\n    \r\n    Args:\r\n        name: Style name ('vibrant', 'subtle', 'neural', 'designer', or 'none')\r\n        \r\n    Returns:\r\n        AnimationStyle instance\r\n        \r\n    Raises:\r\n        KeyError if style name not found\r\n    \"\"\"\r\n    name_lower = name.lower()\r\n    if name_lower not in ANIMATION_STYLES:\r\n        raise KeyError(f\"Unknown animation style: {name}. \"\r\n                      f\"Available styles: {list(ANIMATION_STYLES.keys())}\")\r\n    return ANIMATION_STYLES[name_lower]\r\n\r\n\r\ndef get_available_styles() -> list:\r\n    \"\"\"Return list of available style names.\"\"\"\r\n    return list(ANIMATION_STYLES.keys())\r\n\r\n\r\ndef get_style_info() -> list:\r\n    \"\"\"Return list of (name, display_name, description) for all styles.\"\"\"\r\n    return [\r\n        (style.name, style.display_name, style.description)\r\n        for style in ANIMATION_STYLES.values()\r\n    ]"
  },
  {
    "path": "src/brain_about_tab.py",
    "content": "import random\nimport os\nfrom PyQt5 import QtCore, QtGui, QtWidgets\nfrom .brain_base_tab import BrainBaseTab\nfrom .localisation import Localisation\nfrom .compute_backend import get_backend\n\n# Predefined list of approved squid names\nSQUID_NAMES = [\n    \"Algernon\", \"Cuthbert\", \"Englebert\", \"D'Artagnan\",\n    \"Gaspard\", \"Ulysses\", \"Leopold\", \"Miroslav\",\n    \"Artemis\", \"Jacques\", \"Cecil\", \"Wilhelm\", \"Giskard\"\n]\n\nclass AboutTab(BrainBaseTab):\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\n        # Initialize localisation\n        self.loc = Localisation.instance()\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\n        self.initialize_ui()\n\n    def update_from_brain_state(self, state):\n        \"\"\"Update tab based on brain state - handle personality updates\"\"\"\n        if not hasattr(self, 'personality_label'):\n            return\n\n        # Check for personality in state\n        if 'personality' in state:\n            # Get the raw personality string (e.g., \"timid\")\n            raw_personality = str(state['personality']).lower()\n            \n            # Get the localized display name (e.g., \"Timido\")\n            display_personality = self.loc.get_personality_name(raw_personality)\n\n            # Only update if the personality has actually changed\n            current_text = self.personality_label.text()\n            # We construct the prefix to check against\n            prefix = f\"{self.loc.get('squid_personality')}: \"\n            current_personality_display = current_text.replace(prefix, \"\").strip()\n\n            if display_personality != current_personality_display:\n                # Update the label with localized text\n                self.personality_label.setText(f\"{prefix}{display_personality}\")\n\n                # Enable care tips button if we now have a personality\n                if hasattr(self, 'care_tips_button'):\n                    # Check against \"Unknown\" or localized equivalent if needed\n                    is_valid = raw_personality != \"unknown\"\n                    self.care_tips_button.setEnabled(is_valid)\n                    \n                    # Update button callback to use current personality\n                    try:\n                        self.care_tips_button.clicked.disconnect()\n                    except TypeError:\n                        pass\n                    # Pass the RAW personality to show_care_tips so it can look up the key correctly\n                    self.care_tips_button.clicked.connect(lambda: self.show_care_tips(raw_personality))\n\n        # Check for squid object updates\n        if hasattr(self.tamagotchi_logic, 'squid') and self.tamagotchi_logic.squid:\n            squid = self.tamagotchi_logic.squid\n\n            # Update name if it exists\n            if hasattr(squid, 'name') and hasattr(self, 'name_label'):\n                current_name = self.name_label.text()\n                if squid.name != current_name:\n                    self.name_label.setText(squid.name)\n\n    def initialize_ui(self):\n        # Get version info first\n        version_info = self.get_version_info()\n\n        # Resolve compute backend display string\n        _backend = get_backend()\n        _bname = _backend.name.lower()\n        if _bname.startswith('onnx') and 'unavailable' not in _bname:\n            _provider = ''\n            if '[' in _backend.name and ']' in _backend.name:\n                _raw = _backend.name.split('[')[1].rstrip(']')\n                _short = {\n                    'DmlExecutionProvider':      'DirectML',\n                    'QNNExecutionProvider':      'QNN·HTP',\n                    'OpenVINOExecutionProvider': 'OpenVINO',\n                    'CPUExecutionProvider':      'CPU',\n                }\n                _provider = _short.get(_raw, _raw.replace('ExecutionProvider', ''))\n            backend_text = f'ONNX · {_provider}' if _provider else 'ONNX'\n        elif 'unavailable' in _bname:\n            backend_text = 'NumPy (ONNX unavailable)'\n        else:\n            backend_text = 'NumPy'\n        \n        from .display_scaling import DisplayScaling\n        \n        # Main text content using QTextEdit\n        about_text = QtWidgets.QTextEdit()\n        about_text.setReadOnly(True)\n        \n        # Set scaled font size for QTextEdit\n        font = about_text.font()\n        font.setPointSize(DisplayScaling.font_size(10))\n        about_text.setFont(font)\n        \n        # Determine the squid name and personality - more robust approach\n        squid_name = random.choice(SQUID_NAMES)\n        raw_personality = \"unknown\"\n        display_personality = \"Unknown\"\n        \n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'squid') and self.tamagotchi_logic.squid:\n                squid = self.tamagotchi_logic.squid\n                \n                # Get personality if available\n                if hasattr(squid, 'personality'):\n                    raw_personality = str(squid.personality).split('.')[-1].lower()\n                    display_personality = self.loc.get_personality_name(raw_personality)\n                    # print(f\"  Found personality: {raw_personality} ({display_personality})\")\n                \n                # Handle name (existing or assign new)\n                if hasattr(squid, 'name'):\n                    if squid.name:\n                        squid_name = squid.name\n                    else:\n                        squid.name = squid_name\n                else:\n                    # Initialize name attribute\n                    squid.name = squid_name\n        \n        # Build About text with version info and LOCALIZED strings\n        about_html = f\"\"\"\n            <h1>{self.loc.get('dosidicus_title')}</h1>\n            <p>\n                <img src=\"images/string.png\"\n                    width=\"128\" height=\"128\"\n                    style=\"float: right; margin: 10px;\">\n                <p> <a href=\"https://github.com/ViciousSquid/Dosidicus\">github.com/ViciousSquid/Dosidicus</a><br>\n                {self.loc.get('dosidicus_desc')} </p> <br>\n                <div style=\"text-align: right;\"><b>{self.loc.get('string_acronym')}</b>\n                <br><br></div><ul>{self.loc.get('created_by')} Rufus Pearce (ViciousSquid)<br>\n                <b>{self.loc.get('version_dosidicus')} {version_info['dosidicus']}</b><br>\n                {self.loc.get('version_brain_tool')} {version_info['brain_tool']}<br>\n                {self.loc.get('version_decision')} {version_info['decision_engine']}<br>\n                {self.loc.get('version_neuro')} {version_info['neurogenesis']}<br>\n                Backend: {backend_text}<br>\n                <p>{self.loc.get('research_project')}</p><br><br>\n                </ul>\n                        \"\"\"\n        about_text.setHtml(about_html)\n        \n        # Create a custom widget for the badge\n        badge_widget = QtWidgets.QWidget()\n        badge_layout = QtWidgets.QVBoxLayout(badge_widget)\n        badge_layout.setContentsMargins(0, 0, 0, 0)\n        badge_layout.setSpacing(0)\n        \n        # Badge container\n        badge_container = QtWidgets.QWidget()\n        badge_container.setFixedWidth(DisplayScaling.scale(300))\n        badge_container.setStyleSheet(\"\"\"\n            background-color: white;\n            border: 4px solid #FF0000;\n            border-radius: 5px;\n        \"\"\")\n        \n        badge_inner_layout = QtWidgets.QVBoxLayout(badge_container)\n        badge_inner_layout.setContentsMargins(0, 0, 0, 0)\n        badge_inner_layout.setSpacing(0)\n        \n        # \"HELLO\" label\n        hello_label = QtWidgets.QLabel(self.loc.get(\"hello\"))\n        hello_label.setAlignment(QtCore.Qt.AlignCenter)\n        hello_label.setStyleSheet(f\"\"\"\n            font-family: Arial, sans-serif;\n            font-size: {DisplayScaling.font_size(38)}px;\n            font-weight: bold;\n            color: #FFFFFF;\n            background-color: #FF0000;\n        \"\"\")\n        badge_inner_layout.addWidget(hello_label)\n        \n        # \"my name is...\" label\n        my_name_label = QtWidgets.QLabel(self.loc.get(\"my_name_is\"))\n        my_name_label.setAlignment(QtCore.Qt.AlignCenter)\n        my_name_label.setStyleSheet(f\"\"\"\n            font-family: Arial, sans-serif;\n            font-size: {DisplayScaling.font_size(20)}px;\n            color: #FFFFFF;\n            background-color: #FF0000;\n        \"\"\")\n        badge_inner_layout.addWidget(my_name_label)\n        \n        # Name label - editable on double-click\n        self.name_label = QtWidgets.QLabel(squid_name)\n        self.name_label.setAlignment(QtCore.Qt.AlignCenter)\n        self.name_label.setStyleSheet(f\"\"\"\n            font-family: Arial, sans-serif;\n            font-size: {DisplayScaling.font_size(38)}px;\n            font-weight: bold;\n            color: #000000;\n            background-color: white;\n        \"\"\")\n        self.name_label.mouseDoubleClickEvent = lambda event: self.edit_name()\n        self.name_label.setToolTip(self.loc.get(\"change_name\")) \n        self.name_label.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))\n        badge_inner_layout.addWidget(self.name_label)\n        \n        badge_layout.addWidget(badge_container, alignment=QtCore.Qt.AlignHCenter)\n        \n        # Add personality information below the badge\n        personality_container = QtWidgets.QWidget()\n        personality_layout = QtWidgets.QVBoxLayout(personality_container)\n        personality_layout.setContentsMargins(DisplayScaling.scale(10), DisplayScaling.scale(20), DisplayScaling.scale(10), DisplayScaling.scale(10))\n        \n        # Personality label - store reference for updates\n        # Using \"squid_personality\" key\n        self.personality_label = QtWidgets.QLabel(f\"{self.loc.get('squid_personality')}: {display_personality}\")\n        self.personality_label.setAlignment(QtCore.Qt.AlignCenter)\n        self.personality_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(22)}px;\")\n        personality_layout.addWidget(self.personality_label)\n        \n       # Button container\n        button_container = QtWidgets.QWidget()\n        button_layout = QtWidgets.QHBoxLayout(button_container)\n        button_layout.setContentsMargins(0, DisplayScaling.scale(10), 0, 0)\n\n        # Add stretchable space to the left\n        button_layout.addStretch()\n\n        # Add Certificate button\n        certificate_button = QtWidgets.QPushButton(self.loc.get(\"view_certificate\"))\n        certificate_button.clicked.connect(self.show_certificate)\n        certificate_button.setStyleSheet(f\"font-size: {DisplayScaling.font_size(18)}px; padding: {DisplayScaling.scale(12)}px;\")\n        # button_layout.addWidget(certificate_button) \n\n        # Add color picker button\n        color_button = QtWidgets.QPushButton(self.loc.get(\"change_colour\")) \n        color_button.clicked.connect(self.open_color_picker)\n        color_button.setStyleSheet(f\"font-size: {DisplayScaling.font_size(18)}px; padding: {DisplayScaling.scale(12)}px; background-color: #FFC0CB;\") \n        button_layout.addWidget(color_button)\n\n        # Add stretchable space to the right\n        button_layout.addStretch()\n\n        # Add button container to personality layout\n        personality_layout.addWidget(button_container)\n\n        # Add all widgets to the main layout\n        self.layout.addWidget(about_text)\n        self.layout.addWidget(badge_widget)\n        self.layout.addWidget(personality_container)\n        \n        print(f\"AboutTab initialization complete - Personality: {display_personality}\")\n\n    def open_color_picker(self):\n        color = QtWidgets.QColorDialog.getColor()\n        if color.isValid():\n            if self.tamagotchi_logic and self.tamagotchi_logic.squid:\n                self.tamagotchi_logic.squid.apply_tint(color)\n                if hasattr(self.tamagotchi_logic, 'brain_window') and \\\n                hasattr(self.tamagotchi_logic.brain_window, 'statistics_tab'):\n                    self.tamagotchi_logic.brain_window.statistics_tab.increment_stat('times_colour_changed')\n\n    def edit_name(self):\n        \"\"\"Allow user to edit squid name on double-click\"\"\"\n        if not hasattr(self, 'name_label'):\n            return\n\n        current_name = self.name_label.text()\n        new_name, ok = QtWidgets.QInputDialog.getText(\n            self, \n            self.loc.get(\"change_name\"), \n            self.loc.get(\"enter_new_name\"),\n            QtWidgets.QLineEdit.Normal, \n            current_name\n        )\n        if ok and new_name:\n            self.name_label.setText(new_name)\n            # Update the squid's name\n            if hasattr(self.tamagotchi_logic, 'squid') and self.tamagotchi_logic.squid:\n                self.tamagotchi_logic.squid.name = new_name\n\n    def show_certificate(self):\n        \"\"\"Show the squid certificate window\"\"\"\n        try:\n            from .certificate import SquidCertificateWindow\n\n            if not hasattr(self, 'certificate_window') or self.certificate_window is None:\n                self.certificate_window = SquidCertificateWindow(self, self.tamagotchi_logic)\n            else:\n                self.certificate_window.update_certificate()\n\n            self.certificate_window.show()\n            self.certificate_window.raise_()\n        except Exception as e:\n            print(f\"Error showing certificate: {e}\")\n            import traceback\n            traceback.print_exc()\n\n    def show_care_tips(self, personality_type_raw):\n        \"\"\"Show care tips for the specific personality type\"\"\"\n        # Ensure we are working with the raw string key (e.g., 'timid')\n        personality_type_raw = str(personality_type_raw).lower()\n        \n        # Get localized name and tips\n        localized_name = self.loc.get_personality_name(personality_type_raw)\n        tips = self.get_care_tips(personality_type_raw)\n\n        # Create a dialog to display the tips\n        dialog = QtWidgets.QDialog(self)\n        dialog.setWindowTitle(f\"{self.loc.get('care_tips')}: {localized_name}\")\n        dialog.setMinimumSize(600, 800)\n\n        layout = QtWidgets.QVBoxLayout(dialog)\n\n        # Add a title\n        # Uses format string \"Care Tips for {personality} Squids\"\n        title_text = self.loc.get(\"care_tips_for\", personality=localized_name)\n        title = QtWidgets.QLabel(title_text)\n        title.setStyleSheet(\"font-size: 20px; font-weight: bold; margin-bottom: 15px;\")\n        title.setAlignment(QtCore.Qt.AlignCenter)\n        layout.addWidget(title)\n\n        # Add the tips content\n        tips_text = QtWidgets.QTextEdit()\n        tips_text.setReadOnly(True)\n\n        font = tips_text.font()\n        font.setPointSize(12)\n        tips_text.setFont(font)\n\n        tips_text.setPlainText(tips)\n        tips_text.setStyleSheet(\"line-height: 1.6;\")\n        layout.addWidget(tips_text)\n\n        # Add a close button\n        close_button = QtWidgets.QPushButton(self.loc.get(\"close\"))\n        close_button.clicked.connect(dialog.close)\n        close_button.setFixedWidth(150)\n        close_button.setStyleSheet(\"font-size: 18px; padding: 8px;\")\n        layout.addWidget(close_button, alignment=QtCore.Qt.AlignRight)\n\n        dialog.exec_()\n\n    def get_care_tips(self, personality_type):\n        \"\"\"Return care tips for a specific personality type using Localisation\"\"\"\n        # This now delegates entirely to the Localisation class which handles languages\n        return self.loc.get_care_tips(personality_type)\n\n    def get_version_info(self):\n        \"\"\"Read version information from the version file\"\"\"\n        version_info = {\n            \"dosidicus\": \"Dosidicus-2 2.6.2.0 STRINg2\",\n            \"brain_tool\": \"06.03.26\",\n            \"decision_engine\": \"4.0\",\n            \"neurogenesis\": \"ver3_unified\"\n        }\n\n        try:\n            # Look for version file in the project root\n            version_file = os.path.join(os.path.dirname(__file__), '..', 'version')\n            if os.path.exists(version_file):\n                with open(version_file, 'r') as f:\n                    for line in f:\n                        if ':' in line:\n                            key, value = line.split(':', 1)\n                            key = key.strip().lower()\n                            value = value.strip()\n                            if key in version_info:\n                                version_info[key] = value\n        except Exception as e:\n            print(f\"Error reading version file: {e}\")\n\n        return version_info\n"
  },
  {
    "path": "src/brain_base_tab.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom .display_scaling import DisplayScaling\n\nclass BrainBaseTab(QtWidgets.QWidget):\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\n        super().__init__(parent)\n        self.parent = parent\n        self.tamagotchi_logic = tamagotchi_logic\n        self.brain_widget = brain_widget\n        self.config = config\n        self.debug_mode = debug_mode\n        self.layout = QtWidgets.QVBoxLayout()\n        self.setLayout(self.layout)\n\n        # Set an explicit base font so all child widgets inherit a consistent\n        # size regardless of platform defaults (important for Nuitka builds\n        # where Qt may resolve a smaller system font than CPython does).\n        base_font = QtGui.QFont()\n        base_font.setPointSize(DisplayScaling.font_size(10))\n        self.setFont(base_font)\n\n    def set_tamagotchi_logic(self, tamagotchi_logic):\n        \"\"\"Update the tamagotchi_logic reference\"\"\"\n        #print(f\"BrainBaseTab.set_tamagotchi_logic: {tamagotchi_logic is not None}\")\n        self.tamagotchi_logic = tamagotchi_logic\n        \n    def update_from_brain_state(self, state):\n        \"\"\"Update tab based on brain state - override in subclasses\"\"\"\n        pass\n        \n    def create_button(self, text, callback, color):\n        \"\"\"Common utility for creating consistent buttons\"\"\"\n        button = QtWidgets.QPushButton(text)\n        button.clicked.connect(callback)\n        button.setStyleSheet(f\"background-color: {color}; border: 1px solid black; padding: 5px;\")\n        button.setFixedSize(200, 50)\n        return button\n    \n    \n\n\n"
  },
  {
    "path": "src/brain_constants.py",
    "content": "\"\"\"\r\nbrain_constants.py - Shared constants for Dosidicus-2 brain system\r\n\r\nThis file defines the canonical neuron categories used across:\r\n- brain_widget.py\r\n- brain_tool.py  \r\n- brain_designer.py\r\n- brain_neuron_hooks.py\r\n\r\nImport from here to ensure consistency.\r\n\"\"\"\r\n\r\n# =============================================================================\r\n# REQUIRED NEURONS - Mandatory for all brain designs\r\n# These 8 neurons MUST exist in any valid Dosidicus brain.\r\n# =============================================================================\r\n\r\n# Core stat neurons - positions match brain_widget.py's original_neuron_positions\r\nCORE_NEURONS = {\r\n    \"hunger\": (127, 81),\r\n    \"happiness\": (361, 81),\r\n    \"cleanliness\": (627, 81),\r\n    \"sleepiness\": (840, 81),\r\n    \"satisfaction\": (271, 380),\r\n    \"anxiety\": (491, 389),\r\n    \"curiosity\": (701, 386),\r\n}\r\n\r\n# can_see_food is MANDATORY - the squid must be able to see food\r\n# This is separate from optional sensors because it's required for basic functionality\r\nMANDATORY_SENSOR = {\r\n    \"can_see_food\": (50, 200),\r\n}\r\n\r\n# All required neurons combined (7 core + 1 mandatory sensor)\r\nREQUIRED_NEURONS = {**CORE_NEURONS, **MANDATORY_SENSOR}\r\n\r\n# Ordered list for consistent iteration\r\nCORE_NEURON_NAMES = [\r\n    \"hunger\", \"happiness\", \"cleanliness\", \"sleepiness\",\r\n    \"satisfaction\", \"anxiety\", \"curiosity\"\r\n]\r\n\r\nREQUIRED_NEURON_NAMES = CORE_NEURON_NAMES + [\"can_see_food\"]\r\n\r\n# =============================================================================\r\n# INPUT SENSORS - Optional neurons that receive values from game state\r\n# These neurons get their activation from BrainNeuronHooks, not from\r\n# neural propagation. They represent environmental/state observations.\r\n# NOTE: can_see_food is NOT here - it's in REQUIRED_NEURONS\r\n# =============================================================================\r\nINPUT_SENSORS = {\r\n    \"external_stimulus\": (50, 50),      # Environmental changes (resize, interactions)\r\n    \"plant_proximity\": (50, 250),       # Distance to nearest plant decoration\r\n    \"threat_level\": (50, 350),          # Computed from anxiety + startle state\r\n    \"pursuing_food\": (150, 50),         # Currently chasing food\r\n    \"is_sick\": (150, 150),              # Sickness state\r\n    \"is_fleeing\": (150, 250),           # Currently fleeing\r\n    \"is_eating\": (150, 350),            # Currently eating\r\n    \"is_sleeping\": (250, 50),           # Currently sleeping\r\n    \"is_startled\": (250, 150),          # Startled state\r\n}\r\n\r\n# Tuple for compatibility with brain_neuron_hooks.py (includes can_see_food)\r\nDEFAULT_INPUT_SENSORS = (\r\n    'external_stimulus',\r\n    'can_see_food',  # Listed here for hooks compatibility but it's mandatory\r\n    'plant_proximity',\r\n    'threat_level',\r\n    'pursuing_food',\r\n    'is_sick',\r\n    'is_fleeing',\r\n    'is_eating',\r\n    'is_sleeping',\r\n    'is_startled',\r\n)\r\n\r\n# =============================================================================\r\n# BINARY NEURONS - Neurons that should display as on/off (0 or 100)\r\n# These use bright green/red coloring instead of gradient heat maps.\r\n# =============================================================================\r\nBINARY_NEURONS = {\r\n    'can_see_food',\r\n    'is_eating',\r\n    'is_sleeping',\r\n    'is_sick',\r\n    'pursuing_food',\r\n    'is_fleeing',\r\n    'is_startled',\r\n    'external_stimulus',  # Usually high/low, treat as binary-ish\r\n}\r\n\r\n# =============================================================================\r\n# EXCLUDED NEURONS - Status neurons that exist but aren't visualized normally\r\n# These are tracked in brain state but not shown in the main visualization.\r\n# =============================================================================\r\nEXCLUDED_NEURONS = [\r\n    'is_sick',\r\n    'is_eating', \r\n    'pursuing_food',\r\n    'direction',\r\n    'is_sleeping'\r\n]\r\n\r\n# =============================================================================\r\n# VISUAL STYLE CONSTANTS - For consistent rendering across tools\r\n# =============================================================================\r\n\r\n# Ring colors for neuron types\r\nCORE_NEURON_RING_COLOR = (255, 215, 0)      # Gold\r\nINPUT_SENSOR_RING_COLOR = (100, 149, 237)   # Cornflower blue\r\nCUSTOM_NEURON_RING_COLOR = (180, 180, 180)  # Gray\r\n\r\n# Ring widths\r\nPROTECTED_RING_WIDTH = 3\r\nNORMAL_RING_WIDTH = 2\r\n\r\n# Default neuron colors by type\r\nDEFAULT_COLORS = {\r\n    'core': (150, 150, 220),      # Soft purple\r\n    'input': (100, 200, 150),     # Soft green  \r\n    'output': (220, 150, 150),    # Soft red\r\n    'hidden': (180, 180, 200),    # Neutral\r\n    'sensor': (150, 200, 220),    # Soft blue\r\n}\r\n\r\n# =============================================================================\r\n# LAYER DEFAULTS\r\n# =============================================================================\r\nDEFAULT_LAYER_HEIGHT = 120\r\nDEFAULT_LAYER_SPACING = 150\r\n\r\nLAYER_COLORS = {\r\n    'input': {\r\n        'fill': (200, 255, 200, 80),\r\n        'border': (150, 220, 150, 120)\r\n    },\r\n    'output': {\r\n        'fill': (255, 200, 200, 80),\r\n        'border': (220, 150, 150, 120)\r\n    },\r\n    'hidden': {\r\n        'fill': (230, 230, 255, 80),\r\n        'border': (200, 200, 240, 120)\r\n    }\r\n}\r\n\r\n\r\n# =============================================================================\r\n# HELPER FUNCTIONS\r\n# =============================================================================\r\n\r\ndef is_core_neuron(name: str) -> bool:\r\n    \"\"\"Check if a neuron name is a core stat neuron (7 stats).\"\"\"\r\n    return name in CORE_NEURONS\r\n\r\ndef is_required_neuron(name: str) -> bool:\r\n    \"\"\"Check if a neuron is required (core + can_see_food).\"\"\"\r\n    return name in REQUIRED_NEURONS\r\n\r\ndef is_input_sensor(name: str) -> bool:\r\n    \"\"\"Check if a neuron name is an optional input sensor.\"\"\"\r\n    return name in INPUT_SENSORS\r\n\r\ndef is_any_sensor(name: str) -> bool:\r\n    \"\"\"Check if a neuron receives values from hooks (including can_see_food).\"\"\"\r\n    return name in INPUT_SENSORS or name in MANDATORY_SENSOR or name in DEFAULT_INPUT_SENSORS\r\n\r\ndef is_binary_neuron(name: str) -> bool:\r\n    \"\"\"Check if a neuron should display as binary (on/off).\"\"\"\r\n    return name in BINARY_NEURONS\r\n\r\ndef is_protected_neuron(name: str) -> bool:\r\n    \"\"\"Check if a neuron is protected (cannot be deleted or renamed).\"\"\"\r\n    return is_required_neuron(name)\r\n\r\ndef get_neuron_category(name: str) -> str:\r\n    \"\"\"Get the category of a neuron: 'core', 'required', 'sensor', or 'custom'.\"\"\"\r\n    if is_core_neuron(name):\r\n        return 'core'\r\n    elif name == 'can_see_food':\r\n        return 'required'\r\n    elif is_input_sensor(name):\r\n        return 'sensor'\r\n    else:\r\n        return 'custom'\r\n\r\ndef get_all_standard_neurons() -> dict:\r\n    \"\"\"Get all standard neurons (required + optional sensors) with positions.\"\"\"\r\n    result = dict(REQUIRED_NEURONS)\r\n    result.update(INPUT_SENSORS)\r\n    return result\r\n\r\ndef get_missing_required(existing_neurons: set) -> list:\r\n    \"\"\"Get list of required neurons not in the given set.\"\"\"\r\n    return [name for name in REQUIRED_NEURONS if name not in existing_neurons]"
  },
  {
    "path": "src/brain_decisions_tab.py",
    "content": "# src/brain_decisions_tab.py\r\n\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom .brain_base_tab import BrainBaseTab\r\nfrom .display_scaling import DisplayScaling\r\nfrom .localisation import Localisation\r\n\r\nclass DecisionsTab(BrainBaseTab):\r\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\r\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\r\n        self.loc = Localisation.instance()\r\n        self.initialize_ui()\r\n\r\n    def initialize_ui(self):\r\n        \"\"\"\r\n        Initializes the UI with a persistent, non-flickering layout for the decision path\r\n        and a fixed bar at the bottom for the final action.\r\n        \"\"\"\r\n        self.layout.setContentsMargins(DisplayScaling.scale(15), DisplayScaling.scale(15), DisplayScaling.scale(15), DisplayScaling.scale(15))\r\n        self.layout.setSpacing(DisplayScaling.scale(10))\r\n\r\n        # Main container\r\n        main_container = QtWidgets.QWidget()\r\n        main_container.setObjectName(\"mainContainer\")\r\n        main_container.setStyleSheet(\"background-color: #f8f9fa; border-radius: 10px;\")\r\n        main_layout = QtWidgets.QVBoxLayout(main_container)\r\n        main_layout.setContentsMargins(DisplayScaling.scale(10), DisplayScaling.scale(10), DisplayScaling.scale(10), DisplayScaling.scale(10))\r\n        self.layout.addWidget(main_container)\r\n\r\n        # Title\r\n        title_layout = QtWidgets.QHBoxLayout()\r\n        title_icon = QtWidgets.QLabel(\"🧠\")\r\n        title_icon.setStyleSheet(f\"font-size: {DisplayScaling.font_size(34)}px;\")\r\n        title_label = QtWidgets.QLabel(self.loc.get(\"thought_process\"))\r\n        title_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(30)}px; font-weight: bold; color: #343a40;\")\r\n        title_layout.addWidget(title_icon)\r\n        title_layout.addWidget(title_label)\r\n        title_layout.addStretch()\r\n        main_layout.addLayout(title_layout)\r\n\r\n        # Scroll area for the decision path (takes up the expandable space)\r\n        path_scroll_area = QtWidgets.QScrollArea()\r\n        path_scroll_area.setWidgetResizable(True)\r\n        path_scroll_area.setStyleSheet(\"QScrollArea { border: none; background-color: #f8f9fa; }\")\r\n        main_layout.addWidget(path_scroll_area, 1)\r\n        \r\n        path_container = QtWidgets.QWidget()\r\n        self.path_layout = QtWidgets.QVBoxLayout(path_container)\r\n        self.path_layout.setSpacing(DisplayScaling.scale(15))\r\n        self.path_layout.setAlignment(QtCore.Qt.AlignTop)\r\n        path_scroll_area.setWidget(path_container)\r\n\r\n        # --- Create persistent widgets and labels for each step ---\r\n        # Step 1: Current State\r\n        step1, self.step1_label = self._create_path_step_widget(1, self.loc.get(\"step1_title\"), \"📡\")\r\n        self.path_layout.addWidget(step1)\r\n        self.path_layout.addWidget(self._create_arrow())\r\n\r\n        # Step 2: Base Urges\r\n        step2, self.step2_label = self._create_path_step_widget(2, self.loc.get(\"step2_title\"), \"⚖️\")\r\n        self.path_layout.addWidget(step2)\r\n        self.path_layout.addWidget(self._create_arrow())\r\n        \r\n        # Step 3: Personality & Memory\r\n        step3, self.step3_label = self._create_path_step_widget(3, self.loc.get(\"step3_title\"), \"🎭\")\r\n        self.path_layout.addWidget(step3)\r\n        self.path_layout.addWidget(self._create_arrow())\r\n\r\n        # Step 4: Final Decision\r\n        step4, self.step4_label = self._create_path_step_widget(4, self.loc.get(\"step4_title\"), \"✅\")\r\n        self.path_layout.addWidget(step4)\r\n\r\n        # --- Final Action Bar (at the bottom) ---\r\n        final_action_bar = QtWidgets.QFrame()\r\n        final_action_bar.setObjectName(\"finalActionBar\")\r\n        final_action_bar.setStyleSheet(\"\"\"\r\n            #finalActionBar {\r\n                background-color: #e9ecef;\r\n                border: 1px solid #ced4da;\r\n                border-radius: 8px;\r\n            }\r\n        \"\"\")\r\n        final_action_bar.setFixedHeight(DisplayScaling.scale(60))\r\n        \r\n        bar_layout = QtWidgets.QHBoxLayout(final_action_bar)\r\n        bar_layout.setContentsMargins(DisplayScaling.scale(15), DisplayScaling.scale(5), DisplayScaling.scale(15), DisplayScaling.scale(5))\r\n        \r\n        action_title_label = QtWidgets.QLabel(f\"<b>{self.loc.get('final_action')}</b>\")\r\n        action_title_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(22)}px; color: #495057;\")\r\n        \r\n        self.final_action_label = QtWidgets.QLabel(\"...\")\r\n        self.final_action_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(22)}px; font-weight: bold; color: #007bff;\")\r\n\r\n        bar_layout.addWidget(action_title_label)\r\n        bar_layout.addWidget(self.final_action_label)\r\n        bar_layout.addStretch()\r\n\r\n        main_layout.addWidget(final_action_bar)\r\n        \r\n        self.update_path_with_placeholder()\r\n\r\n    def _create_path_step_widget(self, step_number, title, icon):\r\n        \"\"\"Creates a styled widget for a single step and returns it and its content label.\"\"\"\r\n        step_widget = QtWidgets.QWidget()\r\n        step_widget.setObjectName(\"stepWidget\")\r\n        step_widget.setStyleSheet(f\"\"\"\r\n            #stepWidget {{\r\n                background-color: #ffffff;\r\n                border: 1px solid #dee2e6;\r\n                border-radius: 8px;\r\n                padding: {DisplayScaling.scale(10)}px;\r\n            }}\r\n        \"\"\")\r\n        step_layout = QtWidgets.QVBoxLayout(step_widget)\r\n\r\n        header_layout = QtWidgets.QHBoxLayout()\r\n        icon_label = QtWidgets.QLabel(icon)\r\n        icon_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(30)}px;\")\r\n        title_label = QtWidgets.QLabel(f\"<b>{self.loc.get('step')} {step_number}: {title}</b>\")\r\n        title_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(22)}px; color: #495057;\")\r\n        header_layout.addWidget(icon_label)\r\n        header_layout.addWidget(title_label)\r\n        header_layout.addStretch()\r\n        step_layout.addLayout(header_layout)\r\n\r\n        content_label = QtWidgets.QLabel(\"...\")\r\n        content_label.setWordWrap(True)\r\n        content_label.setAlignment(QtCore.Qt.AlignTop)\r\n        content_label.setStyleSheet(f\"padding-left: {DisplayScaling.scale(10)}px; padding-top: {DisplayScaling.scale(5)}px; font-size: {DisplayScaling.font_size(19)}px;\")\r\n        step_layout.addWidget(content_label)\r\n        \r\n        return step_widget, content_label\r\n\r\n    def update_path_with_placeholder(self):\r\n        \"\"\"Sets initial placeholder content on the persistent labels.\"\"\"\r\n        placeholder_text = f\"<i style='color: #6c757d; font-size: {DisplayScaling.font_size(19)}px;'>{self.loc.get('awaiting_thought')}</i>\"\r\n        self.step1_label.setText(placeholder_text)\r\n        self.step2_label.setText(placeholder_text)\r\n        self.step3_label.setText(placeholder_text)\r\n        self.step4_label.setText(placeholder_text)\r\n        self.final_action_label.setText(self.loc.get(\"awaiting_decision\"))\r\n\r\n    def update_from_brain_state(self, state):\r\n        \"\"\"Update visualization based on brain state.\"\"\"\r\n        if hasattr(self.tamagotchi_logic, 'get_decision_data'):\r\n            decision_data = self.tamagotchi_logic.get_decision_data()\r\n            if decision_data:\r\n                self.update_decision_path(decision_data)\r\n\r\n    def update_decision_path(self, data):\r\n        \"\"\"Updates the content of the persistent step labels and the final action bar.\"\"\"\r\n        final_decision = data.get('final_decision', 'N/A')\r\n\r\n        self._update_state_step(data.get('inputs', {}))\r\n        self._update_urges_step(data.get('weights', {}))\r\n        self._update_modifiers_step(data)\r\n        self._update_final_decision_step(data, final_decision)\r\n\r\n        # Update the bottom bar - translate action name if possible\r\n        action_key = final_decision.lower().replace(' ', '_')\r\n        translated_action = self.loc.get(action_key)\r\n        # If no translation found, use original capitalized\r\n        if translated_action == action_key:\r\n            translated_action = final_decision.capitalize()\r\n        self.final_action_label.setText(translated_action)\r\n\r\n    def _translate_object(self, obj_name):\r\n        \"\"\"Translate object name (food, rock, poop, plant)\"\"\"\r\n        key = obj_name.lower()\r\n        translated = self.loc.get(key)\r\n        return translated if translated != key else obj_name\r\n\r\n    def _translate_action(self, action_name):\r\n        \"\"\"Translate action name\"\"\"\r\n        key = action_name.lower().replace(' ', '_')\r\n        translated = self.loc.get(key)\r\n        return translated if translated != key else action_name.capitalize()\r\n\r\n    def _update_state_step(self, inputs):\r\n        text = f\"{self.loc.get('sensing_condition')}<br><ul>\"\r\n        if not inputs:\r\n            text += f\"<li>{self.loc.get('no_sensory_data')}</li>\"\r\n        else:\r\n            visible_items = []\r\n            if inputs.get(\"has_food_visible\"):\r\n                visible_items.append(self.loc.get(\"food\"))\r\n            if inputs.get(\"has_rock_visible\"):\r\n                visible_items.append(self.loc.get(\"rock\"))\r\n            if inputs.get(\"has_poop_visible\"):\r\n                visible_items.append(self.loc.get(\"poop\"))\r\n            if inputs.get(\"has_plant_visible\"):\r\n                visible_items.append(self.loc.get(\"plant\"))\r\n\r\n            if visible_items:\r\n                text += f\"<li><b>{self.loc.get('visible_objects')}:</b> {', '.join(visible_items)}</li>\"\r\n            else:\r\n                text += f\"<li><b>{self.loc.get('visible_objects')}:</b> {self.loc.get('none')}</li>\"\r\n\r\n            excluded_keys = {\"has_food_visible\", \"has_rock_visible\", \"has_poop_visible\", \"has_plant_visible\"}\r\n            for key, value in sorted(inputs.items()):\r\n                if key not in excluded_keys:\r\n                    formatted_value = f\"{value:.2f}\" if isinstance(value, float) else str(value)\r\n                    # Try to translate the key\r\n                    display_key = key.replace('_', ' ').capitalize()\r\n                    text += f\"<li><b>{display_key}:</b> {formatted_value}</li>\"\r\n        text += \"</ul>\"\r\n        self.step1_label.setText(text)\r\n\r\n    def _update_urges_step(self, weights):\r\n        if not weights:\r\n            self.step2_label.setText(self.loc.get(\"no_urges\"))\r\n            return\r\n\r\n        strongest_urge = max(weights, key=weights.get)\r\n        translated_urge = self._translate_action(strongest_urge)\r\n        text = f\"{self.loc.get('strongest_urge')} <b>{translated_urge}</b>.<br><br>{self.loc.get('initial_scores')}\"\r\n        text += \"<ul>\"\r\n        for action, weight in sorted(weights.items(), key=lambda item: item[1], reverse=True):\r\n            translated_action = self._translate_action(action)\r\n            text += f\"<li><b>{translated_action}:</b> {weight:.3f}</li>\"\r\n        text += \"</ul>\"\r\n        self.step2_label.setText(text)\r\n\r\n    def _update_modifiers_step(self, data):\r\n        weights = data.get('weights', {})\r\n        adj_weights = data.get('adjusted_weights', {})\r\n        text = f\"{self.loc.get('personality_memory_adjust')}<br><ul>\"\r\n\r\n        modified = False\r\n        for action, final_score in adj_weights.items():\r\n            base_score = weights.get(action, final_score)\r\n            delta = final_score - base_score\r\n            if abs(delta) > 0.001:\r\n                direction = self.loc.get(\"score_increased\") if delta > 0 else self.loc.get(\"score_decreased\")\r\n                color = \"#28a745\" if delta > 0 else \"#dc3545\"\r\n                translated_action = self._translate_action(action)\r\n                text += f\"<li>{self.loc.get('score_for')} <b>{translated_action}</b> {direction} {self.loc.get('by_amount')} {abs(delta):.3f} <span style='color:{color};'>({delta:+.3f})</span></li>\"\r\n                modified = True\r\n\r\n        if not modified:\r\n            text += f\"<li>{self.loc.get('no_adjustments')}</li>\"\r\n\r\n        text += \"</ul>\"\r\n        self.step3_label.setText(text)\r\n\r\n    def _update_final_decision_step(self, data, final_decision):\r\n        confidence = data.get('confidence', 0.0)\r\n        adj_weights = data.get('adjusted_weights', {})\r\n\r\n        text = self.loc.get(\"final_scores_text\")\r\n        text += \"<ul>\"\r\n        if not adj_weights:\r\n            text += f\"<li>{self.loc.get('no_final_scores')}</li>\"\r\n        else:\r\n            for action, score in sorted(adj_weights.items(), key=lambda item: item[1], reverse=True):\r\n                translated_action = self._translate_action(action)\r\n                item_text = f\"<li><b>{translated_action}:</b> {score:.3f}</li>\"\r\n                if action == final_decision:\r\n                    item_text = f\"<li style='background-color: #d4edda; border-radius: 4px; padding: 2px;'><b>▶ {translated_action}: {score:.3f}</b></li>\"\r\n                text += item_text\r\n        text += \"</ul>\"\r\n\r\n        translated_decision = self._translate_action(final_decision)\r\n        text += f\"<hr>{self.loc.get('squid_decided')} <b>{translated_decision}</b> {self.loc.get('with_confidence')} <b>{confidence:.1%}</b>.\"\r\n        self.step4_label.setText(text)\r\n\r\n    def _create_arrow(self):\r\n        arrow_label = QtWidgets.QLabel(\"⬇️\")\r\n        arrow_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        arrow_label.setStyleSheet(f\"font-size: {DisplayScaling.font_size(24)}px; color: #adb5bd; margin: -5px 0 -5px 0;\")\r\n        return arrow_label\r\n"
  },
  {
    "path": "src/brain_designer.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nBrain Designer - Visual Neural Network Designer for Dosidicus-2\n\nLaunch the brain designer GUI application with error handling and logging.\n\"\"\"\n\nimport sys\nimport os\nimport shutil\nimport argparse\n\n# Ensure the designer package can be found\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nif script_dir not in sys.path:\n    sys.path.insert(0, script_dir)\n\n\ndef perform_cleanup_and_exit():\n    \"\"\"Recursively delete __pycache__ directories.\"\"\"\n    print(\"🧹 Cleaning environment...\")\n    root_dir = os.path.dirname(os.path.abspath(__file__))\n    \n    deleted_count = 0\n    \n    for root, dirs, files in os.walk(root_dir, topdown=True):\n        # Filter and remove specific directories\n        for name in list(dirs):\n            if name == '__pycache__':\n                path = os.path.join(root, name)\n                try:\n                    shutil.rmtree(path)\n                    print(f\"   Deleted: {path}\")\n                    dirs.remove(name)  # Prevent os.walk from trying to enter this dir\n                    deleted_count += 1\n                except Exception as e:\n                    print(f\"   ❌ Failed to delete {path}: {e}\")\n                    \n    print(f\"✨ Cleanup complete. Removed {deleted_count} directories.\")\n    sys.exit(0)\n\n\ndef show_error_dialog(title: str, message: str):\n    \"\"\"Show an error dialog using PyQt5 if available, otherwise print.\"\"\"\n    try:\n        from PyQt5.QtWidgets import QApplication, QMessageBox\n        \n        # Create app if needed (for showing dialog before main app starts)\n        app = QApplication.instance()\n        if app is None:\n            app = QApplication(sys.argv)\n        \n        msg_box = QMessageBox()\n        msg_box.setIcon(QMessageBox.Critical)\n        msg_box.setWindowTitle(title)\n        msg_box.setText(message)\n        msg_box.setStandardButtons(QMessageBox.Ok)\n        msg_box.exec_()\n    except Exception:\n        # Fallback to console\n        print(f\"\\n{'='*60}\")\n        print(f\"ERROR: {title}\")\n        print('='*60)\n        print(message)\n        print('='*60 + \"\\n\")\n\n\ndef check_dependencies():\n    \"\"\"Check that required dependencies are available.\"\"\"\n    missing = []\n    \n    try:\n        import PyQt5\n    except ImportError:\n        missing.append(\"PyQt5\")\n    \n    if missing:\n        msg = (\n            f\"Missing required dependencies:\\n\\n\"\n            f\"  {', '.join(missing)}\\n\\n\"\n            f\"Please install with:\\n\"\n            f\"  pip install {' '.join(missing)}\"\n        )\n        show_error_dialog(\"Missing Dependencies\", msg)\n        sys.exit(1)\n\n\ndef main():\n    \"\"\"Main entry point with full error handling.\"\"\"\n    \n    # Parse command line arguments\n    parser = argparse.ArgumentParser(description=\"Brain Designer - Visual Neural Network Designer\")\n    parser.add_argument('-c', '--clean', action='store_true',\n                       help='Clean __pycache__ folders from designer directory before starting')\n    parser.add_argument('-d', '--debug', action='store_true',\n                       help='Enable debug mode with logging')\n    \n    # Use parse_known_args so we don't choke if Qt arguments are passed implicitly\n    args, _ = parser.parse_known_args()\n\n    # Perform cleanup if requested\n    if args.clean:\n        perform_cleanup_and_exit()\n    \n    # Check dependencies first\n    check_dependencies()\n    \n    # Import logging after dependency check\n    from src.designer_logging import (\n        initialize_error_handling, \n        get_logger,\n        OperationLogger\n    )\n    \n    # Initialize error handling and logging (only create log files in debug mode)\n    crash_reporter = initialize_error_handling(enable_logging=args.debug)\n    logger = get_logger()\n    \n    # Set up error dialog callback\n    crash_reporter.set_error_dialog_callback(show_error_dialog)\n    \n    if args.debug:\n        logger.info(\"Starting Brain Designer application (debug mode)\")\n    \n    try:\n        from PyQt5.QtWidgets import QApplication\n        from PyQt5.QtGui import QColor, QPalette\n        from PyQt5.QtCore import Qt\n\n        # Enable HiDPI scaling — must be set before QApplication is created\n        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)\n        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)\n\n        app = QApplication(sys.argv)\n        app.setApplicationName(\"Brain Designer (Beta)\")\n        app.setApplicationVersion(\"1.1.0\")\n\n        # Enforce minimum readable font size across all widgets\n        _app_font = app.font()\n        if _app_font.pointSize() < 10:\n            _app_font.setPointSize(10)\n            app.setFont(_app_font)\n        \n        # Light theme setup\n        app.setStyle('Fusion')\n        \n        palette = app.palette()\n        palette.setColor(QPalette.Window, QColor(240, 240, 245))\n        palette.setColor(QPalette.WindowText, QColor(40, 40, 50))\n        palette.setColor(QPalette.Base, QColor(255, 255, 255))\n        palette.setColor(QPalette.AlternateBase, QColor(245, 245, 250))\n        palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 230))\n        palette.setColor(QPalette.ToolTipText, QColor(40, 40, 50))\n        palette.setColor(QPalette.Text, QColor(40, 40, 50))\n        palette.setColor(QPalette.Button, QColor(240, 240, 245))\n        palette.setColor(QPalette.ButtonText, QColor(40, 40, 50))\n        palette.setColor(QPalette.BrightText, QColor(255, 0, 0))\n        palette.setColor(QPalette.Highlight, QColor(70, 130, 200))\n        palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))\n        app.setPalette(palette)\n        \n        from src.designer_window import BrainDesignerWindow\n        \n        window = BrainDesignerWindow()\n        \n        if args.debug:\n            logger.info(\"Showing main window\")\n        window.show()\n        \n        if args.debug:\n            logger.info(\"Entering event loop\")\n        exit_code = app.exec_()\n        \n        if args.debug:\n            logger.info(f\"Application exited with code {exit_code}\")\n        sys.exit(exit_code)\n        \n    except ImportError as e:\n        if args.debug:\n            logger.critical(f\"Import error: {e}\")\n        show_error_dialog(\n            \"Import Error\",\n            f\"Failed to import required module:\\n\\n{e}\\n\\n\"\n            f\"Please ensure all dependencies are installed.\"\n        )\n        sys.exit(1)\n        \n    except Exception as e:\n        if args.debug:\n            logger.critical(f\"Startup error: {e}\", exc_info=True)\n        show_error_dialog(\n            \"Startup Error\",\n            f\"Failed to start Brain Designer:\\n\\n{e}\\n\\n\"\n            f\"Check the logs folder for details.\" if args.debug else f\"Failed to start Brain Designer:\\n\\n{e}\"\n        )\n        sys.exit(1)\n\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "src/brain_designer_launcher.py",
    "content": "\"\"\"\r\nBrain Designer Launcher - Spawns the designer in a subprocess\r\n\r\nThis module provides a function to launch the Brain Designer tool\r\nfrom the main game process, optionally passing debug mode.\r\n\"\"\"\r\n\r\nimport sys\r\nimport os\r\n\r\n\r\ndef launch_brain_designer_process(debug_mode: bool = False):\r\n    \"\"\"\r\n    Entry point for Brain Designer when launched as a subprocess.\r\n    \r\n    Args:\r\n        debug_mode: If True, enables logging in the designer\r\n    \"\"\"\r\n    # Build command line args\r\n    if debug_mode:\r\n        if '-d' not in sys.argv:\r\n            sys.argv.append('-d')\r\n    \r\n    # Import and run the designer's main function\r\n    from src.brain_designer import main as designer_main\r\n    designer_main()"
  },
  {
    "path": "src/brain_dialogs.py",
    "content": "import sys\r\nimport csv\r\nimport os\r\nimport time\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom PyQt5.QtGui import QPixmap, QFont\r\n\r\nclass StimulateDialog(QtWidgets.QDialog):\r\n    def __init__(self, brain_widget, parent=None):\r\n        super().__init__(parent)\r\n        self.brain_widget = brain_widget\r\n        self.current_neuron = None\r\n        \r\n        from .display_scaling import DisplayScaling\r\n        \r\n        self.setWindowTitle(\"Neuron Inspector\")\r\n        self.setFixedSize(DisplayScaling.scale(600), DisplayScaling.scale(500))\r\n        \r\n        # Main layout\r\n        layout = QtWidgets.QVBoxLayout()\r\n        self.setLayout(layout)\r\n        \r\n        # Style all text properly\r\n        self.setStyleSheet(f\"\"\"\r\n            QLabel, QComboBox, QPushButton {{\r\n                font-size: {DisplayScaling.font_size(12)}px;\r\n            }}\r\n            QTextEdit, QListWidget {{\r\n                font-size: {DisplayScaling.font_size(12)}px;\r\n                line-height: {DisplayScaling.scale(1.5)};\r\n            }}\r\n        \"\"\")\r\n        \r\n        # Neuron info section\r\n        self.info_group = QtWidgets.QGroupBox(\"Neuron Information\")\r\n        self.info_layout = QtWidgets.QFormLayout()\r\n        self.info_group.setLayout(self.info_layout)\r\n        layout.addWidget(self.info_group)\r\n        \r\n        # Info fields\r\n        self.name_label = QtWidgets.QLabel()\r\n        self.state_label = QtWidgets.QLabel()\r\n        self.position_label = QtWidgets.QLabel()\r\n        self.type_label = QtWidgets.QLabel()\r\n        \r\n        self.info_layout.addRow(\"Name:\", self.name_label)\r\n        self.info_layout.addRow(\"Current State:\", self.state_label)\r\n        self.info_layout.addRow(\"Position:\", self.position_label)\r\n        self.info_layout.addRow(\"Type:\", self.type_label)\r\n        \r\n        # Connections table\r\n        self.connections_group = QtWidgets.QGroupBox(\"Connections\")\r\n        self.connections_layout = QtWidgets.QVBoxLayout()\r\n        self.connections_group.setLayout(self.connections_layout)\r\n        layout.addWidget(self.connections_group)\r\n        \r\n        self.connections_table = QtWidgets.QTableWidget()\r\n        self.connections_table.setColumnCount(5)\r\n        self.connections_table.setHorizontalHeaderLabels([\r\n            \"Neuron\", \"Direction\", \"Weight\", \"Strength\", \"State\"\r\n        ])\r\n        self.connections_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)\r\n        self.connections_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\r\n        self.connections_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\r\n        self.connections_layout.addWidget(self.connections_table)\r\n        \r\n        # Activity graph\r\n        self.activity_group = QtWidgets.QGroupBox(\"Activity History\")\r\n        self.activity_layout = QtWidgets.QVBoxLayout()\r\n        self.activity_group.setLayout(self.activity_layout)\r\n        layout.addWidget(self.activity_group)\r\n        \r\n        self.activity_plot = QtWidgets.QGraphicsView()\r\n        self.activity_scene = QtWidgets.QGraphicsScene()\r\n        self.activity_plot.setScene(self.activity_scene)\r\n        self.activity_layout.addWidget(self.activity_plot)\r\n        \r\n        # Close button\r\n        self.close_button = QtWidgets.QPushButton(\"Close\")\r\n        self.close_button.clicked.connect(self.close)\r\n        layout.addWidget(self.close_button)\r\n        \r\n        # Connect to brain widget's neuron click signal\r\n        if hasattr(brain_widget, 'neuronClicked'):\r\n            brain_widget.neuronClicked.connect(self.inspect_neuron)\r\n\r\n        self.buttons = QtWidgets.QDialogButtonBox(\r\n            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel\r\n        )\r\n        self.buttons.accepted.connect(self.validate_and_accept)\r\n        self.buttons.rejected.connect(self.reject)\r\n        self.layout.addWidget(self.buttons)\r\n\r\n    def validate_and_accept(self):\r\n        \"\"\"Validate all fields before accepting the dialog\"\"\"\r\n        try:\r\n            # Validate all spinbox values\r\n            for neuron, widget in self.neuron_inputs.items():\r\n                if isinstance(widget, QtWidgets.QSpinBox):\r\n                    value = widget.value()\r\n                    if value < 0 or value > 100:\r\n                        QtWidgets.QMessageBox.warning(\r\n                            self, \"Invalid Value\", \r\n                            f\"{neuron} must be between 0 and 100\"\r\n                        )\r\n                        return\r\n                        \r\n            # If all validations pass, accept the dialog\r\n            self.accept()\r\n        except Exception as e:\r\n            QtWidgets.QMessageBox.critical(\r\n                self, \"Validation Error\", \r\n                f\"An error occurred during validation: {str(e)}\"\r\n            )\r\n\r\n    def get_stimulation_values(self):\r\n        \"\"\"Get stimulation values with proper type conversion\"\"\"\r\n        stimulation_values = {}\r\n        for neuron, input_widget in self.neuron_inputs.items():\r\n            if isinstance(input_widget, QtWidgets.QSpinBox):\r\n                stimulation_values[neuron] = input_widget.value()\r\n            elif isinstance(input_widget, QtWidgets.QComboBox):\r\n                text = input_widget.currentText()\r\n                if text.lower() == 'true':\r\n                    stimulation_values[neuron] = True\r\n                elif text.lower() == 'false':\r\n                    stimulation_values[neuron] = False\r\n                else:\r\n                    stimulation_values[neuron] = text\r\n        return stimulation_values\r\n\r\nclass RecentThoughtsDialog(QtWidgets.QDialog):\r\n    def __init__(self, thought_log, parent=None):\r\n        super().__init__(parent)\r\n        self.setWindowTitle(\"Recent Decisions\")\r\n        self.thought_log = thought_log\r\n\r\n        layout = QtWidgets.QVBoxLayout()\r\n        self.setLayout(layout)\r\n\r\n        # List widget to display recent thoughts\r\n        self.thought_list = QtWidgets.QListWidget()\r\n        self.thought_list.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)\r\n        layout.addWidget(self.thought_list)\r\n\r\n        # Populate the list with summarized thought logs\r\n        for log in self.thought_log:\r\n            summary = f\"Time: {log.get('timestamp', 'Unknown')} - Decision: {log.get('decision', 'Unknown')}\"\r\n            self.thought_list.addItem(summary)\r\n\r\n        # Button layout\r\n        button_layout = QtWidgets.QHBoxLayout()\r\n\r\n        # Save button\r\n        self.save_button = QtWidgets.QPushButton(\"Save Selected\")\r\n        self.save_button.clicked.connect(self.save_selected_thoughts)\r\n        button_layout.addWidget(self.save_button)\r\n\r\n        # Clear button\r\n        self.clear_button = QtWidgets.QPushButton(\"Clear\")\r\n        self.clear_button.clicked.connect(self.clear_all_logs)\r\n        button_layout.addWidget(self.clear_button)\r\n\r\n        layout.addLayout(button_layout)\r\n\r\n    def save_selected_thoughts(self):\r\n        selected_items = self.thought_list.selectedItems()\r\n        if not selected_items:\r\n            QtWidgets.QMessageBox.information(self, \"No Selection\", \"No decisions selected to save.\")\r\n            return\r\n\r\n        # Get the file name to save the selected thoughts\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Save Selected decisions\", \"\", \"Text Files (*.txt)\")\r\n        if file_name:\r\n            with open(file_name, 'w') as file:\r\n                for item in selected_items:\r\n                    file.write(item.text() + \"\\n\")\r\n            QtWidgets.QMessageBox.information(self, \"Save Successful\", f\"Selected decisions saved to {file_name}\")\r\n\r\n    def clear_all_logs(self):\r\n        # Confirm before clearing\r\n        reply = QtWidgets.QMessageBox.question(\r\n            self, 'Clear Logs', \r\n            \"Are you sure you want to clear all decision logs?\", \r\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No\r\n        )\r\n\r\n        if reply == QtWidgets.QMessageBox.Yes:\r\n            # Clear the logs in the parent window\r\n            if hasattr(self.parent(), 'thought_log'):\r\n                self.parent().thought_log.clear()\r\n                self.thought_list.clear()\r\n                QtWidgets.QMessageBox.information(self, \"Logs Cleared\", \"All decision logs have been cleared.\")\r\n\r\nclass LogWindow(QtWidgets.QWidget):\r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        self.setWindowTitle(\"Learning Log\")\r\n        self.resize(640, 480)\r\n\r\n        layout = QtWidgets.QVBoxLayout()\r\n        self.setLayout(layout)\r\n\r\n        self.log_text = QtWidgets.QTextEdit()\r\n        self.log_text.setReadOnly(True)\r\n        layout.addWidget(self.log_text)\r\n\r\n        self.export_button = QtWidgets.QPushButton(\"Export Log\")\r\n        self.export_button.clicked.connect(self.export_log)\r\n        layout.addWidget(self.export_button)\r\n\r\n    def update_log(self, text):\r\n        self.log_text.append(text)\r\n\r\n    def export_log(self):\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Export Log\", \"\", \"Text Files (*.txt)\")\r\n        if file_name:\r\n            with open(file_name, 'w') as f:\r\n                f.write(self.log_text.toPlainText())\r\n            QtWidgets.QMessageBox.information(self, \"Export Successful\", f\"Log exported to {file_name}\")\r\n\r\nclass DiagnosticReportDialog(QtWidgets.QDialog):\r\n    def __init__(self, brain_widget, parent=None):\r\n        super().__init__(parent)\r\n        self.setWindowTitle(\"Network Health Diagnosis\")\r\n        self.setMinimumSize(640, 800)\r\n        \r\n        self.brain_widget = brain_widget\r\n        self.history_data = parent.tamagotchi_logic.get_health_history() if hasattr(parent, 'tamagotchi_logic') else []\r\n        \r\n        self.layout = QtWidgets.QVBoxLayout()\r\n        self.setLayout(self.layout)\r\n        \r\n        # Create tab widget\r\n        self.tabs = QtWidgets.QTabWidget()\r\n        self.layout.addWidget(self.tabs)\r\n        \r\n        # Create report tabs\r\n        self.create_connections_tab()\r\n        self.create_neurons_tab()\r\n        self.create_balance_tab()\r\n        \r\n        # Add history graph section\r\n        self.create_history_section()\r\n        \r\n        # Add close button\r\n        self.close_button = QtWidgets.QPushButton(\"Close\")\r\n        self.close_button.clicked.connect(self.close)\r\n        self.layout.addWidget(self.close_button)\r\n    \r\n    def create_connections_tab(self):\r\n        tab = QtWidgets.QWidget()\r\n        layout = QtWidgets.QVBoxLayout(tab)\r\n        \r\n        # Get the weakest connections\r\n        weakest = self.brain_widget.get_weakest_connections()\r\n        \r\n        # Connections group\r\n        connections_group = QtWidgets.QGroupBox(\"Weakest Connections\")\r\n        connections_layout = QtWidgets.QVBoxLayout()\r\n        \r\n        # Create the table\r\n        table = QtWidgets.QTableWidget()\r\n        table.setColumnCount(3)\r\n        table.setHorizontalHeaderLabels([\"Source\", \"Target\", \"Weight\"])\r\n        \r\n        # Populate table with weakest connections\r\n        table.setRowCount(len(weakest))\r\n        for i, conn_data in enumerate(weakest):\r\n            try:\r\n                # Handle different possible return formats\r\n                if isinstance(conn_data, tuple) and len(conn_data) == 2 and isinstance(conn_data[0], tuple):\r\n                    # Format: ((source, target), weight)\r\n                    (a, b), weight = conn_data\r\n                elif isinstance(conn_data, tuple) and len(conn_data) == 3:\r\n                    # Format: (source, target, weight)\r\n                    a, b, weight = conn_data\r\n                else:\r\n                    # Unknown format, skip\r\n                    continue\r\n                    \r\n                table.setItem(i, 0, QtWidgets.QTableWidgetItem(str(a)))\r\n                table.setItem(i, 1, QtWidgets.QTableWidgetItem(str(b)))\r\n                table.setItem(i, 2, QtWidgets.QTableWidgetItem(f\"{weight:.3f}\"))\r\n                \r\n                # Color code based on weight\r\n                color = QtGui.QColor(\"green\" if weight > 0 else \"red\")\r\n                table.item(i, 2).setForeground(color)\r\n                \r\n            except Exception as e:\r\n                print(f\"Error processing connection: {conn_data}, Error: {e}\")\r\n                continue\r\n        \r\n        connections_layout.addWidget(table)\r\n        connections_group.setLayout(connections_layout)\r\n        layout.addWidget(connections_group)\r\n        \r\n        # Add to tabs\r\n        self.tabs.addTab(tab, \"Connections\")\r\n    \r\n    def create_neurons_tab(self):\r\n        tab = QtWidgets.QWidget()\r\n        layout = QtWidgets.QVBoxLayout()\r\n        \r\n        label = QtWidgets.QLabel(\"<h3>Neuron Activity Report</h3>\")\r\n        layout.addWidget(label)\r\n        \r\n        extremes = self.brain_widget.get_extreme_neurons(3)\r\n        report_text = \"OVERACTIVE NEURONS:\\n\"\r\n        for name, val in extremes['overactive']:\r\n            report_text += f\"{name}: {val:.0f}%\\n\"\r\n        \r\n        report_text += \"\\nUNDERACTIVE NEURONS:\\n\"\r\n        for name, val in extremes['underactive']:\r\n            report_text += f\"{name}: {val:.0f}%\\n\"\r\n        \r\n        text_edit = QtWidgets.QTextEdit()\r\n        text_edit.setPlainText(report_text)\r\n        text_edit.setReadOnly(True)\r\n        layout.addWidget(text_edit)\r\n        \r\n        tab.setLayout(layout)\r\n        self.tabs.addTab(tab, \"Neurons\")\r\n    \r\n    def create_balance_tab(self):\r\n        tab = QtWidgets.QWidget()\r\n        layout = QtWidgets.QVBoxLayout()\r\n        \r\n        label = QtWidgets.QLabel(\"<h3>Connection Balance Report</h3>\")\r\n        layout.addWidget(label)\r\n        \r\n        unbalanced = self.brain_widget.get_unbalanced_connections(5)\r\n        report_text = \"UNBALANCED CONNECTIONS:\\n\\n\"\r\n        for (a, b), (w1, w2), diff in unbalanced:\r\n            report_text += f\"{a}→{b}: {w1:.2f}\\n\"\r\n            report_text += f\"{b}→{a}: {w2:.2f} (Δ{diff:.2f})\\n\\n\"\r\n        \r\n        text_edit = QtWidgets.QTextEdit()\r\n        text_edit.setPlainText(report_text)\r\n        text_edit.setReadOnly(True)\r\n        layout.addWidget(text_edit)\r\n        \r\n        tab.setLayout(layout)\r\n        self.tabs.addTab(tab, \"Balance\")\r\n    \r\n    def create_history_section(self):\r\n        group = QtWidgets.QGroupBox(\"Health History\")\r\n        layout = QtWidgets.QVBoxLayout()\r\n        \r\n        # Add toggle checkbox\r\n        #self.show_history_check = QtWidgets.QCheckBox(\"Show historical trends\")\r\n        #self.show_history_check.toggled.connect(self.toggle_history_graph)\r\n        #layout.addWidget(self.show_history_check)\r\n        \r\n        # Placeholder for graph\r\n        #self.history_graph = QtWidgets.QLabel(\"Graph will appear here when enabled\")\r\n        #self.history_graph.setAlignment(QtCore.Qt.AlignCenter)\r\n        #self.history_graph.setMinimumHeight(200)\r\n        #layout.addWidget(self.history_graph)\r\n        \r\n        #group.setLayout(layout)\r\n        self.layout.addWidget(group)\r\n    \r\n    def toggle_history_graph(self, checked):\r\n        if checked and self.history_data:\r\n            # In a real implementation, you'd generate an actual graph here\r\n            timestamps = [x[0] for x in self.history_data]\r\n            values = [x[1] for x in self.history_data]\r\n            \r\n            # This is placeholder - you'd use matplotlib or similar in practice\r\n            graph_text = \"HEALTH TREND:\\n\\n\"\r\n            for t, v in zip(timestamps[-10:], values[-10:]):\r\n                graph_text += f\"{t}: {'='*int(v/10)}{v:.0f}%\\n\"\r\n            \r\n            self.history_graph.setText(graph_text)\r\n        else:\r\n            self.history_graph.setText(\"Graph will appear here when enabled\")"
  },
  {
    "path": "src/brain_learning_tab.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom .brain_base_tab import BrainBaseTab\nimport random\nimport time\nfrom .localisation import Localisation\n\ntry:\n    from display_scaling import DisplayScaling\nexcept ImportError:\n    class DisplayScaling:\n        @classmethod\n        def font_size(cls, size): return size\n        @classmethod\n        def scale_css(cls, css): return css\n\nclass NeuralNetworkVisualizerTab(BrainBaseTab):\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\n\n        # Ensure brain_widget is not None\n        if brain_widget is None:\n            print(\"WARNING: Brain widget is None. Creating a placeholder.\")\n            from .brain_widget import BrainWidget\n            brain_widget = BrainWidget()\n\n        # Call parent's __init__\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\n\n        # Explicit attribute initialization\n        self.countdown_label = None\n        self.countdown_timer = None\n        self.tab_widget = None\n        self.edu_views = {}\n        self.current_countdown = 30\n        self.learning_history = []\n        self.recent_pairs = []\n\n        # Card display areas\n        self.learning_scroll = None\n        self.learning_content = None\n        self.learning_content_layout = None\n\n        # Blink timer for Hebbian label\n        self.blink_timer = QtCore.QTimer(self)\n        self.blink_timer.setInterval(500)  # 0.5 s toggle\n        self.blink_timer.timeout.connect(self._blink_hebbian_label)\n        self._blink_visible = True\n\n        self.setup_ui()\n\n    def pre_load_data(self):\n        \"\"\"Pre-load data and initialize UI elements to make tab responsive on first click\"\"\"\n        # Update educational content for each tab\n        if hasattr(self, 'edu_views'):\n            for tab_name, edu_view in self.edu_views.items():\n                self.update_educational_content(tab_name=tab_name)\n\n        # Pre-initialize learning cards\n        if hasattr(self, 'learning_content_layout'):\n            # Add placeholder to initialize rendering\n            loc = Localisation.instance()\n            placeholder = self._create_info_card(\n                loc.get(\"learning_ready\"),\n                loc.get(\"learning_ready_desc\"),\n                \"#e3f2fd\"\n            )\n            self.learning_content_layout.addWidget(placeholder)\n\n        # Pre-compute any expensive operations\n        if hasattr(self, 'brain_widget') and hasattr(self.brain_widget, 'weights'):\n            sample_state = self.brain_widget.state.copy() if hasattr(self.brain_widget, 'state') else {}\n            self.update_from_brain_state(sample_state)\n\n    def setup_ui(self):\n        loc = Localisation.instance()\n        \n        # Remove existing widgets from the layout\n        if hasattr(self, '_layout'):\n            while self.layout.count():\n                item = self.layout.takeAt(0)\n                if item.widget():\n                    widget = item.widget()\n                    self.layout.removeWidget(widget)\n                    widget.deleteLater()\n\n        # Main vertical layout\n        main_layout = QtWidgets.QVBoxLayout()\n        self.layout.addLayout(main_layout)\n\n        # Create tab widget for different sections\n        self.tab_widget = QtWidgets.QTabWidget()\n        tab_font = QtGui.QFont()\n        tab_font.setPointSize(DisplayScaling.font_size(11))\n        self.tab_widget.setFont(tab_font)\n        self.tab_widget.setStyleSheet(f\"\"\"\n            QTabWidget::pane {{\n                border: 2px solid #e1e5eb;\n                border-radius: 12px;\n                background-color: #f8f9fa;\n            }}\n            QTabBar::tab {{\n                background: #f8f9fa;\n                border: 1px solid #e1e5eb;\n                padding: 10px 20px;\n                min-width: 120px;\n                margin-right: 5px;\n                border-top-left-radius: 8px;\n                border-top-right-radius: 8px;\n                font-size: {DisplayScaling.font_size(11)}pt;\n                color: #2c3e50;\n            }}\n            QTabBar::tab:selected {{\n                background: #ffffff;\n                border-bottom: none;\n                font-weight: 600;\n            }}\n            QTabBar::tab:hover {{\n                background: #e9ecef;\n            }}\n        \"\"\")\n\n        # ====== LEARNING PAIRS TAB ======\n        self.learning_tab = QtWidgets.QWidget()\n        learning_tab_layout = QtWidgets.QVBoxLayout(self.learning_tab)\n\n        # Header with title and Hebbian countdown\n        header_layout = QtWidgets.QHBoxLayout()\n        header_label = QtWidgets.QLabel(\n            f\"<h2 style='font-size: 24px; color: #2c3e50; font-weight: 600;'>{loc.get('active_learning_pairs')}</h2>\"\n        )\n        header_layout.addWidget(header_label)\n        header_layout.addStretch()\n\n        # Hebbian countdown timer label\n        self.hebbian_timer_label_learning = QtWidgets.QLabel(f\"{loc.get('hebbian_cycle')}: --\")\n        self.hebbian_timer_label_learning.setStyleSheet(\"\"\"\n            font-size: 16px;\n            font-weight: 600;\n            color: #2c3e50;\n            padding: 5px 10px;\n            background-color: #e3f2fd;\n            border-radius: 6px;\n            border: 1px solid #90caf9;\n        \"\"\")\n        header_layout.addWidget(self.hebbian_timer_label_learning)\n\n        learning_tab_layout.addLayout(header_layout)\n\n        # Scroll area for learning cards\n        self.learning_scroll = QtWidgets.QScrollArea()\n        self.learning_scroll.setWidgetResizable(True)\n        self.learning_scroll.setStyleSheet(\"\"\"\n            QScrollArea {\n                border: none;\n                background-color: #f8f9fa;\n            }\n        \"\"\")\n\n        self.learning_content = QtWidgets.QWidget()\n        self.learning_content_layout = QtWidgets.QVBoxLayout(self.learning_content)\n        self.learning_content_layout.setSpacing(15)\n        self.learning_content_layout.setContentsMargins(10, 10, 10, 10)\n        self.learning_content_layout.addStretch()\n\n        self.learning_scroll.setWidget(self.learning_content)\n        learning_tab_layout.addWidget(self.learning_scroll)\n\n        # ====== OVERVIEW TAB ======\n        overview_tab = QtWidgets.QWidget()\n        overview_layout = QtWidgets.QVBoxLayout(overview_tab)\n\n        overview_scroll = QtWidgets.QScrollArea()\n        overview_scroll.setWidgetResizable(True)\n        overview_content = QtWidgets.QWidget()\n        overview_content_layout = QtWidgets.QVBoxLayout(overview_content)\n\n        # Create overview cards\n        overview_card = self._create_educational_card(\n            loc.get(\"hebbian_overview\"),\n            f\"\"\"\n            <div style='font-size: 16px; line-height: 1.8; color: #4a5568;'>\n                <div style='background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 15px;'>\n                    <div style='font-size: 20px; font-weight: bold; color: #1976d2; margin-bottom: 10px;'>\n                        {loc.get(\"hebbian_quote\")}\n                    </div>\n                    <p>{loc.get(\"hebbian_principle\")}</p>\n                </div>\n\n                <p style='margin-bottom: 20px;'>\n                    {loc.get(\"hebbian_explanation\")}\n                </p>\n\n                <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0;'>\n                    <div style='background: #e8f5e9; padding: 15px; border-radius: 8px; border-left: 4px solid #4caf50;'>\n                        <div style='font-weight: bold; color: #2e7d32; margin-bottom: 5px; font-size: 18px;'>{loc.get(\"excitatory_connections\")}</div>\n                        <div style='font-size: 14px;'>{loc.get(\"excitatory_desc\")}</div>\n                    </div>\n\n                    <div style='background: #ffebee; padding: 15px; border-radius: 8px; border-left: 4px solid #f44336;'>\n                        <div style='font-weight: bold; color: #c62828; margin-bottom: 5px; font-size: 18px;'>{loc.get(\"inhibitory_connections\")}</div>\n                        <div style='font-size: 14px;'>{loc.get(\"inhibitory_desc\")}</div>\n                    </div>\n                </div>\n\n                <div style='background: #fff3e0; padding: 15px; border-radius: 8px; margin-top: 20px;'>\n                    <div style='font-weight: bold; color: #e65100; margin-bottom: 10px; font-size: 18px;'>{loc.get(\"in_practice_title\")}</div>\n                    <p style='margin: 0;'>\n                        {loc.get(\"in_practice_text\")}\n                    </p>\n                </div>\n            </div>\n            \"\"\",\n            \"#ffffff\"\n        )\n        overview_content_layout.addWidget(overview_card)\n\n        overview_scroll.setWidget(overview_content)\n        overview_layout.addWidget(overview_scroll)\n\n        # ====== MECHANICS TAB ======\n        mechanics_tab = QtWidgets.QWidget()\n        mechanics_layout = QtWidgets.QVBoxLayout(mechanics_tab)\n\n        mechanics_scroll = QtWidgets.QScrollArea()\n        mechanics_scroll.setWidgetResizable(True)\n        mechanics_content = QtWidgets.QWidget()\n        mechanics_content_layout = QtWidgets.QVBoxLayout(mechanics_content)\n\n        mechanics_card = self._create_educational_card(\n            loc.get(\"mechanics_title\"),\n            f\"\"\"\n            <div style='font-size: 16px; line-height: 1.8; color: #4a5568;'>\n                <p style='margin-bottom: 20px;'>\n                    {loc.get(\"mechanics_intro\")}\n                </p>\n\n                <div style='background: #f3e5f5; padding: 20px; border-radius: 8px; margin: 20px 0;'>\n                    <h3 style='color: #6a1b9a; margin: 0 0 15px 0; font-size: 20px;'>{loc.get(\"learning_rule_title\")}</h3>\n                    <div style='background: white; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 18px; text-align: center; margin-bottom: 15px;'>\n                        <b>Δw = η × x × y</b>\n                    </div>\n                    <p style='margin-bottom: 15px;'>{loc.get(\"where_label\")}</p>\n                    <ul style='list-style: none; padding: 0;'>\n                        <li style='margin-bottom: 12px; padding-left: 10px;'>\n                            {loc.get(\"delta_w_desc\")}\n                        </li>\n                        <li style='margin-bottom: 12px; padding-left: 10px;'>\n                            {loc.get(\"eta_desc\")}\n                        </li>\n                        <li style='margin-bottom: 12px; padding-left: 10px;'>\n                            {loc.get(\"activation_desc\")}\n                        </li>\n                    </ul>\n                </div>\n\n                <div style='background: #e8eaf6; padding: 20px; border-radius: 8px; margin: 20px 0;'>\n                    <h3 style='color: #3f51b5; margin: 0 0 15px 0; font-size: 20px;'>{loc.get(\"example_calc_title\")}</h3>\n                    <div style='background: white; padding: 15px; border-radius: 6px;'>\n                        <p style='margin: 0 0 10px 0;'>{loc.get(\"scenario_label\")}</p>\n                        <ul style='list-style: none; padding: 0; margin: 10px 0;'>\n                            <li>x = 1</li>\n                            <li>y = 1</li>\n                            <li>η = 0.1</li>\n                        </ul>\n                        <div style='background: #f5f5f5; padding: 10px; border-radius: 4px; margin: 10px 0;'>\n                            <b>Δw = 0.1 × 1 × 1 = 0.1</b>\n                        </div>\n                        <p style='margin: 10px 0 0 0;'>\n                            {loc.get(\"calc_result\")}\n                        </p>\n                    </div>\n                </div>\n\n                <div style='background: #fce4ec; padding: 20px; border-radius: 8px;'>\n                    <h3 style='color: #c2185b; margin: 0 0 15px 0; font-size: 20px;'>{loc.get(\"over_time_title\")}</h3>\n                    <p style='margin: 0;'>\n                        {loc.get(\"over_time_text\")}\n                    </p>\n                </div>\n            </div>\n            \"\"\",\n            \"#ffffff\"\n        )\n        mechanics_content_layout.addWidget(mechanics_card)\n\n        mechanics_scroll.setWidget(mechanics_content)\n        mechanics_layout.addWidget(mechanics_scroll)\n\n        # Add all tabs\n        self.tab_widget.addTab(self.learning_tab, loc.get(\"learning_pairs_tab\"))\n        self.tab_widget.addTab(overview_tab, loc.get(\"overview\"))\n        self.tab_widget.addTab(mechanics_tab, loc.get(\"mechanics_tab\"))\n\n        main_layout.addWidget(self.tab_widget)\n\n    def _create_educational_card(self, title, content, bg_color):\n        \"\"\"Create an educational information card\"\"\"\n        card = QtWidgets.QWidget()\n        card.setStyleSheet(f\"\"\"\n            QWidget {{\n                background-color: {bg_color};\n                border-radius: 12px;\n                border: 1px solid #e1e5eb;\n            }}\n        \"\"\")\n\n        card_layout = QtWidgets.QVBoxLayout(card)\n        card_layout.setContentsMargins(25, 25, 25, 25)\n\n        # Title\n        title_label = QtWidgets.QLabel(f\"<h2 style='color: #2c3e50; margin: 0 0 20px 0;'>{title}</h2>\")\n        card_layout.addWidget(title_label)\n\n        # Content\n        content_label = QtWidgets.QTextBrowser()\n        content_label.setHtml(content)\n        content_label.setOpenExternalLinks(True)\n        content_label.setStyleSheet(\"\"\"\n            QTextBrowser {\n                background-color: transparent;\n                border: none;\n                font-size: 16px;\n            }\n        \"\"\")\n        card_layout.addWidget(content_label)\n\n        return card\n\n    def _create_info_card(self, title, description, color):\n        \"\"\"Create a simple info card\"\"\"\n        card = QtWidgets.QWidget()\n        card.setStyleSheet(f\"\"\"\n            QWidget {{\n                background-color: {color};\n                border-radius: 10px;\n                padding: 15px;\n                border: 1px solid #dee2e6;\n            }}\n        \"\"\")\n\n        card_layout = QtWidgets.QVBoxLayout(card)\n        card_layout.setSpacing(8)\n\n        title_label = QtWidgets.QLabel(f\"<b style='font-size: {DisplayScaling.font_size(18)}px;'>{title}</b>\")\n        card_layout.addWidget(title_label)\n\n        desc_label = QtWidgets.QLabel(description)\n        desc_label.setWordWrap(True)\n        desc_label.setStyleSheet(f\"color: #495057; font-size: {DisplayScaling.font_size(14)}px;\")\n        card_layout.addWidget(desc_label)\n\n        return card\n\n    def _create_learning_pair_card(self, pair, weight, weight_change=None, stdp_meta=None):\n        \"\"\"Create a card displaying a learning pair\"\"\"\n        loc = Localisation.instance()\n        # Determine colors based on weight\n        if weight > 0.5:\n            border_color = \"#4caf50\"\n            bg_color = \"#e8f5e9\"\n            weight_color = \"#2e7d32\"\n            strength = loc.get(\"str_excitatory\")\n        elif weight > 0:\n            border_color = \"#8bc34a\"\n            bg_color = \"#f1f8e9\"\n            weight_color = \"#558b2f\"\n            strength = loc.get(\"weak_excitatory\")\n        elif weight > -0.5:\n            border_color = \"#ff9800\"\n            bg_color = \"#fff3e0\"\n            weight_color = \"#e65100\"\n            strength = loc.get(\"weak_inhibitory\")\n        else:\n            border_color = \"#f44336\"\n            bg_color = \"#ffebee\"\n            weight_color = \"#c62828\"\n            strength = loc.get(\"str_inhibitory\")\n\n        # Weight change indicator\n        change_indicator = \"\"\n        if weight_change == \"increase\":\n            change_indicator = f\"<span style='color: #4caf50; font-size: {DisplayScaling.font_size(24)}px; margin-left: 10px;'>↗</span>\"\n        elif weight_change == \"decrease\":\n            change_indicator = f\"<span style='color: #f44336; font-size: {DisplayScaling.font_size(24)}px; margin-left: 10px;'>↘</span>\"\n\n        # STDP: resolve directional arrow and LTP/LTD info\n        stdp_direction = stdp_meta.get('stdp_direction', 'none') if stdp_meta else 'none'\n        if stdp_direction == 'n1_to_n2':\n            arrow_char = \"→\"\n        elif stdp_direction == 'n2_to_n1':\n            arrow_char = \"←\"\n        else:\n            arrow_char = \"↔\"\n\n        card = QtWidgets.QWidget()\n        card.setStyleSheet(f\"\"\"\n            QWidget {{\n                background-color: {bg_color};\n                border-radius: 12px;\n                border: 2px solid {border_color};\n            }}\n            QWidget:hover {{\n                border: 3px solid {self.darken_color(border_color, 20)};\n                background-color: {self.darken_color(bg_color, 5)};\n            }}\n        \"\"\")\n\n        card_layout = QtWidgets.QVBoxLayout(card)\n        card_layout.setContentsMargins(25, 20, 25, 20)\n        card_layout.setSpacing(10)\n\n        # Top row - connection visualization\n        connection_layout = QtWidgets.QHBoxLayout()\n        connection_layout.setSpacing(20)\n\n        # Neuron 1 - large and prominent\n        neuron1_label = QtWidgets.QLabel(f\"<span style='font-size: {DisplayScaling.font_size(24)}px; font-weight: bold; color: #2c3e50;'>{pair[0].upper()}</span>\")\n        neuron1_label.setAlignment(QtCore.Qt.AlignCenter)\n        connection_layout.addWidget(neuron1_label, 1)\n\n        # Arrow and weight - centered\n        arrow_widget = QtWidgets.QWidget()\n        arrow_layout = QtWidgets.QVBoxLayout(arrow_widget)\n        arrow_layout.setSpacing(5)\n        arrow_layout.setContentsMargins(0, 0, 0, 0)\n\n        arrow_label = QtWidgets.QLabel(f\"<span style='font-size: {DisplayScaling.font_size(36)}px; color: #607d8b;'>{arrow_char}</span>\")\n        arrow_label.setAlignment(QtCore.Qt.AlignCenter)\n        arrow_layout.addWidget(arrow_label)\n\n        weight_display = QtWidgets.QLabel(\n            f\"<span style='font-size: {DisplayScaling.font_size(20)}px; font-weight: bold; color: {weight_color};'>{weight:.3f}</span>\"\n        )\n        weight_display.setAlignment(QtCore.Qt.AlignCenter)\n        arrow_layout.addWidget(weight_display)\n\n        connection_layout.addWidget(arrow_widget, 0)\n\n        # Neuron 2 - large and prominent\n        neuron2_label = QtWidgets.QLabel(f\"<span style='font-size: {DisplayScaling.font_size(24)}px; font-weight: bold; color: #2c3e50;'>{pair[1].upper()}</span>\")\n        neuron2_label.setAlignment(QtCore.Qt.AlignCenter)\n        connection_layout.addWidget(neuron2_label, 1)\n\n        card_layout.addLayout(connection_layout)\n\n        # STDP badge row (only when STDP contributed a timing signal)\n        if stdp_meta and stdp_direction != 'none':\n            is_ltp      = stdp_meta.get('is_ltp', False)\n            is_ltd      = stdp_meta.get('is_ltd', False)\n            stdp_delta  = stdp_meta.get('stdp_delta', 0.0)\n            stdp_weight = stdp_meta.get('stdp_weight', 0.0)\n\n            if is_ltp or is_ltd:\n                badge_row = QtWidgets.QHBoxLayout()\n                badge_row.setSpacing(8)\n\n                # LTP / LTD pill\n                if is_ltp:\n                    ltp_ltd_color  = \"#00897b\"\n                    ltp_ltd_bg     = \"#e0f2f1\"\n                    ltp_ltd_border = \"#80cbc4\"\n                    ltp_ltd_text   = \"⚡ LTP\"\n                else:\n                    ltp_ltd_color  = \"#c62828\"\n                    ltp_ltd_bg     = \"#ffebee\"\n                    ltp_ltd_border = \"#ef9a9a\"\n                    ltp_ltd_text   = \"⚡ LTD\"\n\n                badge = QtWidgets.QLabel(ltp_ltd_text)\n                badge.setStyleSheet(f\"\"\"\n                    font-size: {DisplayScaling.font_size(14)}px;\n                    font-weight: 700;\n                    color: {ltp_ltd_color};\n                    background: {ltp_ltd_bg};\n                    border: 1px solid {ltp_ltd_border};\n                    border-radius: 4px;\n                    padding: 3px 9px;\n                \"\"\")\n                badge_row.addWidget(badge)\n\n                # Delta value\n                sign = \"+\" if stdp_delta >= 0 else \"\"\n                delta_label = QtWidgets.QLabel(\n                    f\"<span style='font-size: {DisplayScaling.font_size(14)}px; color: #546e7a;'>\"\n                    f\"STDP Δ {sign}{stdp_delta:.4f} &nbsp;·&nbsp; \"\n                    f\"blend {int(stdp_weight * 100)}% spike-timing</span>\"\n                )\n                badge_row.addWidget(delta_label)\n                badge_row.addStretch()\n\n                card_layout.addLayout(badge_row)\n\n        # Bottom row - metadata\n        meta_layout = QtWidgets.QHBoxLayout()\n\n        strength_label = QtWidgets.QLabel(\n            f\"<span style='font-size: {DisplayScaling.font_size(16)}px; font-weight: 600; color: {weight_color};'>{strength}</span>{change_indicator}\"\n        )\n        meta_layout.addWidget(strength_label)\n\n        meta_layout.addStretch()\n\n        timestamp_label = QtWidgets.QLabel(\n            f\"<span style='color: #6c757d; font-size: {DisplayScaling.font_size(13)}px;'>{time.strftime('%H:%M:%S')}</span>\"\n        )\n        meta_layout.addWidget(timestamp_label)\n\n        card_layout.addLayout(meta_layout)\n\n        return card\n\n    def create_custom_button(self, text, callback, color, font_size=14):\n        \"\"\"Create a button with custom styling\"\"\"\n        button = QtWidgets.QPushButton(text)\n        button.clicked.connect(callback)\n        button.setStyleSheet(f\"\"\"\n            QPushButton {{\n                font-size: {font_size}px;\n                padding: 8px 16px;\n                border-radius: 6px;\n                font-weight: 500;\n                border: 2px solid {color};\n                background-color: {color};\n                color: white;\n                min-width: 100px;\n            }}\n            QPushButton:hover {{\n                background-color: {self.darken_color(color, 20)};\n                border: 2px solid {self.darken_color(color, 20)};\n            }}\n        \"\"\")\n        button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))\n        return button\n\n    def darken_color(self, hex_color, percent):\n        \"\"\"Darken a hex color by a percentage\"\"\"\n        hex_color = hex_color.lstrip('#')\n        r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)\n        r = max(0, int(r * (100 - percent) / 100))\n        g = max(0, int(g * (100 - percent) / 100))\n        b = max(0, int(b * (100 - percent) / 100))\n        return f'#{r:02x}{g:02x}{b:02x}'\n\n    def add_log_entry(self, message, pair=None, weight_change=None, stdp_meta=None):\n        \"\"\"Add a new learning pair card to the display\"\"\"\n        if pair and hasattr(self, 'learning_content_layout'):\n            # Remove placeholder if it exists\n            if self.learning_content_layout.count() == 1:\n                item = self.learning_content_layout.takeAt(0)\n                if item and item.widget():\n                    item.widget().deleteLater()\n\n            # Get weight\n            weight = getattr(self.brain_widget, 'weights', {}).get(pair, 0)\n\n            # Create card\n            card = self._create_learning_pair_card(pair, weight, weight_change, stdp_meta)\n\n            # Insert at the top (before stretch)\n            self.learning_content_layout.insertWidget(0, card)\n\n            # Keep only last 20 cards\n            while self.learning_content_layout.count() > 21:  # 20 cards + 1 stretch\n                item = self.learning_content_layout.takeAt(20)\n                if item and item.widget():\n                    item.widget().deleteLater()\n\n            # Update history\n            if pair not in self.learning_history:\n                self.learning_history.append(pair)\n            self.recent_pairs.append(pair)\n\n    def clear_log(self):\n        \"\"\"Clear all learning pair cards\"\"\"\n        loc = Localisation.instance()\n        if hasattr(self, 'learning_content_layout'):\n            while self.learning_content_layout.count() > 1:  # Keep the stretch\n                item = self.learning_content_layout.takeAt(0)\n                if item and item.widget():\n                    item.widget().deleteLater()\n\n        self.recent_pairs = []\n        self.learning_history = []\n\n        # Add info card\n        placeholder = self._create_info_card(\n            loc.get(\"log_cleared\"),\n            loc.get(\"log_cleared_desc\"),\n            \"#e3f2fd\"\n        )\n        self.learning_content_layout.insertWidget(0, placeholder)\n\n    def update_educational_content(self, pair=None, tab_name=None):\n        \"\"\"Update educational content - kept for compatibility\"\"\"\n        pass\n\n    def update_from_brain_state(self, state):\n        \"\"\"Update display based on brain state changes\"\"\"\n        if hasattr(self.brain_widget, 'recently_updated_neuron_pairs'):\n            for pair in self.brain_widget.recently_updated_neuron_pairs:\n                if pair not in self.learning_history:\n                    weight = getattr(self.brain_widget, 'weights', {}).get(pair, 0)\n\n                    # Determine weight change\n                    prev_weight = self.brain_widget.weights.get(pair, 0)\n                    weight_change = None\n                    if weight > prev_weight:\n                        weight_change = \"increase\"\n                    elif weight < prev_weight:\n                        weight_change = \"decrease\"\n\n                    self.add_log_entry(\"\", pair, weight_change)\n\n        # Sync Hebbian timer from brain_widget\n        if hasattr(self.brain_widget, 'hebbian_countdown_seconds'):\n            self.update_hebbian_label_learning(self.brain_widget.hebbian_countdown_seconds)\n\n    def update_hebbian_label_learning(self, value):\n        \"\"\"Update the Hebbian countdown label and handle blinking when <5s\"\"\"\n        loc = Localisation.instance()\n        if hasattr(self, 'hebbian_timer_label_learning'):\n            # Check if simulation is paused\n            is_paused = False\n            if hasattr(self, 'tamagotchi_logic') and hasattr(self.tamagotchi_logic, 'simulation_speed'):\n                is_paused = (self.tamagotchi_logic.simulation_speed == 0)\n            \n            # Display PAUSE if paused, otherwise show countdown\n            if is_paused:\n                self.hebbian_timer_label_learning.setText(f\"{loc.get('hebbian_cycle')}: {loc.get('hebbian_paused')}\")\n                # Stop blinking when paused\n                if self.blink_timer.isActive():\n                    self.blink_timer.stop()\n                self.hebbian_timer_label_learning.setVisible(True)\n            else:\n                self.hebbian_timer_label_learning.setText(f\"{loc.get('hebbian_cycle')}: {value}s\")\n                \n                # Start/stop blinking\n                if isinstance(value, int) and value < 5:\n                    if not self.blink_timer.isActive():\n                        self.blink_timer.start()\n                else:\n                    if self.blink_timer.isActive():\n                        self.blink_timer.stop()\n                    # Ensure visible when not blinking\n                    self.hebbian_timer_label_learning.setVisible(True)\n\n    def _blink_hebbian_label(self):\n        \"\"\"Toggle visibility of the Hebbian label to create blink effect\"\"\"\n        self._blink_visible = not self._blink_visible\n        self.hebbian_timer_label_learning.setVisible(self._blink_visible)"
  },
  {
    "path": "src/brain_memory_tab.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom .brain_base_tab import BrainBaseTab\nfrom .brain_ui_utils import UiUtils\nfrom .localisation import Localisation  # Import Localisation\nfrom datetime import datetime\n\n\nclass MemoryTab(BrainBaseTab):\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\n        # Initialize localisation\n        self.loc = Localisation.instance()\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\n        self.initialize_ui()\n        \n    def initialize_ui(self):\n        \"\"\"Initialize the memory tab with sub-tabs and card-based display\"\"\"\n        # Print debug info\n        print(f\"MemoryTab initialize_ui: tamagotchi_logic is {self.tamagotchi_logic is not None}\")\n        \n        # Create sub-tabs for different memory types\n        self.memory_subtabs = QtWidgets.QTabWidget()\n        self.memory_subtabs.setFont(QtGui.QFont(\"Arial\", 12))\n        \n        # Short-term memory tab\n        self.stm_tab = QtWidgets.QWidget()\n        self.stm_layout = QtWidgets.QVBoxLayout(self.stm_tab)\n        \n        # Long-term memory tab\n        self.ltm_tab = QtWidgets.QWidget()\n        self.ltm_layout = QtWidgets.QVBoxLayout(self.ltm_tab)\n        \n        # Overview tab\n        self.overview_tab = QtWidgets.QWidget()\n        self.overview_layout = QtWidgets.QVBoxLayout(self.overview_tab)\n        \n        # Add tabs with LOCALIZED names\n        # We keep the emojis but translate the text\n        self.memory_subtabs.addTab(self.stm_tab, f\"🧠 {self.loc.get('short_term_memory')}\")\n        self.memory_subtabs.addTab(self.ltm_tab, f\"📚 {self.loc.get('long_term_memory')}\")\n        self.memory_subtabs.addTab(self.overview_tab, f\"📊 {self.loc.get('overview')}\")\n        \n        # Configure STM tab\n        self.stm_scroll = QtWidgets.QScrollArea()\n        self.stm_scroll.setWidgetResizable(True)\n        self.stm_content = QtWidgets.QWidget()\n        self.stm_content_layout = QtWidgets.QVBoxLayout(self.stm_content)\n        self.stm_scroll.setWidget(self.stm_content)\n        self.stm_layout.addWidget(self.stm_scroll)\n        \n        # Configure LTM tab\n        self.ltm_header_label = QtWidgets.QLabel(\"it happened often...\")\n        ltm_header_font = self.ltm_header_label.font()\n        ltm_header_font.setItalic(True)\n        ltm_header_font.setPointSize(11)\n        self.ltm_header_label.setFont(ltm_header_font)\n        self.ltm_header_label.setAlignment(QtCore.Qt.AlignLeft)\n        self.ltm_header_label.setContentsMargins(6, 4, 0, 2)\n        self.ltm_layout.addWidget(self.ltm_header_label)\n\n        self.ltm_scroll = QtWidgets.QScrollArea()\n        self.ltm_scroll.setWidgetResizable(True)\n        self.ltm_content = QtWidgets.QWidget()\n        self.ltm_content_layout = QtWidgets.QVBoxLayout(self.ltm_content)\n        self.ltm_scroll.setWidget(self.ltm_content)\n        self.ltm_layout.addWidget(self.ltm_scroll)\n        \n        # Configure Overview tab\n        self.overview_stats = QtWidgets.QTextEdit()\n        self.overview_stats.setReadOnly(True)\n        self.overview_layout.addWidget(self.overview_stats)\n        \n        # Add memory subtabs to main memory tab layout, set to expand and fill\n        self.layout.addWidget(self.memory_subtabs)\n        self.layout.setStretchFactor(self.memory_subtabs, 1)\n        \n        # Initialize memory display\n        self.update_memory_display()\n        \n    def update_from_brain_state(self, state):\n        \"\"\"Update memory tab based on brain state changes\"\"\"\n        # Only update when state changes and tamagotchi_logic exists\n        if self.tamagotchi_logic is None:\n            print(\"Warning: tamagotchi_logic is None in update_from_brain_state - memory tab will not update\")\n            return\n            \n        self.update_memory_display()\n\n    def set_tamagotchi_logic(self, tamagotchi_logic):\n        \"\"\"Update the tamagotchi_logic reference and refresh memory display\"\"\"\n        super().set_tamagotchi_logic(tamagotchi_logic)\n        print(f\"MemoryTab.set_tamagotchi_logic: {tamagotchi_logic is not None}\")\n        \n        # Print debug info to verify squid and memory_manager\n        if tamagotchi_logic and hasattr(tamagotchi_logic, 'squid'):\n            print(f\"squid reference exists: {tamagotchi_logic.squid is not None}\")\n            if tamagotchi_logic.squid and hasattr(tamagotchi_logic.squid, 'memory_manager'):\n                print(f\"memory_manager exists\")\n            else:\n                print(f\"memory_manager doesn't exist\")\n        else:\n            print(f\"squid reference doesn't exist\")\n        \n        # Refresh the memory display if we have a valid reference chain\n        if (tamagotchi_logic and \n            hasattr(tamagotchi_logic, 'squid') and \n            tamagotchi_logic.squid and \n            hasattr(tamagotchi_logic.squid, 'memory_manager')):\n            self.update_memory_display()\n    \n    def update_memory_display(self):\n        \"\"\"Update all memory displays\"\"\"\n        try:\n            # Get short-term and long-term memories\n            if hasattr(self.tamagotchi_logic, 'squid') and hasattr(self.tamagotchi_logic.squid, 'memory_manager'):\n                # Get memories\n                stm = self.tamagotchi_logic.squid.memory_manager.get_all_short_term_memories()\n                ltm = self.tamagotchi_logic.squid.memory_manager.get_all_long_term_memories()\n                \n                # Filter displayable memories\n                stm_filtered = [m for m in stm if self._is_displayable_memory(m)]\n                ltm_filtered = [m for m in ltm if self._is_displayable_memory(m)]\n                \n                # De-duplicate memories based on category and key\n                stm_deduped = []\n                seen_keys = set()\n                for m in stm_filtered:\n                    key = (m.get('category', ''), m.get('key', ''))\n                    if key not in seen_keys:\n                        seen_keys.add(key)\n                        stm_deduped.append(m)\n                \n                ltm_deduped = []\n                seen_keys = set()\n                for m in ltm_filtered:\n                    key = (m.get('category', ''), m.get('key', ''))\n                    if key not in seen_keys:\n                        seen_keys.add(key)\n                        ltm_deduped.append(m)\n                \n                # Save scroll positions before rebuilding\n                stm_scroll_pos = self.stm_scroll.verticalScrollBar().value()\n                ltm_scroll_pos = self.ltm_scroll.verticalScrollBar().value()\n\n                # Clear existing content\n                self._clear_layout(self.stm_content_layout)\n                self._clear_layout(self.ltm_content_layout)\n                \n                # Add memory cards\n                for memory in stm_deduped:\n                    self._create_memory_widget(memory, self.stm_content_layout)\n                    \n                for memory in ltm_deduped:\n                    self._create_memory_widget(memory, self.ltm_content_layout)\n                \n                # Update overview\n                self._update_overview_stats(stm_deduped, ltm_deduped)\n                \n                # Force UI update\n                self.stm_content.update()\n                self.ltm_content.update()\n                \n                # Make sure the scroll areas show their content\n                self.stm_scroll.setWidget(self.stm_content)\n                self.ltm_scroll.setWidget(self.ltm_content)\n\n                # Restore scroll positions\n                self.stm_scroll.verticalScrollBar().setValue(stm_scroll_pos)\n                self.ltm_scroll.verticalScrollBar().setValue(ltm_scroll_pos)\n                \n        except Exception as e:\n            print(f\"Error updating memory tab: {e}\")\n            import traceback\n            traceback.print_exc()\n\n    def _clear_layout(self, layout):\n        \"\"\"Clear all widgets from the given layout\"\"\"\n        while layout.count():\n            item = layout.takeAt(0)\n            widget = item.widget()\n            if widget is not None:\n                widget.deleteLater()\n\n    def _is_displayable_memory(self, memory):\n        \"\"\"Check if a memory should be displayed in the UI\"\"\"\n        if not isinstance(memory, dict):\n            return False\n        if 'category' not in memory:\n            return False\n        if 'value' not in memory or memory.get('value') is None or memory.get('value') == \"\":\n            return False\n\n        cat = memory.get('category', '').lower()\n        key = memory.get('key', '')\n\n        # Always-show categories (no further filtering needed)\n        _ALWAYS_SHOW = {\n            'play', 'food', 'favourite_plant',\n            'mental_state', 'behaviour', 'behavior',\n            'environment', 'social', 'observation',\n            'travel', 'emotion', 'achievement', 'cleanliness',\n        }\n        if cat in _ALWAYS_SHOW:\n            return True\n\n        # interaction: skip raw dicts with None\n        if cat == 'interaction':\n            value = str(memory.get('value', ''))\n            if '{' in value and '}' in value and 'None' in value:\n                return False\n            return True\n\n        # decorations: skip timestamp-keyed entries\n        if cat == 'decorations':\n            if isinstance(key, str) and key.replace('.', '', 1).isdigit():\n                return False\n            formatted_value = memory.get('formatted_value', '')\n            if 'interaction with' in formatted_value.lower():\n                parts = formatted_value.split('with')\n                if len(parts) > 1:\n                    fname = parts[1].strip().split(':')[0].strip()\n                    if any(c.isdigit() for c in fname) and '.' in fname:\n                        return False\n            return True\n\n        # Filter out internal behavior status messages\n        if cat == 'behavior':\n            value = str(memory.get('value', '')).lower()\n            if any(s in value for s in ('returned to', 'status changed', 'after fleeing')):\n                return False\n            if len(value.split()) <= 3 and any(s in value for s in ('status', 'roaming', 'fleeing')):\n                return False\n\n        # Skip timestamp-like keys\n        if isinstance(key, str) and key.replace('.', '', 1).isdigit():\n            return False\n\n        # Skip timestamp-containing values\n        formatted_value = memory.get('formatted_value', '')\n        value = str(memory.get('value', ''))\n        if 'timestamp' in formatted_value.lower() or 'timestamp' in value.lower():\n            return False\n\n        if formatted_value:\n            return True\n        if value and not value.replace('.', '', 1).isdigit():\n            return True\n\n        return False\n\n\n    def add_test_memory(self):\n        \"\"\"Add a test memory to verify display mechanism\"\"\"\n        print(\"Adding test memory...\")\n        if hasattr(self.tamagotchi_logic, 'squid') and hasattr(self.tamagotchi_logic.squid, 'memory_manager'):\n            print(\"Found memory manager, creating test memory...\")\n            \n            # Create a test memory\n            formatted_value = \"Test memory: Happiness +10, Satisfaction +15\"\n            self.tamagotchi_logic.squid.memory_manager.add_short_term_memory(\n                'food', 'test_food', formatted_value, importance=10)\n            \n            # Verify memory was added\n            all_memories = self.tamagotchi_logic.squid.memory_manager.get_all_short_term_memories()\n            print(f\"Current memory count: {len(all_memories)}\")\n            \n            # Update the display\n            self.update_memory_display()\n            print(\"Test memory added and display updated\")\n        else:\n            print(\"ERROR: Could not find squid.memory_manager\")\n    \n    # ------------------------------------------------------------------ #\n    #  Thumbnail helper                                                    #\n    # ------------------------------------------------------------------ #\n    def _make_thumbnail(self, image_path, size, angle_deg=0, invert=False):\n        \"\"\"\n        Load *image_path*, scale to *size*×*size* (keeping aspect ratio),\n        then rotate by *angle_deg* degrees (small random tilt).\n        If *invert* is True the colours are flipped to white (for dark cards).\n        Returns a QLabel ready to drop into a layout, or None if the\n        image can't be loaded.\n        \"\"\"\n        import os as _os\n        if not image_path or not _os.path.exists(image_path):\n            return None\n\n        src = QtGui.QPixmap(image_path)\n        if src.isNull():\n            return None\n\n        scaled = src.scaled(size, size,\n                            QtCore.Qt.KeepAspectRatio,\n                            QtCore.Qt.SmoothTransformation)\n\n        if invert:\n            # Paint a white rectangle over the image using Difference blending,\n            # which flips every channel to its inverse while preserving alpha.\n            inverted = QtGui.QPixmap(scaled.size())\n            inverted.fill(QtCore.Qt.transparent)\n            painter = QtGui.QPainter(inverted)\n            painter.drawPixmap(0, 0, scaled)\n            painter.setCompositionMode(QtGui.QPainter.CompositionMode_Difference)\n            painter.fillRect(inverted.rect(), QtGui.QColor(255, 255, 255))\n            painter.end()\n            scaled = inverted\n\n        if angle_deg != 0:\n            # Rotate onto a transparent canvas large enough to avoid clipping\n            pad = int(size * 0.45)\n            canvas = QtGui.QPixmap(size + pad * 2, size + pad * 2)\n            canvas.fill(QtCore.Qt.transparent)\n            painter = QtGui.QPainter(canvas)\n            painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)\n            cx = canvas.width() / 2\n            cy = canvas.height() / 2\n            painter.translate(cx, cy)\n            painter.rotate(angle_deg)\n            painter.translate(-scaled.width() / 2, -scaled.height() / 2)\n            painter.drawPixmap(0, 0, scaled)\n            painter.end()\n            scaled = canvas\n\n        label = QtWidgets.QLabel()\n        label.setPixmap(scaled)\n        label.setFixedSize(scaled.size())\n        label.setAlignment(QtCore.Qt.AlignCenter)\n        return label\n\n    # ------------------------------------------------------------------ #\n    #  Effect-pills helper                                                 #\n    # ------------------------------------------------------------------ #\n    def _make_effect_pills(self, effects, scale_fn, font_size_fn):\n        \"\"\"Return a QWidget containing coloured stat pills, or None.\"\"\"\n        if not effects:\n            return None\n        _EMOJI = {\n            'happiness': '😊', 'satisfaction': '✨',\n            'anxiety': '😰', 'hunger': '🍖', 'energy': '⚡',\n            'cleanliness': '🚿', 'curiosity': '🔍',\n        }\n        pills_widget = QtWidgets.QWidget()\n        pills_layout = QtWidgets.QHBoxLayout(pills_widget)\n        pills_layout.setContentsMargins(0, 0, 0, 0)\n        pills_layout.setSpacing(scale_fn(4))\n        for stat, delta in effects.items():\n            try:\n                delta = float(delta)\n            except (TypeError, ValueError):\n                continue\n            sign = '+' if delta >= 0 else ''\n            emoji = _EMOJI.get(stat, '')\n            pill = QtWidgets.QLabel(f\"{emoji} {stat.capitalize()} {sign}{int(delta)}\")\n            color = '#D1FFD1' if delta > 0 else '#FFD1DC' if delta < 0 else '#FFFACD'\n            pill.setStyleSheet(\n                f\"background:{color}; border-radius:{scale_fn(8)}px;\"\n                f\" padding:2px {scale_fn(6)}px;\"\n                f\" font-size:{font_size_fn(11)}px;\"\n            )\n            pills_layout.addWidget(pill)\n        pills_layout.addStretch()\n        return pills_widget\n\n    # ------------------------------------------------------------------ #\n    #  Card metadata resolver                                              #\n    # ------------------------------------------------------------------ #\n    def _card_meta(self, memory):\n        \"\"\"\n        Return (header_str, thumb_path_or_None, content_str, effects_dict_or_None)\n        for every memory category the squid can produce.\n        \"\"\"\n        import os as _os\n        cat   = memory.get('category', '').lower()\n        key   = memory.get('key', '')\n        value = memory.get('value', '')\n\n        # ---- play -------------------------------------------------------\n        if cat == 'play' and isinstance(value, dict):\n            activity    = value.get('activity', '')\n            description = value.get('description', activity.replace('_', ' ').capitalize())\n            effects     = value.get('effects', {})\n            item_file   = value.get('item', '')   # filename stored at throw time\n            _TITLES = {\n                'rock_throwing':   'Rock Throwing',\n                'urchin_throwing': 'Urchin Throwing',\n                'poop_throwing':   'Poop Throwing',\n            }\n            _DEFAULT_THUMBS = {\n                'rock_throwing':   'images/decoration/rock01.png',\n                'urchin_throwing': 'images/decoration/rock03.png',\n                'poop_throwing':   'images/poop1.png',\n            }\n            # Prefer the actual item filename if it exists on disk\n            thumb = (item_file if item_file and _os.path.exists(item_file)\n                     else _DEFAULT_THUMBS.get(activity))\n            return (_TITLES.get(activity, 'Play'),\n                    thumb,\n                    description,\n                    effects)\n\n        # ---- food -------------------------------------------------------\n        if cat == 'food':\n            thumb = f'images/{key}.png'\n            return ('Eating', thumb, str(value), None)\n\n        # ---- favourite_plant (long-term) --------------------------------\n        if cat == 'favourite_plant':\n            plant_name = _os.path.splitext(_os.path.basename(key))[0]\n            lines = [plant_name]\n            if isinstance(value, dict):\n                if value.get('reason'):\n                    lines.append(value['reason'])\n                if value.get('anxiety_reduction'):\n                    lines.append('Reduces anxiety')\n            return ('Favourite Plant', key, '\\n'.join(lines), None)\n\n        # ---- interaction ------------------------------------------------\n        if cat == 'interaction':\n            if key == 'plant_contact' and isinstance(value, dict):\n                plant_path = value.get('plant_key', '')\n                plant_name = _os.path.splitext(_os.path.basename(plant_path))[0]\n                return ('Plant Contact', plant_path,\n                        f'Touched {plant_name} – feeling calmer', None)\n            if 'rock' in key:\n                item_path = value.get('item', '') if isinstance(value, dict) else ''\n                return ('Picked Up Rock', item_path or 'images/decoration/rock01.png',\n                        'Picked up a rock', None)\n            if 'poop' in key:\n                item_path = value.get('item', '') if isinstance(value, dict) else ''\n                return ('Picked Up Poop', item_path or 'images/poop1.png',\n                        'Picked up some poop…', None)\n            return ('Interaction', None, str(value), None)\n\n        # ---- mental_state -----------------------------------------------\n        if cat == 'mental_state':\n            if key == 'startled':\n                return ('Startled!', 'images/startled.png', str(value), None)\n            return ('Mental State', None, str(value), None)\n\n        # ---- behaviour / behavior ---------------------------------------\n        if cat in ('behaviour', 'behavior'):\n            if key == 'ink_cloud':\n                return ('Ink Cloud!', 'images/inkcloud.png', str(value), None)\n            if key == 'startle_response':\n                return ('Startle Response', 'images/startled.png', str(value), None)\n            if key == 'calm_after_startle':\n                return ('Calmed Down', None, str(value), None)\n            return ('Behaviour', None, str(value), None)\n\n        # ---- environment ------------------------------------------------\n        if cat == 'environment':\n            if key == 'plant_calming_effect':\n                return ('Plant Calming', 'images/plant.png', str(value), None)\n            if key == 'window_enlarged':\n                return ('More Space!', None, str(value), None)\n            if key == 'window_reduced':\n                return ('Less Space', None, str(value), None)\n            return ('Environment', None, str(value), None)\n\n        # ---- decorations ------------------------------------------------\n        if cat == 'decorations':\n            thumb = key if (_os.path.exists(key) if key else False) else None\n            name  = _os.path.splitext(_os.path.basename(key))[0] if key else 'decoration'\n            if isinstance(value, dict):\n                content = ', '.join(\n                    f\"{k.capitalize()} {'+' if v >= 0 else ''}{v:.0f}\"\n                    for k, v in value.items() if isinstance(v, (int, float))\n                ) or str(value)\n            else:\n                content = str(value)\n            return (name.replace('_', ' ').capitalize(), thumb, content, None)\n\n        # ---- social -----------------------------------------------------\n        if cat == 'social':\n            _SOCIAL = {\n                'squid_meeting':        'Met Another Squid',\n                'squid_detection':      'Spotted a Squid',\n                'squid_lost':           'Lost Sight of Squid',\n                'decoration_exchange':  'Decoration Exchange',\n                'targeted':             'Targeted by Rock',\n            }\n            return (_SOCIAL.get(key, 'Social'), None, str(value), None)\n\n        # ---- observation ------------------------------------------------\n        if cat == 'observation':\n            if 'rock' in key:\n                return ('Saw Rock Thrown', 'images/decoration/rock01.png',\n                        str(value), None)\n            return ('Observation', None, str(value), None)\n\n        # ---- travel -----------------------------------------------------\n        if cat == 'travel':\n            _TRAVEL = {\n                'ate_on_trip':        'Ate on Trip',\n                'played_on_trip':     'Played on Trip',\n                'completed_journey':  'Journey Complete',\n            }\n            return (_TRAVEL.get(key, 'Travel'), None, str(value), None)\n\n        # ---- cleanliness ------------------------------------------------\n        if cat == 'cleanliness':\n            return ('Washed Clean', 'images/icons/clean.png', str(value), None)\n\n        # ---- emotion ----------------------------------------------------\n        if cat == 'emotion':\n            _EMO = {\n                'happy_return':     'Happy Return',\n                'calm_return':      'Calm Return',\n                'intense_curiosity': 'Intense Curiosity',\n                'fear':             'Fear',\n            }\n            _EMO_THUMBS = {\n                'intense_curiosity': 'images/curious.png',\n                'fear':              'images/startled.png',\n            }\n            return (_EMO.get(key, 'Emotion'),\n                    _EMO_THUMBS.get(key),\n                    str(value), None)\n\n        # ---- achievement ------------------------------------------------\n        if cat == 'achievement':\n            return (key.replace('_', ' ').capitalize(), None, str(value), None)\n\n        # ---- neurogenesis -----------------------------------------------\n        if cat == 'neurogenesis':\n            return ('Neurogenesis', None, str(value), None)\n\n        # ---- fallback ---------------------------------------------------\n        content = memory.get('formatted_value', str(value))\n        return (cat.capitalize(), None, content, None)\n\n    # ------------------------------------------------------------------ #\n    #  Card builder                                                        #\n    # ------------------------------------------------------------------ #\n    def _create_memory_widget(self, memory, target_layout):\n        \"\"\"Create a memory card widget and add it to the target layout\"\"\"\n        import random as _random\n        from .display_scaling import DisplayScaling\n\n        THUMB_SIZE  = DisplayScaling.scale(64)   # bigger thumbnails\n        MAX_ANGLE   = 12                          # ± degrees of random tilt\n\n        memory_widget = QtWidgets.QFrame()\n        memory_widget.setFrameStyle(QtWidgets.QFrame.Box | QtWidgets.QFrame.Raised)\n        memory_widget.setLineWidth(DisplayScaling.scale(2))\n\n        bg_color = self._get_memory_color(memory)\n        memory_widget.setStyleSheet(f\"background-color: {bg_color};\")\n        memory_widget.setMinimumHeight(DisplayScaling.scale(200))\n        memory_widget.setMinimumWidth(DisplayScaling.scale(300))\n        memory_widget.setMaximumHeight(DisplayScaling.scale(250))\n\n        card_layout = QtWidgets.QVBoxLayout(memory_widget)\n        card_layout.setSpacing(DisplayScaling.scale(4))\n\n        cat_key = memory.get('category', 'unknown').lower()\n\n        # --- resolve metadata -------------------------------------------\n        header_text, thumb_path, content_text, effects = self._card_meta(memory)\n\n        # --- header label -----------------------------------------------\n        header = QtWidgets.QLabel(header_text)\n        hfont = header.font()\n        hfont.setBold(True)\n        hfont.setPointSize(DisplayScaling.font_size(12))\n        header.setFont(hfont)\n        if cat_key == 'neurogenesis':\n            header.setStyleSheet(\"color: white;\")\n        card_layout.addWidget(header)\n\n        # --- thumbnail + content row ------------------------------------\n        row_widget = QtWidgets.QWidget()\n        row_layout = QtWidgets.QHBoxLayout(row_widget)\n        row_layout.setContentsMargins(0, 0, 0, 0)\n        row_layout.setSpacing(DisplayScaling.scale(8))\n\n        angle = _random.uniform(-MAX_ANGLE, MAX_ANGLE)\n        if cat_key == 'neurogenesis':\n            brain_label = QtWidgets.QLabel(\"🧠\")\n            bfont = brain_label.font()\n            bfont.setPointSize(DisplayScaling.font_size(32))\n            brain_label.setFont(bfont)\n            brain_label.setAlignment(QtCore.Qt.AlignCenter)\n            row_layout.addWidget(brain_label, alignment=QtCore.Qt.AlignVCenter)\n        else:\n            thumb_label = self._make_thumbnail(thumb_path, THUMB_SIZE, angle)\n            if thumb_label:\n                row_layout.addWidget(thumb_label, alignment=QtCore.Qt.AlignVCenter)\n\n        content_label = QtWidgets.QLabel(content_text)\n        content_label.setWordWrap(True)\n        cfont = content_label.font()\n        cfont.setPointSize(DisplayScaling.font_size(10))\n        content_label.setFont(cfont)\n        if cat_key == 'neurogenesis':\n            content_label.setStyleSheet(\"color: white;\")\n        row_layout.addWidget(content_label, stretch=1)\n        card_layout.addWidget(row_widget)\n\n        # --- effect pills -----------------------------------------------\n        pills = self._make_effect_pills(effects, DisplayScaling.scale,\n                                        DisplayScaling.font_size)\n        if pills:\n            card_layout.addWidget(pills)\n\n        # --- timestamp --------------------------------------------------\n        timestamp = memory.get('timestamp', '')\n        if isinstance(timestamp, (int, float)) and timestamp > 0:\n            from datetime import datetime\n            timestamp = datetime.fromtimestamp(timestamp).strftime(\"%H:%M:%S\")\n        elif isinstance(timestamp, str):\n            try:\n                from datetime import datetime\n                timestamp = datetime.fromisoformat(timestamp).strftime(\"%H:%M:%S\")\n            except Exception:\n                pass\n\n        time_label = QtWidgets.QLabel(f\"{self.loc.get('time_label')} {timestamp}\")\n        tfont = time_label.font()\n        tfont.setPointSize(DisplayScaling.font_size(9))\n        time_label.setFont(tfont)\n        if cat_key == 'neurogenesis':\n            time_label.setStyleSheet(\"color: #ADD8E6;\")\n        card_layout.addWidget(time_label, alignment=QtCore.Qt.AlignRight)\n\n        # --- importance star --------------------------------------------\n        if memory.get('importance', 0) >= 5:\n            imp_label = QtWidgets.QLabel(f\"⭐ {self.loc.get('important_label')}\")\n            imp_label.setStyleSheet(\n                f\"color:#FF5733; font-weight:bold;\"\n                f\" font-size:{DisplayScaling.font_size(9)}px;\"\n            )\n            card_layout.addWidget(imp_label, alignment=QtCore.Qt.AlignRight)\n\n        # --- wire up ----------------------------------------------------\n        target_layout.addWidget(memory_widget)\n        memory_widget.setToolTip(self._create_memory_tooltip(memory))\n        memory_widget.mousePressEvent = (\n            lambda event, mem=memory: self._on_memory_card_clicked(mem)\n        )\n\n        # Plant hover tint\n        if cat_key == 'favourite_plant':\n            plant_key = memory.get('key', '')\n            memory_widget.enterEvent = (\n                lambda event, k=plant_key: self._tint_scene_plant(k)\n            )\n            memory_widget.leaveEvent = (\n                lambda event, k=plant_key: self._untint_scene_plant(k)\n            )\n\n        return memory_widget\n    \n    def _tint_scene_plant(self, filename):\n        \"\"\"Apply a green tint to the matching plant decoration in the live scene.\"\"\"\n        item = self._find_scene_decoration(filename)\n        if item is None:\n            return\n        try:\n            original = item.original_pixmap or item.pixmap()\n            if original.isNull():\n                return\n            tinted = QtGui.QPixmap(original.size())\n            tinted.fill(QtCore.Qt.transparent)\n            painter = QtGui.QPainter(tinted)\n            painter.drawPixmap(0, 0, original)\n            painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceAtop)\n            painter.fillRect(tinted.rect(), QtGui.QColor(0, 200, 80, 120))  # semi-transparent green\n            painter.end()\n            item.setPixmap(tinted)\n        except Exception as e:\n            print(f\"[MemoryTab] Could not tint plant: {e}\")\n\n    def _untint_scene_plant(self, filename):\n        \"\"\"Restore the plant decoration in the live scene to its original pixmap.\"\"\"\n        item = self._find_scene_decoration(filename)\n        if item is None:\n            return\n        try:\n            original = item.original_pixmap\n            if original and not original.isNull():\n                item.setPixmap(original)\n        except Exception as e:\n            print(f\"[MemoryTab] Could not restore plant: {e}\")\n\n    def _find_scene_decoration(self, filename):\n        \"\"\"Return the ResizablePixmapItem in the scene whose filename matches.\"\"\"\n        try:\n            scene = self.tamagotchi_logic.user_interface.scene\n            for item in scene.items():\n                if hasattr(item, 'filename') and item.filename == filename:\n                    return item\n        except Exception:\n            pass\n        return None\n\n    def _get_memory_color(self, memory):\n        \"\"\"Determine the background color for a memory based on its valence\"\"\"\n        cat = memory.get('category', '').lower()\n        key = memory.get('key', '')\n        val = memory.get('value', '')\n\n        # Explicit prefix overrides\n        formatted_value = memory.get('formatted_value', str(val)).lower()\n        if formatted_value.startswith('positive:'):\n            return \"#D1FFD1\"\n        if formatted_value.startswith('negative:'):\n            return \"#FFD1DC\"\n\n        # Play activities\n        if cat == 'play' and isinstance(val, dict):\n            return \"#D1FFD1\" if val.get('is_positive', True) else \"#FFD1DC\"\n\n        # Food is always positive\n        if cat == 'food':\n            return \"#D1FFD1\"\n\n        # Favourite plant / plant calming\n        if cat == 'favourite_plant':\n            return \"#C8F7C5\"\n        if key == 'plant_calming_effect':\n            return \"#E0FFD1\"\n\n        # Plant contact = positive\n        if cat == 'interaction' and key == 'plant_contact':\n            return \"#E0FFD1\"\n\n        # Startled / scary events\n        if cat == 'mental_state' and key == 'startled':\n            return \"#FFD1DC\"\n        if cat in ('behaviour', 'behavior') and key in ('ink_cloud', 'startle_response'):\n            return \"#FFD1DC\"\n        if cat in ('behaviour', 'behavior') and key == 'calm_after_startle':\n            return \"#D1FFD1\"\n\n        # Environment\n        if cat == 'environment':\n            if key == 'window_enlarged':\n                return \"#D1FFD1\"\n            if key == 'window_reduced':\n                return \"#FFD1DC\"\n\n        # Social\n        if cat == 'social':\n            if key == 'targeted':\n                return \"#FFD1DC\"\n            return \"#E8F4FD\"  # pale blue\n\n        # Cleanliness always positive\n        if cat == 'cleanliness':\n            return \"#B8D4F0\"  # mid-tone cornflower blue — distinct from curiosity #E8F4FD\n\n        # Emotion sub-types\n        if cat == 'emotion':\n            if key == 'fear':\n                return \"#FFD1DC\"\n            if key == 'intense_curiosity':\n                return \"#E8F4FD\"  # pale blue\n            if 'happy' in key:\n                return \"#D1FFD1\"\n            return \"#FFFACD\"\n        if cat == 'achievement':\n            return \"#FFF3CD\"  # gold-ish\n\n        # Neurogenesis — royal blue\n        if cat == 'neurogenesis':\n            return \"#4169E1\"\n\n        # Numeric dict values – sum to decide\n        if isinstance(val, dict):\n            total = sum(float(v) for v in val.values() if isinstance(v, (int, float)))\n            if total > 0:\n                return \"#D1FFD1\"\n            if total < 0:\n                return \"#FFD1DC\"\n\n        return \"#FFFACD\"  # default pastel yellow\n\n    \n    def _create_memory_tooltip(self, memory):\n        \"\"\"Create detailed tooltip for a memory card with LOCALIZED labels\"\"\"\n        tooltip = \"<html><body style='white-space:pre'>\"\n        tooltip += f\"<b>{self.loc.get('category_label')}</b> {memory.get('category', self.loc.get('unknown'))}\\n\"\n        tooltip += f\"<b>{self.loc.get('key_label')}</b> {memory.get('key', self.loc.get('unknown'))}\\n\"\n        \n        timestamp = memory.get('timestamp', '')\n        if isinstance(timestamp, str):\n            try:\n                timestamp = datetime.fromisoformat(timestamp).strftime(\"%H:%M:%S\")\n            except:\n                timestamp = \"\"\n        \n        tooltip += f\"<b>{self.loc.get('time_label')}</b> {timestamp}\\n\"\n        \n        if 'importance' in memory:\n            # Reusing 'important_label' but as a header, or 'Importance' if added to loc\n            tooltip += f\"<b>Importance:</b> {memory.get('importance')}\\n\"\n        \n        if 'access_count' in memory:\n            tooltip += f\"<b>{self.loc.get('access_count')}</b> {memory.get('access_count')}\\n\"\n        \n        # Add full content\n        full_content = memory.get('formatted_value', str(memory.get('value', '')))\n        tooltip += f\"\\n<b>{self.loc.get('full_content')}</b>\\n{full_content}\\n\"\n        \n        # Add effects if present\n        if isinstance(memory.get('raw_value'), dict):\n            tooltip += f\"\\n<b>{self.loc.get('effects_label')}</b>\\n\"\n            for key, value in memory['raw_value'].items():\n                if isinstance(value, (int, float)):\n                    tooltip += f\"  {key}: {value:+.2f}\\n\"\n        \n        tooltip += \"</body></html>\"\n        return tooltip\n    \n    def _update_overview_stats(self, stm, ltm):\n        \"\"\"Update the overview tab with statistics\"\"\"\n        # Import datetime correctly at the top of your function\n        from datetime import datetime\n        \n        stats_html = \"\"\"\n        <style>\n            .stat-box { \n                background: #f8f9fa; \n                border-radius: 10px; \n                padding: 15px; \n                margin: 10px;\n            }\n            .stat-title { \n                font-size: 10pt; \n                color: #2c3e50; \n                margin-bottom: 10px;\n            }\n        </style>\n        \"\"\"\n        \n        # Memory counts - Localized Labels\n        stats_html += f\"\"\"\n        <div class=\"stat-box\">\n            <div class=\"stat-title\">📈 {self.loc.get('memory_stats')}</div>\n            <table>\n                <tr><td>{self.loc.get('short_term_memory')}:</td><td style=\"padding-left: 20px;\">{len(stm)}</td></tr>\n                <tr><td>{self.loc.get('long_term_memory')}:</td><td style=\"padding-left: 20px;\">{len(ltm)}</td></tr>\n            </table>\n        </div>\n        \"\"\"\n        \n        # Category breakdown\n        categories = {}\n        for m in stm + ltm:\n            cat = m.get('category', 'unknown')\n            categories[cat] = categories.get(cat, 0) + 1\n        \n        category_html = \"\\n\".join(\n            f\"<tr><td>{k}:</td><td style='padding-left: 20px;'>{v}</td></tr>\"\n            for k, v in sorted(categories.items())\n        )\n        \n        stats_html += f\"\"\"\n        <div class=\"stat-box\">\n            <div class=\"stat-title\">🗂️ {self.loc.get('categories')}</div>\n            <table>{category_html}</table>\n        </div>\n        \"\"\"\n        \n        # Fix: Convert timestamp to string format for consistent comparison\n        def get_timestamp_key(memory):\n            timestamp = memory.get('timestamp', '')\n            if isinstance(timestamp, datetime):  # Corrected: Use datetime instead of datetime.datetime\n                return timestamp.isoformat()  # Convert datetime to string\n            return str(timestamp)  # Ensure string format\n        \n        # Use the new key function for sorting\n        recent_memories = sorted(stm, key=get_timestamp_key, reverse=True)[:5]\n        \n        self.overview_stats.setHtml(stats_html)\n\n    def _update_memory_importance(self, memory):\n        \"\"\"Increase importance of displayed memory and check for transfer\"\"\"\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'squid'):\n            return\n\n        # Get memory manager\n        memory_manager = self.tamagotchi_logic.squid.memory_manager\n\n        # Only process short-term memories\n        all_stm = memory_manager.get_all_short_term_memories()\n        matching_memories = [m for m in all_stm if\n                            m.get('category') == memory.get('category') and\n                            m.get('key') == memory.get('key')]\n\n        if not matching_memories:\n            return\n\n        # Increase memory access count and importance\n        category = memory.get('category', '')\n        key = memory.get('key', '')\n\n        if hasattr(memory_manager, 'update_memory_importance'):\n            try:\n                # Increment importance by 1\n                memory_manager.update_memory_importance(category, key, 1)\n            except Exception as e:\n                print(f\"Error updating memory importance: {e}\")\n\n        # Check if memory meets transfer criteria\n        if self._should_transfer_to_long_term(memory):\n            try:\n                # Transfer memory to long-term\n                memory_manager.transfer_to_long_term_memory(category, key)\n                print(f\"Memory transferred to long-term: {category}, {key}\")\n            except Exception as e:\n                print(f\"Error transferring memory to long-term: {e}\")\n\n            # Update displays\n            self.update_memory_display()\n\n    def _should_transfer_to_long_term(self, memory):\n        \"\"\"Check if a memory should be transferred to long-term\"\"\"\n        # Criteria for transfer:\n        \n        # 1. Extremely high importance (>= 8) - require higher importance\n        if memory.get('importance', 0) >= 8:\n            return True\n        \n        # 2. Repeated access - memory has been accessed frequently (>= 4 times)\n        if memory.get('access_count', 0) >= 4:\n            return True\n        \n        # 3. Combination of moderately important and repeated access\n        if memory.get('importance', 0) >= 5 and memory.get('access_count', 0) >= 3:\n            return True\n        \n        # 4. Special categories that should be remembered long-term\n        if memory.get('category') == 'health' and memory.get('importance', 0) >= 6:\n            return True\n        \n        # Most play activities should not automatically go to long-term\n        # unless they're truly significant or repeated\n        if memory.get('category') == 'play':\n            # Only really exceptional play events go to long-term\n            return memory.get('importance', 0) >= 9\n        \n        return False\n    \n    def _on_memory_card_clicked(self, memory):\n        \"\"\"Handle memory card clicks - update importance and check for transfer\"\"\"\n        print(f\"Memory card clicked: {memory.get('category')}\")\n        self._update_memory_importance(memory)\n"
  },
  {
    "path": "src/brain_network_tab.py",
    "content": "import json\nimport time\nimport subprocess\nimport os\nimport sys\nfrom PyQt5 import QtCore, QtGui, QtWidgets\nfrom .brain_base_tab import BrainBaseTab\nfrom .brain_dialogs import StimulateDialog, DiagnosticReportDialog\nfrom .display_scaling import DisplayScaling\nfrom .laboratory import NeuronLaboratory\nfrom .animation_styles import get_available_styles, get_style_info\nfrom .localisation import Localisation\nfrom .compute_backend import get_backend\n\nclass NetworkTab(BrainBaseTab):\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None,\n                 config_manager=None, debug_mode=False):\n        super().__init__(parent, tamagotchi_logic, brain_widget, config_manager)\n        self.config_manager = config_manager   # store it\n        self.debug_mode = debug_mode\n        self.neurogenesis_timer_value = 0\n\n        # Initialize persistent storage for metrics\n        self._last_active_neurons = 0\n        self._last_total_neurons = 0\n        self._last_connections = 0\n\n        if not hasattr(self, 'layout') or self.layout is None:\n            self.layout = QtWidgets.QVBoxLayout(self)\n\n        self.initialize_ui()\n        self.setup_timers()\n        self.neuron_lab_dialog = None\n\n    # ------------------------------------------------------------------\n    #  UI BUILD\n    # ------------------------------------------------------------------\n    def initialize_ui(self):\n        \"\"\"\n        Build the complete Network-tab UI with metrics, timers, controls,\n        neurogenesis counters, checkboxes, and emergency status indicators.\n        \"\"\"\n        loc = Localisation.instance()\n\n        # --------------------  CLEAR ANY OLD LAYOUT  -------------------- \n        if self.layout is not None:\n            while self.layout.count():\n                item = self.layout.takeAt(0)\n                widget = item.widget()\n                if widget is not None:\n                    widget.deleteLater()\n                else:\n                    sub_layout = item.layout()\n                    if sub_layout is not None:\n                        self._clear_layout_recursively(sub_layout)\n\n        # --------------------  TOP METRICS BAR  -------------------- \n        self.metrics_bar = QtWidgets.QWidget()\n        self.metrics_bar.setStyleSheet(\"background-color: rgb(200, 200, 200);\")\n        self.metrics_bar.setFixedHeight(DisplayScaling.scale(50))\n        metrics_layout = QtWidgets.QHBoxLayout(self.metrics_bar)\n        metrics_layout.setContentsMargins(DisplayScaling.scale(10), 0,\n                                        DisplayScaling.scale(10), 0)\n        metrics_layout.setSpacing(DisplayScaling.scale(20))\n\n        metrics_font = QtGui.QFont()\n        metrics_font.setPointSize(DisplayScaling.font_size(10))\n\n        self.neurons_label = QtWidgets.QLabel(f\"{loc.get('stats_neurons')}: N/A\")\n        self.neurons_label.setAlignment(QtCore.Qt.AlignCenter)\n        self.neurons_label.setFont(metrics_font)\n        metrics_layout.addWidget(self.neurons_label)\n\n        self.connections_label = QtWidgets.QLabel(f\"{loc.get('stats_connections')}: N/A\")\n        self.connections_label.setAlignment(QtCore.Qt.AlignCenter)\n        self.connections_label.setFont(metrics_font)\n        metrics_layout.addWidget(self.connections_label)\n\n        self.health_label = QtWidgets.QLabel(f\"{loc.get('stats_health')}: N/A\")\n        self.health_label.setAlignment(QtCore.Qt.AlignCenter)\n        self.health_label.setFont(metrics_font)\n        metrics_layout.addWidget(self.health_label)\n\n        self.emergency_status_label = QtWidgets.QLabel()\n        self.emergency_status_label.setStyleSheet(f\"\"\"\n            QLabel {{\n                color: white;\n                background-color: #d32f2f;\n                padding: 5px;\n                border-radius: 5px;\n                font-weight: bold;\n                font-size: {DisplayScaling.font_size(10)}px;\n            }}\n        \"\"\")\n        self.emergency_status_label.setVisible(False)\n        self.emergency_status_label.setFixedWidth(DisplayScaling.scale(200))\n        metrics_layout.addWidget(self.emergency_status_label)\n\n        metrics_layout.addStretch()\n\n        # Hebbian timer display\n        timers_container = QtWidgets.QWidget()\n        timers_container.setStyleSheet(\"background-color: black; border-radius: 5px;\")\n        timers_layout = QtWidgets.QHBoxLayout(timers_container)\n        timers_layout.setContentsMargins(DisplayScaling.scale(10),\n                                    DisplayScaling.scale(5),\n                                    DisplayScaling.scale(10),\n                                    DisplayScaling.scale(5))\n        timers_layout.setSpacing(DisplayScaling.scale(10))\n\n        font_size_timers = DisplayScaling.font_size(10)\n        timer_font = QtGui.QFont()\n        timer_font.setPointSize(font_size_timers)\n\n        self.hebbian_timer_label = QtWidgets.QLabel(f\"{loc.get('hebbian_cycle')}: XX\")\n        self.hebbian_timer_label.setStyleSheet(\"color: white;\")\n        self.hebbian_timer_label.setFont(timer_font)\n        timers_layout.addWidget(self.hebbian_timer_label)\n\n        metrics_layout.addWidget(timers_container)\n        self.layout.insertWidget(0, self.metrics_bar)\n\n        # --------------------  MAIN CONTENT  -------------------- \n        main_content_widget = QtWidgets.QWidget()\n        main_content_layout = QtWidgets.QVBoxLayout(main_content_widget)\n\n        if self.brain_widget:\n            main_content_layout.addWidget(self.brain_widget, 1)\n\n        self.layout.addWidget(main_content_widget)\n\n        # --------------------  NEUROGENESIS COUNTERS (BOTTOM-RIGHT)  -------------------- \n        values_display_layout = QtWidgets.QHBoxLayout()\n        values_display_layout.addStretch(1)\n\n        values_container = QtWidgets.QWidget()\n        values_box = QtWidgets.QHBoxLayout(values_container)\n        values_box.setContentsMargins(10, 5, 10, 5)\n        values_box.setSpacing(10)\n\n        font_size_values = DisplayScaling.font_size(10)\n        value_font = QtGui.QFont(\"Arial\", font_size_values)\n        value_font.setBold(True)\n\n        # Add the global cooldown label\n        self.global_cooldown_label = QtWidgets.QLabel(f\"{loc.get('global_cooldown')}: 0.0s\")\n        self.global_cooldown_label.setFont(value_font)\n        values_box.addWidget(self.global_cooldown_label)\n\n        values_display_layout.addWidget(values_container)\n\n        # --------------------  BOTTOM BAR --------------------\n        bottom_bar = QtWidgets.QHBoxLayout()\n\n        # 1. STYLE DROPDOWN (Far Left)\n        self.anim_combo = QtWidgets.QComboBox()\n        # Populate with actual style names from animation_styles.py\n        style_info = get_style_info()  # [(name, display_name, description), ...]\n        for name, display_name, description in style_info:\n            self.anim_combo.addItem(display_name, name)  # Display \"Classic\", store \"classic\"\n            self.anim_combo.setItemData(self.anim_combo.count() - 1, description, QtCore.Qt.ToolTipRole)\n        \n        # Set current style from brain_widget\n        if self.brain_widget:\n            current_style = self.brain_widget.get_animation_style()\n            for i in range(self.anim_combo.count()):\n                if self.anim_combo.itemData(i) == current_style:\n                    self.anim_combo.setCurrentIndex(i)\n                    break\n        \n        self.anim_combo.currentIndexChanged.connect(self._change_animation_style)\n\n        # Shared small font for all bottom-bar controls\n        bottom_bar_font = QtGui.QFont()\n        bottom_bar_font.setPointSize(DisplayScaling.font_size(10))\n        self.anim_combo.setFont(bottom_bar_font)\n\n        style_label = QtWidgets.QLabel(loc.get(\"style_label\"))\n        style_label.setFont(bottom_bar_font)\n        bottom_bar.addWidget(style_label)\n        bottom_bar.addWidget(self.anim_combo)\n\n        # Spacing\n        bottom_bar.addSpacing(20)\n\n        # 2. CHECKBOXES (Center-Left)\n        self.checkbox_links = QtWidgets.QCheckBox(loc.get(\"chk_links\"))\n        self.checkbox_links.setFont(bottom_bar_font)\n        self.checkbox_links.setChecked(True)\n        if self.brain_widget:\n            self.checkbox_links.stateChanged.connect(self.brain_widget.toggle_links)\n            # Connect to neurogenesis signal to refresh links display when new neuron is created\n            self.brain_widget.neuronCreated.connect(self._on_neuron_created)\n        bottom_bar.addWidget(self.checkbox_links)\n\n        self.checkbox_weights = QtWidgets.QCheckBox(loc.get(\"chk_weights\"))\n        self.checkbox_weights.setFont(bottom_bar_font)\n        self.checkbox_weights.setChecked(False)\n        if self.brain_widget:\n            self.checkbox_weights.stateChanged.connect(self.brain_widget.toggle_weights)\n        bottom_bar.addWidget(self.checkbox_weights)\n\n        self.checkbox_pruning = QtWidgets.QCheckBox(loc.get(\"chk_pruning\"))\n        self.checkbox_pruning.setFont(bottom_bar_font)\n        self.checkbox_pruning.setChecked(True)\n        self.checkbox_pruning.stateChanged.connect(self.toggle_pruning)\n        bottom_bar.addWidget(self.checkbox_pruning)\n\n        # 3. STRETCH (Pushes everything else to the far right)\n        bottom_bar.addStretch()\n\n        # 4. LOAD BRAIN BUTTON (Far Right)\n        from .custom_brain_loader import add_load_brain_button\n        add_load_brain_button(self, bottom_bar)\n\n        self.layout.addLayout(bottom_bar)\n\n        # --------------------  FUNCTIONAL EXISTING HELPERS  -------------------- \n        self.update_metrics_display()\n        self.update_hebbian_label()\n\n    def showEvent(self, event):\n        \"\"\"Network tab became visible – start timer if wanted.\"\"\"\n        super().showEvent(event)\n\n    def hideEvent(self, event):\n        \"\"\"Network tab hidden – stop timer.\"\"\"\n        super().hideEvent(event)\n\n    # ------------------------------------------------------------------\n    #  MISC EXISTING HELPERS\n    # ------------------------------------------------------------------\n    def _clear_layout_recursively(self, layout_to_clear):\n        \"\"\"Helper to recursively clear a layout and delete its widgets.\"\"\"\n        if layout_to_clear is None:\n            return\n        while layout_to_clear.count():\n            item = layout_to_clear.takeAt(0)\n            widget = item.widget()\n            if widget is not None:\n                widget.deleteLater()\n            else:\n                sub_layout = item.layout()\n                if sub_layout is not None:\n                    self._clear_layout_recursively(sub_layout)\n\n    def _open_brain_designer(self):\n        \"\"\"\n        Switch the main window to Brain Designer mode.\n        Replaces the current view with the designer interface.\n        \"\"\"\n        # Find the main SquidBrainWindow\n        # self.parent() might be QTabWidget, its parent is SquidBrainWindow\n        window = self.window()\n        \n        if hasattr(window, 'switch_to_designer_mode'):\n            window.switch_to_designer_mode()\n        else:\n            print(\"❌ Could not find switch_to_designer_mode method on main window\")\n            # Fallback (legacy process launch)\n            try:\n                from .brain_designer_launcher import launch_brain_designer_process\n                launch_brain_designer_process()\n            except ImportError:\n                QtWidgets.QMessageBox.warning(self, \"Error\", \"Cannot launch Brain Designer.\")\n\n    def _change_animation_style(self, index):\n        \"\"\"\n        Handle animation style dropdown change.\n        Called when user selects a new style from the combo box.\n        \n        Args:\n            index: The index of the selected item in the combo box\n        \"\"\"\n        if not self.brain_widget:\n            return\n            \n        # Get the internal style name (stored as item data)\n        style_name = self.anim_combo.itemData(index)\n        if not style_name:\n            return\n        \n        # Use brain_widget's API for real-time style switching\n        success = self.brain_widget.set_animation_style(style_name)\n        \n        if success:\n            # Optionally persist to config for next session\n            if self.config_manager:\n                self.config_manager.set_animation_style(style_name)\n\n    def _on_neuron_created(self, neuron_name: str):\n        \"\"\"\n        Handle neurogenesis neuron creation event.\n        Toggles the 'Show links' checkbox OFF and then ON again after 2 seconds\n        to refresh the links display for the newly created neuron.\n        \n        Args:\n            neuron_name: The name of the newly created neuron\n        \"\"\"\n        if not hasattr(self, 'checkbox_links') or self.checkbox_links is None:\n            return\n        \n        # Only toggle if links are currently shown\n        if self.checkbox_links.isChecked():\n            # Turn off links\n            self.checkbox_links.setChecked(False)\n            \n            # Schedule turning links back on after 2 seconds\n            QtCore.QTimer.singleShot(2000, self._restore_links_checkbox)\n    \n    def _restore_links_checkbox(self):\n        \"\"\"Restore the links checkbox to checked state after delay.\"\"\"\n        if hasattr(self, 'checkbox_links') and self.checkbox_links is not None:\n            self.checkbox_links.setChecked(True)\n    def _update_backend_label(self):\n        \"\"\"Backend label removed from UI — no-op kept for call-site compatibility.\"\"\"\n        pass\n\n    def setup_timers(self):\n        # QTimer fires every second for global-cooldown updates and label refresh.\n        # The Hebbian countdown value is NOT owned here – it is read from\n        # brain_widget.hebbian_countdown_seconds (same source as the Learning tab).\n        self.hebbian_countdown = QtCore.QTimer(self)\n        self.hebbian_countdown.timeout.connect(self._on_every_second)\n        self.hebbian_countdown.start(1000)\n        self.update_hebbian_label()\n        self._update_global_cooldown_label()\n\n    def _on_every_second(self):\n        \"\"\"Called every second by the QTimer.\n\n        The Hebbian countdown is read from brain_widget.hebbian_countdown_seconds\n        – the same source the Learning tab uses – so both displays are always in sync.\n        This method no longer owns or mutates the counter value.\n        \"\"\"\n        # 1. Refresh Hebbian label from the authoritative source\n        self.update_hebbian_label()\n\n        # 2. Update global-cooldown counter\n        self._update_global_cooldown_label()\n\n    def update_hebbian_timer(self):\n        \"\"\"Kept for API compatibility. The counter is owned by brain_widget;\n        this method simply refreshes the display label.\"\"\"\n        self.update_hebbian_label()\n\n    def update_hebbian_label(self):\n        \"\"\"Refresh the Hebbian countdown label from brain_widget.hebbian_countdown_seconds\n        – the same source the Learning tab uses – ensuring both are always in sync.\"\"\"\n        loc = Localisation.instance()\n        if not hasattr(self, 'hebbian_timer_label'):\n            return\n\n        # Pause state check (mirrors Learning tab behaviour exactly)\n        is_paused = (\n            self.tamagotchi_logic is not None\n            and hasattr(self.tamagotchi_logic, 'simulation_speed')\n            and self.tamagotchi_logic.simulation_speed == 0\n        )\n        if is_paused:\n            self.hebbian_timer_label.setText(\n                f\"{loc.get('hebbian_cycle')}: {loc.get('hebbian_paused')}\"\n            )\n            return\n\n        # Read the authoritative value from brain_widget\n        value = getattr(\n            self.brain_widget, 'hebbian_countdown_seconds',\n            getattr(self.config, 'hebbian_cycle_seconds', 30)\n        )\n        self.hebbian_timer_label.setText(f\"{loc.get('hebbian_cycle')}: {value}\")\n\n    def update_metrics_display(self):\n        '''Update the metrics bar with current network statistics - single source of truth'''\n        loc = Localisation.instance()\n        if hasattr(self, 'brain_widget') and self.brain_widget:\n            # Get neuron positions directly from brain widget\n            neuron_positions = getattr(self.brain_widget, 'neuron_positions', {})\n            total_neurons = len(neuron_positions)\n            \n            # Get connection count from weights\n            weights_dict = getattr(self.brain_widget, 'weights', {})\n            connection_count = len(weights_dict)\n            \n            # Network health calculation\n            if hasattr(self.brain_widget, 'calculate_network_health'):\n                health_value = self.brain_widget.calculate_network_health()\n                health_percentage_str = f\"{health_value:.1f}%\" if isinstance(health_value, (int, float)) else \"N/A\"\n            else:\n                # Fallback health calculation based on connections per neuron\n                if total_neurons > 0:\n                    connections_per_neuron = connection_count / total_neurons if total_neurons > 0 else 0\n                    if connections_per_neuron < 1.5 and total_neurons > 7:\n                        health_percentage_str = \"Low Density\"\n                    else:\n                        health_percentage_str = \"Optimal\"\n                else:\n                    health_percentage_str = \"N/A\"\n        else:\n            total_neurons = 0\n            connection_count = 0\n            health_percentage_str = \"N/A\"\n\n        # Update labels with consistent data\n        if hasattr(self, 'neurons_label'):\n            self.neurons_label.setText(f\"{loc.get('stats_neurons')}: {total_neurons}\")\n        if hasattr(self, 'connections_label'):\n            self.connections_label.setText(f\"{loc.get('stats_connections')}: {connection_count}\")\n        if hasattr(self, 'health_label'):\n            self.health_label.setText(f\"{loc.get('stats_health')}: {health_percentage_str}\")\n            # Set color based on health status\n            if health_percentage_str == \"Optimal\":\n                self.health_label.setStyleSheet(\"font-weight: bold; color: green;\")\n            elif health_percentage_str == \"Low Density\":\n                self.health_label.setStyleSheet(\"font-weight: bold; color: orange;\")\n            else:\n                self.health_label.setStyleSheet(\"font-weight: bold; color: gray;\")\n\n        # Update global cooldown label\n        self._update_global_cooldown_label()\n\n    def update_from_brain_state(self, state):\n        self.update_metrics_display()\n\n        # -----  GLOBAL COOLDOWN  ----- \n        self._update_global_cooldown_label()\n\n        # functional-neuron stats\n        if hasattr(self.brain_widget, 'enhanced_neurogenesis'):\n            self._update_functional_neuron_stats()\n\n    def _update_global_cooldown_label(self):\n        \"\"\"Update the global cooldown label with the remaining time.\"\"\"\n        loc = Localisation.instance()\n        if not self.brain_widget:\n            self.global_cooldown_label.setText(f\"{loc.get('global_cooldown')}: N/A\")\n            return\n\n        eng = getattr(self.brain_widget, 'enhanced_neurogenesis', None)\n        if eng is None:\n            self.global_cooldown_label.setText(f\"{loc.get('global_cooldown')}: N/A\")\n            return\n\n        # This now returns the real cooldown value\n        remaining = eng.get_global_cooldown_remaining()\n        self.global_cooldown_label.setText(f\"{loc.get('global_cooldown')}: {remaining:.1f}s\")\n\n    def _create_functional_stats_area(self):\n        \"\"\"Creates the container for functional stats label and the two side-by-side emoji buttons.\"\"\"\n        loc = Localisation.instance()\n        \n        # Avoid creating multiple times\n        if hasattr(self, 'functional_stats_area') and self.functional_stats_area is not None:\n            return\n\n        # Wrapper that holds the green card + button container\n        self.functional_stats_area = QtWidgets.QWidget()\n        self.stats_and_button_layout = QtWidgets.QHBoxLayout(self.functional_stats_area)\n        self.stats_and_button_layout.setContentsMargins(0, 0, 0, 0)\n        self.stats_and_button_layout.setSpacing(10)\n\n        # 1. Functional-stats label (green card) — this is the target for overlay\n        self.functional_stats_label = QtWidgets.QLabel()\n        self.functional_stats_label.setWordWrap(True)\n        self.functional_stats_label.setStyleSheet(\"\"\"\n            QLabel {\n                background-color: #e8f5e9;\n                padding: 8px;\n                border-radius: 5px;\n                border: 1px solid #4CAF50;\n                margin: 5px 5px 5px 8px;\n            }\n        \"\"\")\n        stats_font = QtGui.QFont()\n        stats_font.setPointSize(DisplayScaling.font_size(11))\n        self.functional_stats_label.setFont(stats_font)\n        self.stats_and_button_layout.addWidget(self.functional_stats_label, 1)\n\n        # 2. Button container (single button for Brain Designer)\n        self.new_button_container = QtWidgets.QWidget()\n        self.new_button_container.setFixedSize(DisplayScaling.scale(100), DisplayScaling.scale(100))\n        btn_layout = QtWidgets.QHBoxLayout(self.new_button_container)\n        btn_layout.setContentsMargins(0, 0, 0, 0)\n        btn_layout.setSpacing(0)\n\n        # Brain Designer button (enlarged to fill space)\n        self.new_50x50_button = QtWidgets.QPushButton(\"🧠\")\n        self.new_50x50_button.setFixedSize(DisplayScaling.scale(100), DisplayScaling.scale(80))\n        self.new_50x50_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #e1f5fe;\n                border: 1px solid #029be5;\n                border-radius: 8px;\n                font-size: 24pt;  /* Slightly larger emoji */\n            }\n            QPushButton:hover {\n                background-color: #b3e5fc;\n            }\n        \"\"\")\n        self.new_50x50_button.clicked.connect(self._open_brain_designer)\n        self.new_50x50_button.setToolTip(loc.get(\"tooltip_brain_designer\"))\n        btn_layout.addWidget(self.new_50x50_button)\n\n        # Second emoji button (Experience Buffer)\n        self.buffer_button = QtWidgets.QPushButton(\"🔍\")\n        self.buffer_button.setFixedSize(DisplayScaling.scale(42), DisplayScaling.scale(42))\n        self.buffer_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #fff3e0;\n                border: 1px solid #ff8f00;\n                border-radius: 8px;\n                font-size: 20pt;\n            }\n            QPushButton:hover {\n                background-color: #ffe0b2;\n            }\n        \"\"\")\n        self.buffer_button.clicked.connect(self._show_experience_buffer)\n        self.buffer_button.setToolTip(loc.get(\"tooltip_experience_buffer\"))\n        #btn_layout.addWidget(self.buffer_button)\n\n        self.stats_and_button_layout.addWidget(self.new_button_container)\n\n        # === INSERT THE STATS AREA JUST ABOVE THE BOTTOM BAR ===\n        self.layout.insertWidget(self.layout.count() - 1, self.functional_stats_area)\n\n        # === CREATE THE BINDINGS OVERLAY TARGETING ONLY THE GREEN LABEL ===\n        try:\n            from .brain_network_tab_banners import BindingOverlay\n            self.binding_overlay = BindingOverlay(self, self.functional_stats_label)\n            self.binding_overlay.hide()  # Hidden until bindings are loaded\n        except ImportError as e:\n            print(\"Could not import BindingOverlay:\", e)\n            self.binding_overlay = None\n\n        # Initially hidden until we have functional neuron data\n        self.functional_stats_area.hide()\n\n    def flash_emergency_creation(self, neuron_name):\n        \"\"\"Show visual indicator of emergency neuron creation\"\"\"\n        loc = Localisation.instance()\n        if hasattr(self, 'emergency_status_label'):\n            self.emergency_status_label.setText(loc.get(\"emergency_alert\", name=neuron_name))\n            self.emergency_status_label.setVisible(True)\n            QtCore.QTimer.singleShot(5000, lambda: self.emergency_status_label.setVisible(False))\n\n    def _toggle_neuron_laboratory(self):\n        \"\"\"Toggles the visibility of the Neuron Laboratory dialog.\"\"\"\n        loc = Localisation.instance()\n        if not self.brain_widget:\n            QtWidgets.QMessageBox.warning(self, loc.get(\"msg_missing_brain\"), loc.get(\"msg_cannot_open_lab\"))\n            return\n\n        if self.neuron_lab_dialog is None:\n            # Instantiate the dialog only once\n            self.neuron_lab_dialog = NeuronLaboratory(self.brain_widget, parent=self)\n\n        if self.neuron_lab_dialog.isVisible():\n            self.neuron_lab_dialog.hide()\n        else:\n            self.neuron_lab_dialog.show()\n\n    def _update_functional_neuron_stats(self):\n        \"\"\"Display functional neuron statistics\"\"\"\n        loc = Localisation.instance()\n        if not hasattr(self.brain_widget, 'enhanced_neurogenesis'):\n            return\n\n        eng = self.brain_widget.enhanced_neurogenesis\n        functional_neurons = eng.functional_neurons\n\n        spec_counts = {}\n        total_utility = 0\n        total_activations = 0\n\n        for name, func_neuron in functional_neurons.items():\n            spec = func_neuron.specialization\n            spec_counts[spec] = spec_counts.get(spec, 0) + 1\n            total_utility += func_neuron.utility_score\n            total_activations += func_neuron.activation_count\n\n        # === Ensure stats area exists ===\n        if not hasattr(self, 'functional_stats_area') or self.functional_stats_area is None:\n            self._create_functional_stats_area()\n\n        # === Update text ===\n        if spec_counts:\n            avg_utility = total_utility / len(functional_neurons) if functional_neurons else 0\n            text = f\"<b>🧬 {loc.get('func_neurons_title')}:</b><br>\"\n            text += f\"<b>{loc.get('count_label')}:</b> {len(functional_neurons)} | \"\n            text += f\"<b>{loc.get('avg_utility_label')}:</b> {avg_utility:.2f} | \"\n            text += f\"<b>{loc.get('total_activations_label')}:</b> {total_activations}<br>\"\n            text += f\"<b>{loc.get('specialisations_label')}:</b> \"\n            spec_list = [f\"<i>{spec}</i>({count})\" for spec, count in sorted(spec_counts.items(), key=lambda x: x[1], reverse=True)]\n            text += \", \".join(spec_list)\n        else:\n            text = f\"<b>🧬 {loc.get('func_neurons_title')}:</b><br>\"\n            text += f\"<b>{loc.get('count_label')}:</b> 0 | \"\n            text += f\"<b>{loc.get('avg_utility_label')}:</b> N/A | \"\n            text += f\"<b>{loc.get('total_activations_label')}:</b> 0<br>\"\n            text += f\"<b>{loc.get('specialisations_label')}:</b> None\"\n\n        self.functional_stats_label.setText(text)\n        self.functional_stats_area.show()\n\n    # ------------------------------------------------------------------\n    #  PRE-EXISTING FUNCTIONALITY\n    # ------------------------------------------------------------------\n    def preload(self):\n        if hasattr(self, 'brain_widget') and self.brain_widget and hasattr(self.brain_widget, 'update'):\n            self.brain_widget.update()\n        if hasattr(self, 'checkbox_links'):\n            self.checkbox_links.setChecked(True)\n        if hasattr(self, 'checkbox_weights'):\n            self.checkbox_weights.setChecked(False)\n\n    def toggle_pruning(self, state):\n        if hasattr(self, 'brain_widget') and self.brain_widget and hasattr(self.brain_widget, 'toggle_pruning'):\n            enabled = (state == QtCore.Qt.Checked)\n            self.brain_widget.toggle_pruning(enabled)\n            if not enabled:\n                print(\"\\033[91mWARNING: Pruning disabled - neurogenesis unconstrained!\\033[0m\")\n\n    def stimulate_brain(self):\n        if not self.brain_widget:\n            return\n        dialog = StimulateDialog(self.brain_widget, self)\n        if dialog.exec_() == QtWidgets.QDialog.Accepted:\n            stimulation_values = dialog.get_stimulation_values()\n            if stimulation_values is not None and hasattr(self.brain_widget, 'update_state'):\n                self.brain_widget.update_state(stimulation_values)\n                if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'update_from_brain'):\n                    self.tamagotchi_logic.update_from_brain(stimulation_values)\n\n    def save_brain_state(self):\n        if not self.brain_widget:\n            return\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Save Brain State\", \"\", \"JSON Files (*.json)\")\n        if file_name:\n            state_to_save = {}\n            if hasattr(self.brain_widget, 'get_brain_state'):\n                state_to_save = self.brain_widget.get_brain_state()\n            elif hasattr(self.brain_widget, 'state'):\n                state_to_save = self.brain_widget.state\n            try:\n                with open(file_name, 'w') as f:\n                    json.dump(state_to_save, f, indent=4)\n            except Exception as e:\n                print(f\"Error saving brain state: {e}\")\n\n    def load_brain_state(self):\n        if not self.brain_widget:\n            return\n        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, \"Load Brain State\", \"\", \"JSON Files (*.json)\")\n        if file_name:\n            try:\n                with open(file_name, 'r') as f:\n                    state = json.load(f)\n                if hasattr(self.brain_widget, 'set_brain_state'):\n                    self.brain_widget.set_brain_state(state)\n                elif hasattr(self.brain_widget, 'update_state'):\n                    self.brain_widget.update_state(state)\n                \n                # Force disable and re-enable links after loading brain state\n                if hasattr(self, 'checkbox_links') and self.checkbox_links.isChecked():\n                    self.checkbox_links.setChecked(False)\n                    QtCore.QTimer.singleShot(1000, self._restore_links_checkbox)\n                \n                self.update_metrics_display()\n            except Exception as e:\n                print(f\"Error loading brain state: {e}\")\n\n    def show_diagnostic_report(self):\n        if not self.brain_widget:\n            return\n        dialog = DiagnosticReportDialog(self.brain_widget, self)\n        dialog.exec_()\n\n    def create_button(self, text, callback, color_hex):\n        button = QtWidgets.QPushButton(text)\n        button.clicked.connect(callback)\n\n        padding_val = DisplayScaling.scale(5)\n        btn_width = DisplayScaling.scale(150)   # Adjusted for potentially more buttons\n        btn_height = DisplayScaling.scale(40)   # Adjusted height\n        font_size_val = DisplayScaling.font_size(11)\n\n        button.setStyleSheet(f\"background-color: {color_hex}; border: 1px solid black; padding: {padding_val}px;\")\n        button.setFixedSize(btn_width, btn_height)\n        font = button.font()\n        font.setPointSize(font_size_val)\n        button.setFont(font)\n        return button\n\n    def _show_experience_buffer(self):\n        \"\"\"Show the Experience Buffer window when magnifying glass is clicked\"\"\"\n        loc = Localisation.instance()\n        if not self.brain_widget:\n            QtWidgets.QMessageBox.warning(self, loc.get(\"msg_missing_brain\"), loc.get(\"msg_cannot_open_buffer\"))\n            return\n\n        if not hasattr(self.brain_widget, 'enhanced_neurogenesis') or self.brain_widget.enhanced_neurogenesis is None:\n            QtWidgets.QMessageBox.warning(self, loc.get(\"msg_no_neurogenesis\"), loc.get(\"msg_neurogenesis_not_init\"))\n            return\n\n        # Create or show the dialog\n        if self.experience_buffer_dialog is None:\n            self.experience_buffer_dialog = ExperienceBufferDialog(self.brain_widget, self)\n        \n        self.experience_buffer_dialog.refresh_data()\n        self.experience_buffer_dialog.show()\n        self.experience_buffer_dialog.raise_()\n        self.experience_buffer_dialog.activateWindow()\n\n    def _show_decorations(self):\n        \"\"\"Show the Decorations window\"\"\"\n        loc = Localisation.instance()\n        # Navigate up the parent hierarchy to find the main window/UI object\n        # Handle both cases: parent as a method (PyQt default) or as an attribute\n        parent = self.parent() if callable(self.parent) else self.parent\n        \n        while parent is not None:\n            # Check if this parent has the decoration_window attribute\n            if hasattr(parent, 'decoration_window'):\n                parent.decoration_window.show()\n                parent.decoration_window.raise_()\n                parent.decoration_window.activateWindow()\n                return\n            # Get the next parent (handle both method and attribute cases)\n            parent = parent.parent() if callable(parent.parent) else parent.parent\n        \n        # If we couldn't find it, show a warning\n        QtWidgets.QMessageBox.warning(self, loc.get(\"msg_decorations_unavailable\"), \n                                     loc.get(\"msg_decorations_fail\"))\n\n\nclass ExperienceBufferDialog(QtWidgets.QDialog):\n    \"\"\"Floating window showing the experience buffer contents\"\"\"\n    \n    def __init__(self, brain_widget, parent=None):\n        super().__init__(parent)\n        self.brain_widget = brain_widget\n        self.loc = Localisation.instance()\n        self.setWindowTitle(self.loc.get(\"buffer_title\"))\n        self.setMinimumSize(800, 480)\n        self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint)\n        \n        self.setup_ui()\n        \n    def setup_ui(self):\n        \"\"\"Create the UI elements\"\"\"\n        layout = QtWidgets.QVBoxLayout(self)\n        \n        # Header with info\n        header = QtWidgets.QLabel(self.loc.get(\"buffer_header\"))\n        header.setStyleSheet(\"font-weight: bold; font-size: 12pt; padding: 5px;\")\n        layout.addWidget(header)\n        \n        # Create table to show experiences\n        self.table = QtWidgets.QTableWidget()\n        self.table.setColumnCount(4)\n        self.table.setHorizontalHeaderLabels([\n            self.loc.get(\"col_type\"),\n            self.loc.get(\"col_pattern\"),\n            self.loc.get(\"col_outcome\"),\n            self.loc.get(\"col_time\")\n        ])\n        \n        # Set column widths\n        self.table.setColumnWidth(0, 100)   # Type\n        self.table.setColumnWidth(1, 400)  # Pattern\n        self.table.setColumnWidth(2, 100)   # Outcome\n        self.table.setColumnWidth(3, 100)   # Time\n        \n        # Style the table\n        self.table.setStyleSheet(\"\"\"\n            QTableWidget {\n                background-color: white;\n                gridline-color: #ddd;\n            }\n            QHeaderView::section {\n                background-color: #3f51b5;\n                color: white;\n                padding: 5px;\n                font-weight: bold;\n            }\n        \"\"\")\n        \n        self.table.setAlternatingRowColors(True)\n        self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\n        \n        layout.addWidget(self.table)\n        \n        # Bottom info panel\n        self.info_label = QtWidgets.QLabel()\n        self.info_label.setStyleSheet(\"padding: 5px; background-color: #f5f5f5; border-radius: 3px;\")\n        layout.addWidget(self.info_label)\n        \n        # Refresh button\n        button_layout = QtWidgets.QHBoxLayout()\n        refresh_btn = QtWidgets.QPushButton(self.loc.get(\"btn_refresh\"))\n        refresh_btn.clicked.connect(self.refresh_data)\n        refresh_btn.setFixedSize(80, 30)\n        button_layout.addStretch()\n        button_layout.addWidget(refresh_btn)\n        layout.addLayout(button_layout)\n        \n    def refresh_data(self):\n        \"\"\"Update the table with current experience buffer data\"\"\"\n        if not hasattr(self.brain_widget, 'enhanced_neurogenesis') or self.brain_widget.enhanced_neurogenesis is None:\n            return\n            \n        eng = self.brain_widget.enhanced_neurogenesis\n        \n        if not hasattr(eng, 'experience_buffer'):\n            self.info_label.setText(\"⚠️ Experience buffer not available\")\n            return\n            \n        buffer = eng.experience_buffer\n        experiences = list(buffer.buffer)\n        \n        # Clear and populate table\n        self.table.setRowCount(len(experiences))\n        \n        for i, exp in enumerate(reversed(experiences)):  # Most recent first\n            # Type column\n            type_item = QtWidgets.QTableWidgetItem(exp.trigger_type.capitalize())\n            type_color = {\n                'novelty': '#2196F3',\n                'stress': '#F44336',\n                'reward': '#4CAF50'\n            }.get(exp.trigger_type, '#757575')\n            type_item.setForeground(QtGui.QColor(type_color))\n            type_item.setFont(QtGui.QFont(\"Arial\", 9, QtGui.QFont.Bold))\n            self.table.setItem(i, 0, type_item)\n            \n            # Pattern column\n            pattern = exp.get_pattern_signature()\n            pattern_item = QtWidgets.QTableWidgetItem(pattern)\n            pattern_item.setToolTip(pattern)\n            self.table.setItem(i, 1, pattern_item)\n            \n            # Outcome column\n            outcome_item = QtWidgets.QTableWidgetItem(exp.outcome.capitalize())\n            outcome_item.setForeground(QtGui.QColor('#4CAF50' if exp.outcome == 'positive' else '#757575'))\n            self.table.setItem(i, 2, outcome_item)\n            \n            # Time column (relative)\n            time_ago = int(time.time() - exp.timestamp)\n            if time_ago < 60:\n                time_str = f\"{time_ago}s ago\"\n            elif time_ago < 3600:\n                time_str = f\"{time_ago // 60}m ago\"\n            else:\n                time_str = f\"{time_ago // 3600}h ago\"\n            time_item = QtWidgets.QTableWidgetItem(time_str)\n            time_item.setForeground(QtGui.QColor('#757575'))\n            self.table.setItem(i, 3, time_item)\n        \n        # Update info label with pattern counts\n        pattern_counts = buffer.pattern_counts\n        top_patterns = sorted(pattern_counts.items(), key=lambda x: x[1], reverse=True)[:3]\n        \n        if top_patterns:\n            pattern_text = f\"{self.loc.get('top_patterns')}: \" + \" | \".join([f\"{p}: {c}\" for p, c in top_patterns])\n        else:\n            pattern_text = self.loc.get('no_patterns')\n            \n        self.info_label.setText(f\"📊 {self.loc.get('buffer_size')}: {len(experiences)}/{buffer.buffer.maxlen} | {pattern_text}\")"
  },
  {
    "path": "src/brain_network_tab_banners.py",
    "content": "from PyQt5 import QtWidgets, QtCore, QtGui\r\n\r\nclass BindingOverlay(QtWidgets.QWidget):\r\n    \"\"\"\r\n    Overlay banners that attach to the NetworkTab's functional stats area.\r\n    Displays loaded bindings and collapses to a 15px strip on click.\r\n    \"\"\"\r\n    \r\n    def __init__(self, network_tab, target_widget):\r\n        super().__init__(network_tab)\r\n        self.network_tab = network_tab\r\n        self.target_widget = target_widget\r\n        self.is_expanded = True\r\n        self.bindings = []\r\n        \r\n        # Auto-collapse state tracking - only auto-collapse the first time\r\n        self._first_auto_collapse_done = False\r\n        self._auto_collapse_timer = QtCore.QTimer(self)\r\n        self._auto_collapse_timer.setSingleShot(True)\r\n        self._auto_collapse_timer.timeout.connect(self._perform_auto_collapse)\r\n        \r\n        # CRITICAL FIX: Enable CSS background painting\r\n        self.setAttribute(QtCore.Qt.WA_StyledBackground, True)\r\n        \r\n        # UI Setup\r\n        self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)\r\n        self.hide() \r\n        \r\n        # Layout\r\n        self.main_layout = QtWidgets.QVBoxLayout(self)\r\n        self.main_layout.setContentsMargins(5, 5, 5, 5)\r\n        \r\n        # Content Container\r\n        self.content_widget = QtWidgets.QWidget()\r\n        self.content_layout = QtWidgets.QVBoxLayout(self.content_widget)\r\n        self.content_layout.setContentsMargins(0, 0, 0, 0)\r\n        self.content_layout.setSpacing(2)\r\n        \r\n        # Header\r\n        self.header_label = QtWidgets.QLabel(\"🔗 Bindings Loaded:\")\r\n        self.header_label.setStyleSheet(\"font-weight: bold; font-size: 10pt; color: #1b5e20;\")\r\n        self.content_layout.addWidget(self.header_label)\r\n        \r\n        # Scroll area\r\n        self.scroll_area = QtWidgets.QScrollArea()\r\n        self.scroll_area.setWidgetResizable(True)\r\n        self.scroll_area.setStyleSheet(\"\"\"\r\n            QScrollArea { background: transparent; border: none; }\r\n            QScrollBar:vertical { width: 10px; }\r\n        \"\"\")\r\n        # Allow scrolling if list exceeds banner height\r\n        self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\r\n        self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\r\n        \r\n        self.scroll_widget = QtWidgets.QWidget()\r\n        self.scroll_widget.setStyleSheet(\"background: transparent;\")\r\n        self.scroll_layout = QtWidgets.QVBoxLayout(self.scroll_widget)\r\n        self.scroll_layout.setContentsMargins(2, 0, 0, 0)\r\n        self.scroll_layout.setSpacing(4)\r\n        \r\n        self.scroll_area.setWidget(self.scroll_widget)\r\n        self.content_layout.addWidget(self.scroll_area)\r\n        \r\n        self.main_layout.addWidget(self.content_widget)\r\n        \r\n        # Visual Style - Sage Green (#a4d4a3)\r\n        self.setStyleSheet(\"\"\"\r\n            BindingOverlay {\r\n                background-color: #a4d4a3;\r\n                border: 1px solid #689f38;\r\n                border-radius: 5px;\r\n            }\r\n            QLabel {\r\n                color: #1b5e20;  /* Dark green text for contrast */\r\n                font-family: 'Consolas', 'Courier New', monospace;\r\n                font-size: 9pt;\r\n            }\r\n        \"\"\")\r\n\r\n        # Shadow effect\r\n        shadow = QtWidgets.QGraphicsDropShadowEffect(self)\r\n        shadow.setBlurRadius(10)\r\n        shadow.setColor(QtGui.QColor(0, 0, 0, 80))\r\n        shadow.setOffset(0, 2)\r\n        self.setGraphicsEffect(shadow)\r\n\r\n        if self.network_tab:\r\n            self.network_tab.installEventFilter(self)\r\n\r\n    def _perform_auto_collapse(self):\r\n        \"\"\"Perform the auto-collapse if banner is still expanded after first show\"\"\"\r\n        if self.is_expanded:\r\n            self.animate_toggle()\r\n\r\n    def update_bindings(self, bindings_list):\r\n        \"\"\"Populate the overlay with a list of binding widgets.\"\"\"\r\n        if not bindings_list:\r\n            self.hide()\r\n            return\r\n\r\n        # Force target visibility\r\n        if self.target_widget and not self.target_widget.isVisible():\r\n            self.target_widget.setVisible(True)\r\n\r\n        # Clear old items\r\n        while self.scroll_layout.count():\r\n            item = self.scroll_layout.takeAt(0)\r\n            if item.widget():\r\n                item.widget().deleteLater()\r\n\r\n        # Add new items\r\n        for binding in bindings_list:\r\n            # Create a container widget for the row\r\n            row_widget = QtWidgets.QWidget()\r\n            row_layout = QtWidgets.QHBoxLayout(row_widget)\r\n            row_layout.setContentsMargins(0, 0, 0, 0)\r\n            row_layout.setSpacing(5)\r\n            \r\n            if isinstance(binding, dict):\r\n                n_name = binding.get('neuron_name', binding.get('neuron', '?'))\r\n                raw_action = binding.get('output_hook', binding.get('action', binding.get('action_name', '?')))\r\n                \r\n                # Clean action name\r\n                if isinstance(raw_action, str) and raw_action.startswith('neuron_output_'):\r\n                    action = raw_action.replace('neuron_output_', '')\r\n                else:\r\n                    action = raw_action\r\n                \r\n                # Logic Details\r\n                threshold = float(binding.get('threshold', 0))\r\n                mode = binding.get('trigger_mode', 'rising')\r\n                params = binding.get('hook_params', {})\r\n                \r\n                # Map mode to symbol\r\n                mode_symbols = {\r\n                    'rising': '↑',    # Crossing up\r\n                    'falling': '↓',   # Crossing down\r\n                    'above': '>',     # While above\r\n                    'below': '<',     # While below\r\n                    'change': 'Δ'     # On change\r\n                }\r\n                symbol = mode_symbols.get(mode, '>')\r\n\r\n                # 1. Text Label (Neuron > Threshold -> Action)\r\n                text_lbl = QtWidgets.QLabel(f\"{n_name} <b>{symbol} {threshold:g}</b> ➡ {action}\")\r\n                text_lbl.setTextFormat(QtCore.Qt.RichText)\r\n                row_layout.addWidget(text_lbl)\r\n\r\n                # 2. Parameter Visualization\r\n                if params and all(k in params for k in ('red', 'green', 'blue')):\r\n                    # It's a color binding - Show 15x15 box\r\n                    try:\r\n                        r = int(params['red'])\r\n                        g = int(params['green'])\r\n                        b = int(params['blue'])\r\n                        \r\n                        color_box = QtWidgets.QFrame()\r\n                        color_box.setFixedSize(15, 15)\r\n                        # Dark border for visibility on green background\r\n                        color_box.setStyleSheet(f\"background-color: rgb({r},{g},{b}); border: 1px solid #1b5e20; border-radius: 2px;\")\r\n                        row_layout.addWidget(color_box)\r\n                    except ValueError:\r\n                        pass # Invalid color data, skip box\r\n                elif params:\r\n                    # Non-color params, show generic text\r\n                    import json\r\n                    p_text = json.dumps(params).replace('\"', '').replace('{', '').replace('}', '')\r\n                    if p_text:\r\n                        p_lbl = QtWidgets.QLabel(f\"[{p_text}]\")\r\n                        p_lbl.setStyleSheet(\"font-size: 8pt; color: #2e7d32;\")\r\n                        row_layout.addWidget(p_lbl)\r\n            else:\r\n                lbl = QtWidgets.QLabel(str(binding))\r\n                row_layout.addWidget(lbl)\r\n            \r\n            row_layout.addStretch() # Align contents to left\r\n            self.scroll_layout.addWidget(row_widget)\r\n            \r\n        self.scroll_layout.addStretch()\r\n        self.bindings = bindings_list\r\n        \r\n        self.show()\r\n        self.raise_()\r\n        \r\n        # Align slightly after show to ensure correct geometry\r\n        QtCore.QTimer.singleShot(10, self.align_to_target)\r\n        \r\n        # Auto-collapse after 6 seconds on first show only\r\n        if not self._first_auto_collapse_done:\r\n            self._first_auto_collapse_done = True\r\n            self._auto_collapse_timer.start(6000)  # 6 seconds\r\n        \r\n        # Ensure banner is expanded when new bindings are loaded\r\n        if not self.is_expanded:\r\n            self.animate_toggle()\r\n\r\n    def align_to_target(self):\r\n        \"\"\"Aligns the overlay to the target widget.\"\"\"\r\n        if not self.target_widget or not self.target_widget.isVisible():\r\n            return\r\n\r\n        # Get the target widget's geometry in its immediate parent's coordinates\r\n        target_geo = self.target_widget.geometry()\r\n        \r\n        # CRITICAL FIX: Map coordinates from target's parent (functional_stats_area) \r\n        # to network_tab's coordinate system\r\n        parent = self.target_widget.parent()\r\n        if parent:\r\n            # Map top-left and bottom-right corners to network_tab coordinates\r\n            top_left = parent.mapTo(self.network_tab, target_geo.topLeft())\r\n            bottom_right = parent.mapTo(self.network_tab, target_geo.bottomRight())\r\n            target_geo = QtCore.QRect(top_left, bottom_right)\r\n        \r\n        # Apply the correctly-mapped geometry\r\n        if self.is_expanded:\r\n            self.setGeometry(target_geo)\r\n        else:\r\n            # Collapsed state - show as a narrow strip\r\n            self.setGeometry(\r\n                target_geo.x(), \r\n                target_geo.y(), \r\n                15, \r\n                target_geo.height()\r\n        )\r\n\r\n    def eventFilter(self, source, event):\r\n        if source == self.network_tab and event.type() == QtCore.QEvent.Resize:\r\n            self.align_to_target()\r\n        return super().eventFilter(source, event)\r\n\r\n    def mousePressEvent(self, event):\r\n        \"\"\"Cancel auto-collapse timer if user manually toggles\"\"\"\r\n        self._auto_collapse_timer.stop()\r\n        self.animate_toggle()\r\n        super().mousePressEvent(event)\r\n\r\n    def animate_toggle(self):\r\n        if not self.target_widget:\r\n            return\r\n\r\n        # CRITICAL FIX: Get correctly mapped geometry (same as align_to_target)\r\n        target_geo = self.target_widget.geometry()\r\n        parent = self.target_widget.parent()\r\n        if parent:\r\n            top_left = parent.mapTo(self.network_tab, target_geo.topLeft())\r\n            bottom_right = parent.mapTo(self.network_tab, target_geo.bottomRight())\r\n            target_geo = QtCore.QRect(top_left, bottom_right)\r\n\r\n        start_rect = self.geometry()\r\n        \r\n        if self.is_expanded:\r\n            # Collapse to narrow strip\r\n            end_rect = QtCore.QRect(target_geo.x(), target_geo.y(), 15, target_geo.height())\r\n            self.content_widget.hide()\r\n        else:\r\n            # Expand to full size\r\n            end_rect = target_geo\r\n            QtCore.QTimer.singleShot(50, self.content_widget.show)\r\n\r\n        self.anim = QtCore.QPropertyAnimation(self, b\"geometry\")\r\n        self.anim.setDuration(300)\r\n        self.anim.setStartValue(start_rect)\r\n        self.anim.setEndValue(end_rect)\r\n        self.anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)\r\n        self.anim.start()\r\n\r\n        self.is_expanded = not self.is_expanded\r\n        self.update()\r\n\r\n    def paintEvent(self, event):\r\n        \"\"\"Draw grip dots when collapsed.\"\"\"\r\n        super().paintEvent(event)\r\n        if not self.is_expanded:\r\n            painter = QtGui.QPainter(self)\r\n            painter.setRenderHint(QtGui.QPainter.Antialiasing)\r\n            painter.setPen(QtCore.Qt.NoPen)\r\n            # Dark green dots to match the text/theme\r\n            painter.setBrush(QtGui.QColor(27, 94, 32, 180)) \r\n            \r\n            cx = self.width() / 2\r\n            cy = self.height() / 2\r\n            for offset in [-10, 0, 10]:\r\n                painter.drawEllipse(QtCore.QPointF(cx, cy + offset), 2.0, 2.0)\r\n\r\n\r\nclass PlaceholderBanner(QtWidgets.QWidget):\r\n    \"\"\"\r\n    A placeholder banner with a pastel yellow background.\r\n    Can be used for warnings, system notifications, or future features.\r\n    \"\"\"\r\n    \r\n    def __init__(self, network_tab, target_widget):\r\n        super().__init__(network_tab)\r\n        self.network_tab = network_tab\r\n        self.target_widget = target_widget\r\n        self.is_expanded = True\r\n        \r\n        self.setAttribute(QtCore.Qt.WA_StyledBackground, True)\r\n        self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)\r\n        \r\n        # Layout\r\n        self.main_layout = QtWidgets.QVBoxLayout(self)\r\n        self.main_layout.setContentsMargins(5, 5, 5, 5)\r\n        \r\n        # Content\r\n        self.content_widget = QtWidgets.QWidget()\r\n        self.content_layout = QtWidgets.QVBoxLayout(self.content_widget)\r\n        self.content_layout.setContentsMargins(0, 0, 0, 0)\r\n        \r\n        self.label = QtWidgets.QLabel(\"⚠️ System Notification\")\r\n        self.label.setWordWrap(True)\r\n        self.label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.content_layout.addWidget(self.label)\r\n        \r\n        self.main_layout.addWidget(self.content_widget)\r\n        \r\n        # Style - Pastel Yellow (#fff9c4)\r\n        self.setStyleSheet(\"\"\"\r\n            PlaceholderBanner {\r\n                background-color: #fff9c4;\r\n                border: 1px solid #fbc02d;\r\n                border-radius: 5px;\r\n            }\r\n            QLabel {\r\n                color: #f57f17;\r\n                font-weight: bold;\r\n                font-size: 10pt;\r\n            }\r\n        \"\"\")\r\n\r\n        # Shadow\r\n        shadow = QtWidgets.QGraphicsDropShadowEffect(self)\r\n        shadow.setBlurRadius(10)\r\n        shadow.setColor(QtGui.QColor(0, 0, 0, 80))\r\n        shadow.setOffset(0, 2)\r\n        self.setGraphicsEffect(shadow)\r\n\r\n        if self.network_tab:\r\n            self.network_tab.installEventFilter(self)\r\n\r\n    def set_message(self, text):\r\n        self.label.setText(text)\r\n        self.show()\r\n        self.raise_()\r\n        QtCore.QTimer.singleShot(10, self.align_to_target)\r\n\r\n    def align_to_target(self):\r\n        if not self.target_widget or not self.target_widget.isVisible():\r\n            return\r\n        \r\n        # Note: If using multiple banners, you may need to offset Y here\r\n        target_geo = self.target_widget.geometry()\r\n        \r\n        if self.is_expanded:\r\n            self.setGeometry(target_geo)\r\n        else:\r\n            self.setGeometry(target_geo.x(), target_geo.y(), 15, target_geo.height())\r\n\r\n    def eventFilter(self, source, event):\r\n        if source == self.network_tab and event.type() == QtCore.QEvent.Resize:\r\n            self.align_to_target()\r\n        return super().eventFilter(source, event)\r\n\r\n    def mousePressEvent(self, event):\r\n        self.animate_toggle()\r\n        super().mousePressEvent(event)\r\n\r\n    def animate_toggle(self):\r\n        if not self.target_widget: return\r\n        \r\n        target_geo = self.target_widget.geometry()\r\n        start_rect = self.geometry()\r\n        \r\n        if self.is_expanded:\r\n            end_rect = QtCore.QRect(target_geo.x(), target_geo.y(), 15, target_geo.height())\r\n            self.content_widget.hide()\r\n        else:\r\n            end_rect = target_geo\r\n            QtCore.QTimer.singleShot(50, self.content_widget.show)\r\n\r\n        self.anim = QtCore.QPropertyAnimation(self, b\"geometry\")\r\n        self.anim.setDuration(300)\r\n        self.anim.setStartValue(start_rect)\r\n        self.anim.setEndValue(end_rect)\r\n        self.anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)\r\n        self.anim.start()\r\n\r\n        self.is_expanded = not self.is_expanded\r\n        self.update()\r\n\r\n    def paintEvent(self, event):\r\n        super().paintEvent(event)\r\n        if not self.is_expanded:\r\n            painter = QtGui.QPainter(self)\r\n            painter.setRenderHint(QtGui.QPainter.Antialiasing)\r\n            painter.setPen(QtCore.Qt.NoPen)\r\n            painter.setBrush(QtGui.QColor(245, 127, 23, 180)) # Dark orange dots\r\n            \r\n            cx = self.width() / 2\r\n            cy = self.height() / 2\r\n            for offset in [-10, 0, 10]:\r\n\r\n                painter.drawEllipse(QtCore.QPointF(cx, cy + offset), 2.0, 2.0)"
  },
  {
    "path": "src/brain_neuron_hooks.py",
    "content": "# brain_neuron_hooks.py\r\nimport math\r\nimport random\r\nimport time\r\nfrom typing import Dict, Callable, Any, Optional\r\n\r\nclass BrainNeuronHooks:\r\n    \"\"\"\r\n    Generic system for wiring input neurons to actual game events.\r\n    Keeps tamagotchi_logic clean by encapsulating all neuron calculation logic.\r\n    \r\n    Supports plugin-registered custom neuron handlers via the plugin manager.\r\n    \"\"\"\r\n    \r\n    def __init__(self, tamagotchi_logic):\r\n        self.logic = tamagotchi_logic\r\n        \r\n        # Registry mapping neuron names to their calculation functions\r\n        self.handlers: Dict[str, Callable] = {\r\n            'external_stimulus': self.calculate_external_stimulus,\r\n            'can_see_food': self.calculate_can_see_food,\r\n            'plant_proximity': self.calculate_plant_proximity,\r\n            'threat_level': self.calculate_threat_level,\r\n            'pursuing_food': self.calculate_pursuing_food,\r\n            'is_sick': self.calculate_is_sick,\r\n            'is_fleeing': self.calculate_is_fleeing,\r\n            'is_eating': self.calculate_is_eating,\r\n            'is_sleeping': self.calculate_is_sleeping,\r\n            'is_startled': self.calculate_is_startled,\r\n        }\r\n        \r\n        # Environmental event history for temporal calculations\r\n        self.event_tracker = {\r\n            'last_window_resize_time': 0,\r\n            'window_resize_magnitude': 0,\r\n            'new_object_appeared': False,\r\n            'last_user_interaction_time': 0,\r\n            'interaction_intensity': 0,\r\n            'last_food_spawn_time': 0,\r\n            'last_poop_spawn_time': 0,\r\n        }\r\n    \r\n    # =========================================================================\r\n    # PLUGIN INTEGRATION\r\n    # =========================================================================\r\n    \r\n    def register_handler(self, neuron_name: str, handler: Callable[[], float]) -> bool:\r\n        \"\"\"\r\n        Register a custom handler for a neuron.\r\n        \r\n        Args:\r\n            neuron_name: The name of the neuron to wire up\r\n            handler: A callable that returns a float (0-100) activation value\r\n            \r\n        Returns:\r\n            True if registered successfully, False if neuron already has a built-in handler\r\n        \"\"\"\r\n        if neuron_name in self.handlers:\r\n            print(f\"[BrainNeuronHooks] Warning: Overwriting existing handler for '{neuron_name}'\")\r\n        \r\n        self.handlers[neuron_name] = handler\r\n        print(f\"[BrainNeuronHooks] Registered handler for neuron: {neuron_name}\")\r\n        return True\r\n    \r\n    def unregister_handler(self, neuron_name: str) -> bool:\r\n        \"\"\"Remove a custom handler for a neuron.\"\"\"\r\n        if neuron_name in self.handlers:\r\n            # Don't remove built-in handlers\r\n            built_ins = {\r\n                'external_stimulus', 'can_see_food', 'plant_proximity', \r\n                'threat_level', 'pursuing_food', 'is_sick', 'is_fleeing',\r\n                'is_eating', 'is_sleeping', 'is_startled'\r\n            }\r\n            if neuron_name in built_ins:\r\n                print(f\"[BrainNeuronHooks] Cannot unregister built-in handler: {neuron_name}\")\r\n                return False\r\n            \r\n            del self.handlers[neuron_name]\r\n            print(f\"[BrainNeuronHooks] Unregistered handler for neuron: {neuron_name}\")\r\n            return True\r\n        return False\r\n    \r\n    def get_registered_neurons(self) -> list:\r\n        \"\"\"Return list of all neurons with registered handlers.\"\"\"\r\n        return list(self.handlers.keys())\r\n    \r\n    def _get_plugin_handlers(self) -> Dict[str, Callable]:\r\n        \"\"\"\r\n        Get any custom neuron handlers registered via the plugin manager.\r\n        \"\"\"\r\n        if not hasattr(self.logic, 'plugin_manager'):\r\n            return {}\r\n        \r\n        pm = self.logic.plugin_manager\r\n        if hasattr(pm, 'get_neuron_handlers'):\r\n            return pm.get_neuron_handlers()\r\n        return {}\r\n\r\n    # ------------------------------------------------------------\r\n\r\n    def calculate_pursuing_food(self) -> float:\r\n        \"\"\"Return 100.0 if squid is pursuing food, 0.0 otherwise.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'pursuing_food', False) else 0.0\r\n\r\n    def calculate_is_sick(self) -> float:\r\n        \"\"\"Return 100.0 if squid is sick, 0.0 otherwise.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'is_sick', False) else 0.0\r\n    \r\n    def calculate_is_startled(self) -> float:\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        # Use same logic as above\r\n        if hasattr(self.logic.squid, 'mental_state_manager') and self.logic.squid.mental_state_manager:\r\n            return 100.0 if self.logic.squid.mental_state_manager.is_state_active('startled') else 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'status', '').lower() == 'startled' else 0.0\r\n\r\n    def calculate_is_fleeing(self) -> float:\r\n        \"\"\"Return 100.0 if squid is fleeing, 0.0 otherwise.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'is_fleeing', False) else 0.0\r\n\r\n    # =========================================================================\r\n    # PUBLIC API - Called from tamagotchi_logic.py\r\n    # =========================================================================\r\n    \r\n    def get_input_neuron_values(self) -> Dict[str, float]:\r\n        \"\"\"\r\n        Calculate activation values for all registered input neurons.\r\n        Returns: {neuron_name: activation_value}\r\n        \"\"\"\r\n        if not hasattr(self.logic, 'brain_window') or not self.logic.brain_window:\r\n            return {}\r\n        \r\n        brain_widget = self.logic.brain_window.brain_widget\r\n        input_values = {}\r\n        \r\n        # Get neuron configurations\r\n        config = getattr(brain_widget, 'config', {})\r\n        neurons_config = config.get_neurogenesis_config().get('neurons', {})\r\n        \r\n        # Merge plugin-registered handlers with built-in handlers\r\n        all_handlers = {**self.handlers}\r\n        plugin_handlers = self._get_plugin_handlers()\r\n        all_handlers.update(plugin_handlers)\r\n        \r\n        for neuron_name in brain_widget.neuron_positions.keys():\r\n            # Skip core stat neurons\r\n            if neuron_name in ['hunger', 'happiness', 'cleanliness', 'sleepiness', \r\n                              'satisfaction', 'anxiety', 'curiosity']:\r\n                continue\r\n            \r\n            # Check if neuron is marked as input type OR has a handler\r\n            neuron_cfg = neurons_config.get(neuron_name, {})\r\n            if neuron_cfg.get('type') == 'input' or neuron_name in all_handlers:\r\n                # Call handler if exists, otherwise default background noise\r\n                if neuron_name in all_handlers:\r\n                    try:\r\n                        input_values[neuron_name] = all_handlers[neuron_name]()\r\n                    except Exception as e:\r\n                        print(f\"[BrainNeuronHooks] Error in handler for '{neuron_name}': {e}\")\r\n                        input_values[neuron_name] = 0.0\r\n                else:\r\n                    input_values[neuron_name] = random.uniform(5, 10)\r\n        \r\n        return input_values\r\n    \r\n    def on_window_resize(self, width_change: int, height_change: int, new_size: tuple):\r\n        \"\"\"Track window resize events for external_stimulus neuron.\"\"\"\r\n        magnitude = math.sqrt(width_change**2 + height_change**2)\r\n        self.event_tracker['window_resize_magnitude'] = min(100, magnitude / 10)\r\n        self.event_tracker['last_window_resize_time'] = time.time()\r\n    \r\n    def on_object_spawned(self, object_type: str):\r\n        \"\"\"Track when new objects appear in the environment.\"\"\"\r\n        self.event_tracker['new_object_appeared'] = True\r\n        \r\n        # Set interaction intensity based on object type\r\n        intensity_map = {\r\n            'food': 30,\r\n            'decorations': 20,\r\n            'poop': 10,\r\n        }\r\n        self.event_tracker['interaction_intensity'] = intensity_map.get(object_type, 15)\r\n        self.event_tracker['last_user_interaction_time'] = time.time()\r\n    \r\n    def on_user_interaction(self, action: str):\r\n        \"\"\"Track user interactions (feeding, cleaning, etc.).\"\"\"\r\n        self.event_tracker['last_user_interaction_time'] = time.time()\r\n        \r\n        intensity_map = {\r\n            'feed': 40,\r\n            'clean': 60,\r\n            'medicine': 70,\r\n            'rock_test': 50,\r\n        }\r\n        self.event_tracker['interaction_intensity'] = intensity_map.get(action, 30)\r\n    \r\n    def update_decay(self):\r\n        \"\"\"Decay environmental trackers each simulation tick.\"\"\"\r\n        self.event_tracker['window_resize_magnitude'] *= 0.95\r\n        self.event_tracker['interaction_intensity'] *= 0.90\r\n    \r\n    # =========================================================================\r\n    # HANDLER FUNCTIONS - Specific neuron calculations\r\n    # =========================================================================\r\n    \r\n    def calculate_external_stimulus(self) -> float:\r\n        \"\"\"\r\n        Calculate activation for external_stimulus neuron based on recent environmental changes.\r\n        Returns value between 0-100.\r\n        \"\"\"\r\n        tracker = self.event_tracker\r\n        current_time = time.time()\r\n        \r\n        # Start with baseline environmental noise\r\n        activation = random.uniform(5, 15)\r\n        \r\n        # Add contribution from window resize events\r\n        if tracker['window_resize_magnitude'] > 0:\r\n            time_since_resize = current_time - tracker['last_window_resize_time']\r\n            decay_factor = max(0, 1 - (time_since_resize / 10.0))\r\n            activation += tracker['window_resize_magnitude'] * decay_factor\r\n        \r\n        # Add contribution from new objects\r\n        if tracker['new_object_appeared']:\r\n            activation += 30\r\n            tracker['new_object_appeared'] = False  # Reset after one tick\r\n        \r\n        # Add contribution from user interactions\r\n        if tracker['interaction_intensity'] > 0:\r\n            time_since_interaction = current_time - tracker['last_user_interaction_time']\r\n            decay_factor = max(0, 1 - (time_since_interaction / 5.0))\r\n            activation += tracker['interaction_intensity'] * decay_factor\r\n        \r\n        return max(0, min(100, activation))\r\n    \r\n    def calculate_can_see_food(self) -> float:\r\n        \"\"\"Return 100.0 if food is visible in vision cone, 0.0 otherwise.\"\"\"\r\n        # 1. Prefer the VisionWorker result (Most accurate/current)\r\n        if hasattr(self.logic, 'latest_vision_result') and self.logic.latest_vision_result:\r\n            return 100.0 if self.logic.latest_vision_result.can_see_food else 0.0\r\n            \r\n        # 2. Fallback to synchronous check (Only if worker hasn't reported yet)\r\n        if not hasattr(self.logic, 'squid') or not self.logic.squid:\r\n            return 0.0\r\n            \r\n        return 100.0 if self.logic.squid.can_see_food() else 0.0\r\n    \r\n    def calculate_plant_proximity(self) -> float:\r\n        \"\"\"\r\n        Calculate activation based on squid's proximity to plant decorations.\r\n        \r\n        Returns:\r\n            100.0 if squid body overlaps with any plant's bounding box (touching)\r\n            0-99 scaled by distance to nearest plant edge if not touching\r\n            0.0 if no plants exist\r\n        \"\"\"\r\n        # 1. First check squid's cached plant proximity (most reliable, updated via signal)\r\n        if hasattr(self.logic, 'squid') and self.logic.squid:\r\n            squid = self.logic.squid\r\n            if hasattr(squid, '_cached_plant_proximity'):\r\n                cached = squid._cached_plant_proximity\r\n                if cached is not None and cached > 0:\r\n                    return cached\r\n            # Also try the getter method\r\n            if hasattr(squid, 'get_plant_proximity'):\r\n                prox = squid.get_plant_proximity()\r\n                if prox is not None and prox > 0:\r\n                    return prox\r\n        \r\n        # 2. Check VisionWorker result (only if attribute actually exists and has value)\r\n        if hasattr(self.logic, 'latest_vision_result') and self.logic.latest_vision_result:\r\n            result = self.logic.latest_vision_result\r\n            if hasattr(result, 'plant_proximity_value') and result.plant_proximity_value is not None:\r\n                if result.plant_proximity_value > 0:\r\n                    return result.plant_proximity_value\r\n\r\n        # 3. Manual fallback - calculate directly from scene\r\n        if not hasattr(self.logic, 'user_interface') or not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        \r\n        squid = self.logic.squid\r\n        if not hasattr(squid, 'squid_item') or not squid.squid_item:\r\n            return 0.0\r\n        \r\n        # Get squid's bounding rect in scene coordinates\r\n        squid_rect = squid.squid_item.sceneBoundingRect()\r\n        \r\n        min_distance = float('inf')\r\n        is_touching = False\r\n        \r\n        # Scan scene for plant decorations\r\n        for item in self.logic.user_interface.scene.items():\r\n            # Check if item has category attribute and is a plant\r\n            if hasattr(item, 'category') and item.category == 'plant':\r\n                plant_rect = item.sceneBoundingRect()\r\n                \r\n                # Check if squid overlaps with plant bounding box\r\n                if squid_rect.intersects(plant_rect):\r\n                    is_touching = True\r\n                    break\r\n                \r\n                # Calculate distance from squid center to plant edge\r\n                squid_center = squid_rect.center()\r\n                \r\n                # Find closest point on plant rect to squid center\r\n                closest_x = max(plant_rect.left(), min(squid_center.x(), plant_rect.right()))\r\n                closest_y = max(plant_rect.top(), min(squid_center.y(), plant_rect.bottom()))\r\n                \r\n                # Distance from squid center to that closest point\r\n                dist = math.hypot(squid_center.x() - closest_x, squid_center.y() - closest_y)\r\n                min_distance = min(min_distance, dist)\r\n        \r\n        # If touching any plant, return max activation\r\n        if is_touching:\r\n            return 100.0\r\n        \r\n        # If no plants found, return 0\r\n        if min_distance == float('inf'):\r\n            return 0.0\r\n        \r\n        # Scale activation by distance (closer = higher, max range ~300px)\r\n        max_range = 300.0\r\n        activation = max(0.0, 100.0 - (min_distance / max_range * 100.0))\r\n        \r\n        return activation\r\n    \r\n    def calculate_threat_level(self) -> float:\r\n        \"\"\"Calculate activation based on current anxiety and startle state.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0\r\n        \r\n        # Base threat on anxiety\r\n        threat_level = self.logic.squid.anxiety\r\n        \r\n        # Increase if startled or fleeing\r\n        if getattr(self.logic.squid, 'is_fleeing', False):\r\n            threat_level = min(100, threat_level + 30)\r\n        \r\n        # Add random fluctuation\r\n        threat_level += random.uniform(-5, 5)\r\n        \r\n        return max(0, min(100, threat_level))\r\n    \r\n    def calculate_is_eating(self) -> float:\r\n        \"\"\"Return 100.0 if squid is eating, 0.0 otherwise.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'is_eating', False) else 0.0\r\n\r\n    def calculate_is_sleeping(self) -> float:\r\n        \"\"\"Return 100.0 if squid is sleeping, 0.0 otherwise.\"\"\"\r\n        if not hasattr(self.logic, 'squid'):\r\n            return 0.0\r\n        return 100.0 if getattr(self.logic.squid, 'is_sleeping', False) else 0.0\r\n\r\n\r\n# Default sensors that have built-in handlers\r\nDEFAULT_INPUT_SENSORS = (\r\n    'external_stimulus',\r\n    'can_see_food',\r\n    'plant_proximity',\r\n    'threat_level',\r\n    'pursuing_food',\r\n    'is_sick',\r\n    'is_fleeing',\r\n    'is_eating',\r\n    'is_sleeping',\r\n    'is_startled',\r\n)"
  },
  {
    "path": "src/brain_neuron_outputs.py",
    "content": "\"\"\"\r\nNeuron Output Binding System\r\n\r\nThis module enables custom neurons in the brain designer to trigger game behaviors\r\nwhen they fire. It creates a bidirectional system:\r\n\r\nINPUT (Sensors):  Game Events → Hooks → Neuron Activation\r\nOUTPUT (Actuators): Neuron Fires → Threshold Check → Hooks → Game Behavior\r\n\"\"\"\r\n\r\nimport time\r\nfrom typing import Dict, List, Callable, Any, Optional\r\nfrom dataclasses import dataclass, field\r\nfrom enum import Enum\r\n\r\n# Try importing PyQt5 for the floating console\r\ntry:\r\n    from PyQt5 import QtWidgets, QtCore, QtGui\r\n    HAS_QT = True\r\nexcept ImportError:\r\n    HAS_QT = False\r\n\r\n# ANSI Colors for Console Output (Fallback)\r\nANSI_ORANGE = \"\\033[38;5;208m\"\r\nANSI_RESET = \"\\033[0m\"\r\n\r\nclass OutputTriggerMode(Enum):\r\n    \"\"\"How the output should be triggered.\"\"\"\r\n    THRESHOLD_RISING = \"rising\"      # Fire when crossing threshold upward\r\n    THRESHOLD_FALLING = \"falling\"    # Fire when crossing threshold downward  \r\n    THRESHOLD_ABOVE = \"above\"        # Fire continuously while above threshold\r\n    THRESHOLD_BELOW = \"below\"        # Fire continuously while below threshold\r\n    ON_CHANGE = \"change\"             # Fire on any significant change\r\n\r\n\r\n@dataclass\r\nclass NeuronOutputBinding:\r\n    \"\"\"\r\n    Binds a neuron to an output hook.\r\n    \r\n    When the neuron's activation meets the trigger conditions,\r\n    the bound hook is fired with the activation value.\r\n    \"\"\"\r\n    neuron_name: str\r\n    output_hook: str\r\n    threshold: float = 70.0\r\n    trigger_mode: OutputTriggerMode = OutputTriggerMode.THRESHOLD_RISING\r\n    cooldown: float = 1.0  # Seconds between firings\r\n    enabled: bool = True\r\n    \r\n    # Optional parameters passed to the hook\r\n    hook_params: Dict[str, Any] = field(default_factory=dict)\r\n    \r\n    # Runtime state (not saved)\r\n    last_fire_time: float = 0.0\r\n    last_activation: float = 0.0\r\n    \r\n    def to_dict(self) -> dict:\r\n        \"\"\"Serialize for saving.\"\"\"\r\n        return {\r\n            'neuron_name': self.neuron_name,\r\n            'output_hook': self.output_hook,\r\n            'threshold': self.threshold,\r\n            'trigger_mode': self.trigger_mode.value,\r\n            'cooldown': self.cooldown,\r\n            'enabled': self.enabled,\r\n            'hook_params': self.hook_params,\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: dict) -> 'NeuronOutputBinding':\r\n        \"\"\"Deserialize from saved data.\"\"\"\r\n        trigger_mode = OutputTriggerMode(data.get('trigger_mode', 'rising'))\r\n        return cls(\r\n            neuron_name=data['neuron_name'],\r\n            output_hook=data['output_hook'],\r\n            threshold=data.get('threshold', 70.0),\r\n            trigger_mode=trigger_mode,\r\n            cooldown=data.get('cooldown', 1.0),\r\n            enabled=data.get('enabled', True),\r\n            hook_params=data.get('hook_params', {}),\r\n        )\r\n    \r\n    def should_fire(self, current_activation: float, current_time: float) -> bool:\r\n        \"\"\"Check if this binding should fire given current activation.\"\"\"\r\n        if not self.enabled:\r\n            return False\r\n        \r\n        # Check cooldown\r\n        if current_time - self.last_fire_time < self.cooldown:\r\n            return False\r\n        \r\n        should_fire = False\r\n        \r\n        if self.trigger_mode == OutputTriggerMode.THRESHOLD_RISING:\r\n            # Fire when crossing threshold upward\r\n            should_fire = (\r\n                self.last_activation < self.threshold and \r\n                current_activation >= self.threshold\r\n            )\r\n        \r\n        elif self.trigger_mode == OutputTriggerMode.THRESHOLD_FALLING:\r\n            # Fire when crossing threshold downward\r\n            should_fire = (\r\n                self.last_activation >= self.threshold and \r\n                current_activation < self.threshold\r\n            )\r\n        \r\n        elif self.trigger_mode == OutputTriggerMode.THRESHOLD_ABOVE:\r\n            # Fire continuously while above threshold\r\n            should_fire = current_activation >= self.threshold\r\n        \r\n        elif self.trigger_mode == OutputTriggerMode.THRESHOLD_BELOW:\r\n            # Fire continuously while below threshold\r\n            should_fire = current_activation < self.threshold\r\n        \r\n        elif self.trigger_mode == OutputTriggerMode.ON_CHANGE:\r\n            # Fire on significant change (>10% difference)\r\n            should_fire = abs(current_activation - self.last_activation) > 10.0\r\n        \r\n        return should_fire\r\n\r\n\r\n# =============================================================================\r\n# FLOATING LOG WINDOW\r\n# =============================================================================\r\n\r\nif HAS_QT:\r\n    class NeuronLogWindow(QtWidgets.QWidget):\r\n        \"\"\"A floating console window for neuron output logs.\"\"\"\r\n        \r\n        def __init__(self):\r\n            super().__init__()\r\n            self.setWindowTitle(\"Neuron Monitor\")\r\n            \r\n            # Window Flags: Tool (small title bar), Stay on Top\r\n            self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint)\r\n            self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)\r\n            \r\n            self.resize(800, 200)\r\n            \r\n            # Styling: Dark background, Orange text (Consolas/Monospace)\r\n            self.setStyleSheet(\"\"\"\r\n                QWidget {\r\n                    background-color: #121212;\r\n                    color: #ffb74d;\r\n                    font-family: 'Consolas', 'Courier New', monospace;\r\n                    font-size: 10pt;\r\n                }\r\n                QPlainTextEdit {\r\n                    border: 1px solid #333;\r\n                    selection-background-color: #333;\r\n                }\r\n            \"\"\")\r\n            \r\n            # Layout\r\n            layout = QtWidgets.QVBoxLayout(self)\r\n            layout.setContentsMargins(2, 2, 2, 2)\r\n            \r\n            # Text Area\r\n            self.text_area = QtWidgets.QPlainTextEdit()\r\n            self.text_area.setReadOnly(True)\r\n            self.text_area.setMaximumBlockCount(1000) # Limit history\r\n            layout.addWidget(self.text_area)\r\n            \r\n            # Initial positioning\r\n            self._position_at_bottom_center()\r\n            \r\n        def _position_at_bottom_center(self):\r\n            \"\"\"Move window to bottom center of primary screen.\"\"\"\r\n            if not QtWidgets.QApplication.instance():\r\n                return\r\n                \r\n            screen = QtWidgets.QApplication.primaryScreen()\r\n            if screen:\r\n                geo = screen.availableGeometry()\r\n                # Center X\r\n                x = geo.x() + (geo.width() - self.width()) // 2\r\n                # Bottom Y (with some padding)\r\n                y = geo.y() + geo.height() - self.height() - 50\r\n                self.move(x, y)\r\n\r\n        def log(self, message: str):\r\n            \"\"\"Append a message to the log.\"\"\"\r\n            timestamp = time.strftime(\"%H:%M:%S\")\r\n            self.text_area.appendPlainText(f\"[{timestamp}] {message}\")\r\n            \r\n            # Auto-scroll to bottom\r\n            bar = self.text_area.verticalScrollBar()\r\n            bar.setValue(bar.maximum())\r\nelse:\r\n    class NeuronLogWindow:\r\n        \"\"\"Dummy class if PyQt is not available.\"\"\"\r\n        def log(self, msg): print(msg)\r\n        def show(self): pass\r\n\r\n\r\n# =============================================================================\r\n# PREDEFINED OUTPUT HOOKS\r\n# =============================================================================\r\n\r\nSTANDARD_OUTPUT_HOOKS = {\r\n    # Movement behaviors\r\n    'neuron_output_flee': {\r\n        'description': 'Trigger fleeing behavior',\r\n        'category': 'movement',\r\n        'default_threshold': 80.0,\r\n    },\r\n    'neuron_output_seek_food': {\r\n        'description': 'Drive food-seeking behavior',\r\n        'category': 'movement',\r\n        'default_threshold': 60.0,\r\n    },\r\n    'neuron_output_seek_plant': {\r\n        'description': 'Move toward nearest plant',\r\n        'category': 'movement',\r\n        'default_threshold': 50.0,\r\n    },\r\n    'neuron_output_approach_rock': {\r\n        'description': 'Approach nearest rock',\r\n        'category': 'movement',\r\n        'default_threshold': 50.0,\r\n    },\r\n    'neuron_output_wander': {\r\n        'description': 'Random exploration movement',\r\n        'category': 'movement',\r\n        'default_threshold': 40.0,\r\n    },\r\n    \r\n    # Action behaviors\r\n    'neuron_output_throw_rock': {\r\n        'description': 'Throw held rock',\r\n        'category': 'action',\r\n        'default_threshold': 70.0,\r\n    },\r\n    'neuron_output_pick_up_rock': {\r\n        'description': 'Pick up nearby rock',\r\n        'category': 'action',\r\n        'default_threshold': 60.0,\r\n    },\r\n    'neuron_output_ink_cloud': {\r\n        'description': 'Release defensive ink cloud',\r\n        'category': 'action',\r\n        'default_threshold': 85.0,\r\n    },\r\n    'neuron_output_eat': {\r\n        'description': 'Eat nearby food',\r\n        'category': 'action',\r\n        'default_threshold': 50.0,\r\n    },\r\n    \r\n    # State changes\r\n    'neuron_output_sleep': {\r\n        'description': 'Initiate sleep',\r\n        'category': 'state',\r\n        'default_threshold': 90.0,\r\n    },\r\n    'neuron_output_wake': {\r\n        'description': 'Wake from sleep',\r\n        'category': 'state',\r\n        'default_threshold': 30.0,\r\n    },\r\n    'neuron_output_startle': {\r\n        'description': 'Trigger startle response',\r\n        'category': 'state',\r\n        'default_threshold': 75.0,\r\n    },\r\n    'neuron_output_calm': {\r\n        'description': 'Reduce anxiety/calm down',\r\n        'category': 'state',\r\n        'default_threshold': 20.0,\r\n    },\r\n    \r\n    # Stat modifications\r\n    'neuron_output_boost_happiness': {\r\n        'description': 'Increase happiness',\r\n        'category': 'stats',\r\n        'default_threshold': 70.0,\r\n    },\r\n    'neuron_output_boost_curiosity': {\r\n        'description': 'Increase curiosity',\r\n        'category': 'stats',\r\n        'default_threshold': 60.0,\r\n    },\r\n    'neuron_output_reduce_anxiety': {\r\n        'description': 'Decrease anxiety',\r\n        'category': 'stats',\r\n        'default_threshold': 30.0,\r\n    },\r\n\r\n     # Colour change\r\n    'neuron_output_change_color': {\r\n        'description': 'Change the squid body color when triggered. Can specify specific color parameters.',\r\n        'category': 'action',\r\n        'default_threshold': 60.0,\r\n        'has_params': True,\r\n    },\r\n    \r\n    # Custom/plugin hooks\r\n    'neuron_output_custom': {\r\n        'description': 'Custom behavior (plugin-defined)',\r\n        'category': 'custom',\r\n        'default_threshold': 50.0,\r\n    },\r\n}\r\n\r\n\r\nclass NeuronOutputMonitor:\r\n    \"\"\"\r\n    Monitors neuron activations and triggers output hooks when thresholds are met.\r\n    \r\n    This is the runtime component that connects the neural network to game behaviors.\r\n    \"\"\"\r\n    \r\n    def __init__(self, tamagotchi_logic):\r\n        self.logic = tamagotchi_logic\r\n        self.bindings: List[NeuronOutputBinding] = []\r\n        self.enabled = True\r\n        \r\n        # Statistics\r\n        self.total_fires = 0\r\n        self.fires_by_hook: Dict[str, int] = {}\r\n        \r\n        # Log Window\r\n        self.log_window = None\r\n        \r\n        # Register default hook handlers\r\n        self._register_default_handlers()\r\n    \r\n    def _ensure_log_window(self):\r\n        \"\"\"Create the log window if it doesn't exist and Qt is available.\"\"\"\r\n        if self.log_window is None and HAS_QT:\r\n            # Only create if QApplication exists\r\n            if QtWidgets.QApplication.instance():\r\n                self.log_window = NeuronLogWindow()\r\n\r\n    def _log(self, message: str):\r\n        \"\"\"Helper to print logs to floating window (or console fallback).\"\"\"\r\n        if HAS_QT and QtWidgets.QApplication.instance():\r\n            self._ensure_log_window()\r\n            if self.log_window:\r\n                self.log_window.log(message)\r\n        else:\r\n            # Fallback for headless or non-Qt environments\r\n            print(f\"{ANSI_ORANGE}[NeuronOutputMonitor]{ANSI_RESET} {message}\")\r\n\r\n    def _register_default_handlers(self):\r\n        \"\"\"Register all standard output hooks and subscribe to available handlers.\"\"\"\r\n        if not hasattr(self.logic, 'plugin_manager'):\r\n            self._log(\"⚠️ Cannot register handlers: plugin_manager not available\")\r\n            return\r\n        \r\n        pm = self.logic.plugin_manager\r\n        \r\n        # Whitelist this monitor as an enabled 'plugin'\r\n        if hasattr(pm, 'enabled_plugins'):\r\n            pm.enabled_plugins.add('neuronoutputmonitor')\r\n            \r\n        self._log(f\"📡 Plugin manager found, registering {len(STANDARD_OUTPUT_HOOKS)} hooks...\")\r\n        \r\n        # Register hooks\r\n        for hook_name in STANDARD_OUTPUT_HOOKS.keys():\r\n            pm.register_hook(hook_name)\r\n            \r\n        # Subscribe handlers\r\n        import inspect\r\n        subscribed_count = 0\r\n        for method_name, method in inspect.getmembers(self, predicate=inspect.ismethod):\r\n            if method_name.startswith('_handle_'):\r\n                base_name = method_name[8:]  # Remove '_handle_' prefix\r\n                hook_name = f\"neuron_output_{base_name}\"\r\n                \r\n                if hook_name in STANDARD_OUTPUT_HOOKS:\r\n                    success = pm.subscribe_to_hook(hook_name, 'NeuronOutputMonitor', method)\r\n                    if success:\r\n                        subscribed_count += 1\r\n        \r\n        self._log(f\"📊 Ready. Total subscriptions: {subscribed_count}\")\r\n        \r\n        # Redundant safety pass\r\n        for method_name, method in inspect.getmembers(self, predicate=inspect.ismethod):\r\n            if method_name.startswith('_handle_'):\r\n                base_name = method_name[8:]\r\n                hook_name = f\"neuron_output_{base_name}\"\r\n                if hook_name in STANDARD_OUTPUT_HOOKS:\r\n                    pm.subscribe_to_hook(hook_name, 'NeuronOutputMonitor', method)\r\n    \r\n    # =========================================================================\r\n    # BINDING MANAGEMENT\r\n    # =========================================================================\r\n\r\n    def monitor(self, neuron_activations: Dict[str, float], current_time: Optional[float] = None):\r\n        \"\"\"Main method called every frame with all current neuron activations.\"\"\"\r\n        if not self.enabled:\r\n            return\r\n            \r\n        # Self-Healing Whitelist\r\n        if hasattr(self.logic, 'plugin_manager'):\r\n            pm = self.logic.plugin_manager\r\n            if hasattr(pm, 'enabled_plugins') and 'neuronoutputmonitor' not in pm.enabled_plugins:\r\n                pm.enabled_plugins.add('neuronoutputmonitor')\r\n        \r\n        current_time = current_time or time.time()\r\n        \r\n        for binding in self.bindings:\r\n            activation = neuron_activations.get(binding.neuron_name, 0.0)\r\n            \r\n            # Check for firing BEFORE updating last_activation\r\n            if binding.should_fire(activation, current_time):\r\n                self._fire_binding(binding, activation, current_time)\r\n            \r\n            # Update last activation for edge detection\r\n            binding.last_activation = activation\r\n    \r\n    def add_binding(self, binding: NeuronOutputBinding) -> bool:\r\n        \"\"\"Add a new output binding.\"\"\"\r\n        self.bindings.append(binding)\r\n        self._log(f\"Added binding: {binding.neuron_name} → {binding.output_hook}\")\r\n        return True\r\n    \r\n    def remove_binding(self, neuron_name: str, output_hook: str) -> bool:\r\n        \"\"\"Remove bindings matching neuron and hook name.\"\"\"\r\n        initial_len = len(self.bindings)\r\n        self.bindings = [\r\n            b for b in self.bindings \r\n            if not (b.neuron_name == neuron_name and b.output_hook == output_hook)\r\n        ]\r\n        return len(self.bindings) < initial_len\r\n    \r\n    def get_bindings_for_neuron(self, neuron_name: str) -> List[NeuronOutputBinding]:\r\n        \"\"\"Get all output bindings for a specific neuron.\"\"\"\r\n        return [b for b in self.bindings if b.neuron_name == neuron_name]\r\n    \r\n    def clear_bindings(self):\r\n        \"\"\"Remove all bindings.\"\"\"\r\n        self.bindings.clear()\r\n    \r\n    def load_bindings_from_brain(self, brain_data: dict):\r\n        \"\"\"Load output bindings from brain configuration data.\"\"\"\r\n        self.clear_bindings()\r\n        \r\n        output_bindings = brain_data.get('output_bindings', [])\r\n        for binding_data in output_bindings:\r\n            try:\r\n                binding = NeuronOutputBinding.from_dict(binding_data)\r\n                self.add_binding(binding)\r\n            except Exception as e:\r\n                self._log(f\"Error loading binding: {e}\")\r\n    \r\n    def export_bindings(self) -> List[dict]:\r\n        \"\"\"Export all bindings as serializable dicts.\"\"\"\r\n        return [b.to_dict() for b in self.bindings]\r\n    \r\n    # =========================================================================\r\n    # RUNTIME PROCESSING\r\n    # =========================================================================\r\n    \r\n    def process_outputs(self):\r\n        \"\"\"\r\n        Process all output bindings.\r\n        Call this each simulation tick after the neural network has been updated.\r\n        \"\"\"\r\n        if not self.enabled or not self.bindings:\r\n            return\r\n        \r\n        if not hasattr(self.logic, 'brain_window') or not self.logic.brain_window:\r\n            return\r\n        \r\n        brain_widget = self.logic.brain_window.brain_widget\r\n        current_time = time.time()\r\n        \r\n        for binding in self.bindings:\r\n            # Get current activation for this neuron\r\n            activation = self._get_neuron_activation(brain_widget, binding.neuron_name)\r\n            \r\n            # If we can't find the activation, we can't trigger\r\n            if activation is None:\r\n                continue\r\n            \r\n            # Check if should fire\r\n            if binding.should_fire(activation, current_time):\r\n                self._fire_binding(binding, activation, current_time)\r\n            \r\n            # Update last activation for next check\r\n            binding.last_activation = activation\r\n    \r\n    def _get_neuron_activation(self, brain_widget, neuron_name: str) -> Optional[float]:\r\n        \"\"\"\r\n        Get the current activation value of a neuron.\r\n        Checks multiple sources to ensure compatibility with different brain versions.\r\n        \"\"\"\r\n        # 1. Check 'neuron_activations' (often used for sensor overrides/inputs)\r\n        if hasattr(brain_widget, 'neuron_activations') and brain_widget.neuron_activations:\r\n            val = brain_widget.neuron_activations.get(neuron_name)\r\n            if val is not None:\r\n                return float(val)\r\n\r\n        # 2. Check 'state' (main storage for neuron values in Dosidicus/Custom brains)\r\n        if hasattr(brain_widget, 'state') and brain_widget.state:\r\n            val = brain_widget.state.get(neuron_name)\r\n            if val is not None:\r\n                return float(val)\r\n\r\n        # 3. Fallback: check config state\r\n        if hasattr(brain_widget, 'config'):\r\n            try:\r\n                state = brain_widget.config.get_neurogenesis_config().get('state', {})\r\n                val = state.get(neuron_name)\r\n                if val is not None:\r\n                    return float(val)\r\n            except:\r\n                pass\r\n        \r\n        return None\r\n    \r\n    def _fire_binding(self, binding: NeuronOutputBinding, activation: float, current_time: float):\r\n        \"\"\"Fire a binding's output hook.\"\"\"\r\n        binding.last_fire_time = current_time\r\n        \r\n        # Update statistics\r\n        self.total_fires += 1\r\n        self.fires_by_hook[binding.output_hook] = self.fires_by_hook.get(binding.output_hook, 0) + 1\r\n        \r\n        # Trigger the hook\r\n        if hasattr(self.logic, 'plugin_manager'):\r\n            self.logic.plugin_manager.trigger_hook(\r\n                binding.output_hook,\r\n                neuron_name=binding.neuron_name,\r\n                activation=activation,\r\n                threshold=binding.threshold,\r\n                squid=self.logic.squid,\r\n                tamagotchi_logic=self.logic,\r\n                **binding.hook_params\r\n            )\r\n        \r\n        # Debug output\r\n        if hasattr(self.logic, 'debug_mode') and self.logic.debug_mode:\r\n            self._log(f\"FIRED: {binding.neuron_name} → {binding.output_hook} ({activation:.0f})\")\r\n    \r\n    # =========================================================================\r\n    # DEFAULT HOOK HANDLERS\r\n    # =========================================================================\r\n    \r\n    def _handle_flee(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and not getattr(squid, 'is_fleeing', False):\r\n            squid.is_fleeing = True\r\n            squid.current_speed = squid.base_speed * 2\r\n            if hasattr(squid, 'mental_state_manager'):\r\n                squid.mental_state_manager.activate_state('fleeing')\r\n    \r\n    def _handle_seek_food(self, neuron_name, activation, squid, tamagotchi_logic, **kwargs):\r\n        if squid and tamagotchi_logic:\r\n            visible_food = squid.get_visible_food() if hasattr(squid, 'get_visible_food') else []\r\n            if visible_food:\r\n                squid.pursuing_food = True\r\n                squid.target_food = visible_food[0]\r\n    \r\n    def _handle_seek_plant(self, neuron_name, activation, squid, tamagotchi_logic, **kwargs):\r\n        if not squid or not tamagotchi_logic: return\r\n        \r\n        nearest_plant = None\r\n        min_dist = float('inf')\r\n        \r\n        for item in tamagotchi_logic.user_interface.scene.items():\r\n            if hasattr(item, 'category') and item.category == 'plant':\r\n                plant_pos = item.sceneBoundingRect().center()\r\n                dist = ((plant_pos.x() - squid.squid_x)**2 + (plant_pos.y() - squid.squid_y)**2)**0.5\r\n                if dist < min_dist:\r\n                    min_dist = dist\r\n                    nearest_plant = item\r\n        \r\n        if nearest_plant:\r\n            squid.status = \"seeking_plant\"\r\n            target_pos = nearest_plant.sceneBoundingRect().center()\r\n            squid.move_toward_position(target_pos)\r\n    \r\n    def _handle_ink_cloud(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and hasattr(squid, 'release_ink'):\r\n            squid.release_ink()\r\n        elif squid and hasattr(squid, 'mental_state_manager'):\r\n            squid.mental_state_manager.activate_state('inking')\r\n\r\n    def _handle_change_color(self, neuron_name, activation, squid, **kwargs):\r\n        \"\"\"\r\n        Handle color change when neuron fires.\r\n        Checks for specific 'red', 'green', 'blue' parameters in kwargs.\r\n        \"\"\"\r\n        if squid and hasattr(squid, 'apply_tint'):\r\n            from PyQt5.QtGui import QColor\r\n            import random\r\n\r\n            try:\r\n                r = int(kwargs.get('red', -1))\r\n                g = int(kwargs.get('green', -1))\r\n                b = int(kwargs.get('blue', -1))\r\n                \r\n                if r >= 0 and g >= 0 and b >= 0:\r\n                    color = QColor(r, g, b)\r\n                    squid.apply_tint(color)\r\n                    self._log(f\"Tint: {r},{g},{b}\")\r\n                    return\r\n            except (ValueError, TypeError):\r\n                pass\r\n            \r\n            # Fallback only if params missing or invalid\r\n            colors = [\r\n                (255, 100, 100), (100, 255, 100), (100, 100, 255),\r\n                (255, 255, 100), (255, 100, 255), (100, 255, 255)\r\n            ]\r\n            rgb = random.choice(colors)\r\n            squid.apply_tint(QColor(*rgb))\r\n            self._log(f\"RandTint: {rgb}\")\r\n    \r\n    def _handle_startle(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and hasattr(squid, 'mental_state_manager'):\r\n            squid.mental_state_manager.activate_state('startled')\r\n    \r\n    def _handle_calm(self, neuron_name, activation, squid, **kwargs):\r\n        if squid:\r\n            squid.anxiety = max(0, squid.anxiety - 10)\r\n            squid.is_fleeing = False\r\n            if hasattr(squid, 'mental_state_manager'):\r\n                squid.mental_state_manager.deactivate_state('startled')\r\n                squid.mental_state_manager.deactivate_state('fleeing')\r\n    \r\n    def _handle_sleep(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and not getattr(squid, 'is_sleeping', False):\r\n            squid.is_sleeping = True\r\n            squid.status = \"sleeping\"\r\n    \r\n    def _handle_wake(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and getattr(squid, 'is_sleeping', False):\r\n            squid.is_sleeping = False\r\n            squid.status = \"roaming\"\r\n    \r\n    def _handle_boost_happiness(self, neuron_name, activation, squid, **kwargs):\r\n        if squid:\r\n            boost = (activation / 100.0) * 5\r\n            squid.happiness = min(100, squid.happiness + boost)\r\n    \r\n    def _handle_reduce_anxiety(self, neuron_name, activation, squid, **kwargs):\r\n        if squid:\r\n            reduction = ((100 - activation) / 100.0) * 5\r\n            squid.anxiety = max(0, squid.anxiety - reduction)\r\n    \r\n    def _handle_wander(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and hasattr(squid, 'wander'):\r\n            squid.wander()\r\n        elif squid:\r\n            squid.status = \"roaming\"\r\n    \r\n    def _handle_approach_rock(self, neuron_name, activation, squid, tamagotchi_logic, **kwargs):\r\n        if not squid or not tamagotchi_logic: return\r\n        \r\n        decorations = tamagotchi_logic.get_nearby_decorations(squid.squid_x, squid.squid_y, 300)\r\n        rocks = [d for d in decorations if hasattr(d, 'category') and d.category == 'rock']\r\n        \r\n        if rocks:\r\n            nearest = min(rocks, key=lambda r: \r\n                ((r.sceneBoundingRect().center().x() - squid.squid_x)**2 + \r\n                 (r.sceneBoundingRect().center().y() - squid.squid_y)**2))\r\n            squid.status = \"approaching_rock\"\r\n            squid.current_rock_target = nearest\r\n    \r\n    def _handle_throw_rock(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and getattr(squid, 'carrying_rock', False):\r\n            import random\r\n            direction = random.choice(['left', 'right'])\r\n            squid.throw_rock(direction)\r\n    \r\n    def _handle_pick_up_rock(self, neuron_name, activation, squid, tamagotchi_logic, **kwargs):\r\n        if not squid or getattr(squid, 'carrying_rock', False): return\r\n        \r\n        if tamagotchi_logic:\r\n            decorations = tamagotchi_logic.get_nearby_decorations(squid.squid_x, squid.squid_y, 50)\r\n            rocks = [d for d in decorations if hasattr(d, 'category') and d.category == 'rock']\r\n            if rocks:\r\n                squid.pick_up_rock(rocks[0])\r\n    \r\n    def _handle_eat(self, neuron_name, activation, squid, **kwargs):\r\n        if squid and hasattr(squid, 'target_food') and squid.target_food:\r\n            squid.is_eating = True\r\n    \r\n    def _handle_boost_curiosity(self, neuron_name, activation, squid, **kwargs):\r\n        if squid:\r\n            boost = (activation / 100.0) * 5\r\n            squid.curiosity = min(100, squid.curiosity + boost)\r\n\r\n\r\ndef get_available_output_hooks() -> Dict[str, dict]:\r\n    return dict(STANDARD_OUTPUT_HOOKS)\r\n\r\n\r\ndef get_output_hooks_by_category() -> Dict[str, Dict[str, dict]]:\r\n    by_category = {}\r\n    for hook_name, info in STANDARD_OUTPUT_HOOKS.items():\r\n        cat = info.get('category', 'other')\r\n        if cat not in by_category:\r\n            by_category[cat] = {}\r\n        by_category[cat][hook_name] = info\r\n    return by_category"
  },
  {
    "path": "src/brain_personality_tab.py",
    "content": "# brain_personality_tab.py\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom .brain_base_tab import BrainBaseTab\r\nfrom .personality import Personality\r\nfrom .localisation import Localisation\r\n\r\nclass PersonalityTab(BrainBaseTab):\r\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\r\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\r\n        self.loc = Localisation.instance()\r\n        self.initialize_ui()\r\n        \r\n    def initialize_ui(self):\r\n        from .display_scaling import DisplayScaling\r\n        \r\n        # Create a scrollable area for the tab content\r\n        scroll_area = QtWidgets.QScrollArea()\r\n        scroll_area.setWidgetResizable(True)\r\n        scroll_area.setFrameShape(QtWidgets.QFrame.NoFrame)\r\n        \r\n        # Create content widget\r\n        content_widget = QtWidgets.QWidget()\r\n        self.tab_layout = QtWidgets.QVBoxLayout(content_widget)\r\n        \r\n        # Use properly scaled font sizes\r\n        self.base_font_size = DisplayScaling.font_size(10)\r\n        self.header_font_size = DisplayScaling.font_size(12)\r\n        \r\n        # Add personality section\r\n        self.init_personality_section()\r\n        \r\n        # Set the scroll area's widget\r\n        scroll_area.setWidget(content_widget)\r\n        \r\n        # Add to main layout\r\n        self.layout.addWidget(scroll_area)\r\n        \r\n    def init_personality_section(self):\r\n        # Separator line\r\n        self.tab_layout.addWidget(QtWidgets.QFrame(frameShape=QtWidgets.QFrame.HLine))\r\n        \r\n        # Personality type label - larger and bolder\r\n        self.personality_type_label = QtWidgets.QLabel(f\"{self.loc.get('squid_personality')}: \")\r\n        font = QtGui.QFont()\r\n        font.setPointSize(self.header_font_size)\r\n        font.setBold(True)\r\n        self.personality_type_label.setFont(font)\r\n        self.tab_layout.addWidget(self.personality_type_label)\r\n\r\n        # Personality modifier label - larger\r\n        self.personality_modifier_label = QtWidgets.QLabel(f\"{self.loc.get('personality_modifier')}: \")\r\n        mod_font = QtGui.QFont()\r\n        mod_font.setPointSize(self.base_font_size)\r\n        mod_font.setBold(True)\r\n        self.personality_modifier_label.setFont(mod_font)\r\n        self.tab_layout.addWidget(self.personality_modifier_label)\r\n\r\n        # Separator\r\n        self.tab_layout.addWidget(QtWidgets.QFrame(frameShape=QtWidgets.QFrame.HLine))\r\n        self.tab_layout.addSpacing(20)\r\n\r\n        # Description section\r\n        description_label = QtWidgets.QLabel(self.loc.get(\"description\"))\r\n        description_label.setFont(font)\r\n        self.tab_layout.addWidget(description_label)\r\n\r\n        self.personality_description = QtWidgets.QTextEdit()\r\n        self.personality_description.setReadOnly(True)\r\n        text_font = QtGui.QFont()\r\n        text_font.setPointSize(self.base_font_size)\r\n        self.personality_description.setFont(text_font)\r\n        self.tab_layout.addWidget(self.personality_description)\r\n        self.tab_layout.addSpacing(20)\r\n\r\n        # Personality modifiers\r\n        self.modifiers_label = QtWidgets.QLabel(self.loc.get(\"personality_modifiers\"))\r\n        self.modifiers_label.setFont(font)\r\n        self.tab_layout.addWidget(self.modifiers_label)\r\n\r\n        self.modifiers_text = QtWidgets.QTextEdit()\r\n        self.modifiers_text.setReadOnly(True)\r\n        self.modifiers_text.setFont(text_font)\r\n        self.tab_layout.addWidget(self.modifiers_text)\r\n        self.tab_layout.addSpacing(20)\r\n\r\n        # Care tips\r\n        self.care_tips_label = QtWidgets.QLabel(self.loc.get(\"care_tips_label\"))\r\n        self.care_tips_label.setFont(font)\r\n        self.tab_layout.addWidget(self.care_tips_label)\r\n\r\n        self.care_tips = QtWidgets.QTextEdit()\r\n        self.care_tips.setReadOnly(True)\r\n        self.care_tips.setFont(text_font)\r\n        self.tab_layout.addWidget(self.care_tips)\r\n        self.tab_layout.addSpacing(20)\r\n\r\n        # Note about personality generation\r\n        note_label = QtWidgets.QLabel(self.loc.get(\"personality_note\"))\r\n        note_font = QtGui.QFont()\r\n        note_font.setPointSize(self.base_font_size)\r\n        note_font.setItalic(True)\r\n        note_label.setFont(note_font)\r\n        self.tab_layout.addWidget(note_label)\r\n        \r\n        # Set fixed heights for text boxes to make them more compact\r\n        for text_box in [self.personality_description, self.modifiers_text, self.care_tips]:\r\n            text_box.setMinimumHeight(150)\r\n            text_box.setMaximumHeight(200)\r\n\r\n    def update_from_brain_state(self, state):\r\n        \"\"\"Update personality info when brain state changes\"\"\"\r\n        if 'personality' in state:\r\n            self.update_personality_display(state['personality'])\r\n            \r\n    def update_personality_display(self, personality):\r\n        \"\"\"Update all personality display elements\"\"\"\r\n        # Get translated personality name\r\n        personality_name = self.loc.get_personality_name(personality)\r\n        \r\n        # Set personality type label\r\n        self.personality_type_label.setText(f\"{self.loc.get('squid_personality')}: {personality_name}\")\r\n        \r\n        # Set personality modifier label\r\n        self.personality_modifier_label.setText(f\"{self.loc.get('personality_modifier')}: {self.get_personality_modifier(personality)}\")\r\n        \r\n        # Set description text\r\n        self.personality_description.setPlainText(self.get_personality_description(personality))\r\n        \r\n        # Set modifiers text\r\n        self.modifiers_text.setPlainText(self.get_personality_modifiers(personality))\r\n        \r\n        # Set care tips text\r\n        self.care_tips.setPlainText(self.get_care_tips(personality))\r\n        \r\n    def get_personality_description(self, personality):\r\n        \"\"\"Get translated personality description\"\"\"\r\n        return self.loc.get_personality_description(personality)\r\n\r\n    def get_personality_modifier(self, personality):\r\n        \"\"\"Get translated personality modifier text\"\"\"\r\n        return self.loc.get_personality_modifier_text(personality)\r\n    \r\n    def get_care_tips(self, personality):\r\n        \"\"\"Get translated care tips\"\"\"\r\n        return self.loc.get_care_tips(personality)\r\n\r\n    def get_personality_modifiers(self, personality):\r\n        \"\"\"Get translated detailed personality modifiers\"\"\"\r\n        return self.loc.get_personality_modifiers(personality)\r\n"
  },
  {
    "path": "src/brain_render_worker.py",
    "content": "\"\"\"\r\nbrain_render_worker.py - Offscreen rendering worker for brain visualization\r\n\r\nRenders the neural network visualization to a QImage in a background thread,\r\nthen the main thread just blits that image. This dramatically improves UI\r\nresponsiveness by moving expensive drawing operations off the main thread.\r\n\r\nUsage:\r\n    1. Create BrainRenderWorker with reference to brain widget\r\n    2. Call request_render() when state changes\r\n    3. Connect to render_complete signal to receive the rendered QImage\r\n    4. In paintEvent, just draw the cached image\r\n\"\"\"\r\n\r\nimport time\r\nimport math\r\nimport re\r\nfrom typing import Dict, List, Tuple, Optional, Any\r\nfrom dataclasses import dataclass, field\r\nfrom PyQt5.QtCore import QThread, pyqtSignal, QMutex, QMutexLocker, QWaitCondition, QSize, Qt, QPointF, QRectF\r\nfrom PyQt5.QtGui import QImage, QPainter, QColor, QPen, QBrush, QFont, QPolygonF\r\n\r\n\r\n@dataclass\r\nclass RenderState:\r\n    \"\"\"Snapshot of all data needed to render the brain\"\"\"\r\n    # Neuron data\r\n    neuron_positions: Dict[str, Tuple[float, float]] = field(default_factory=dict)\r\n    neuron_states: Dict[str, float] = field(default_factory=dict)\r\n    state_colors: Dict[str, Tuple[int, int, int]] = field(default_factory=dict)\r\n    neuron_shapes: Dict[str, str] = field(default_factory=dict)\r\n    \r\n    # Pre-calculated localized labels\r\n    neuron_labels: Dict[str, str] = field(default_factory=dict)\r\n    \r\n    # Connection data\r\n    weights: Dict[Tuple[str, str], float] = field(default_factory=dict)\r\n    communication_events: Dict[str, float] = field(default_factory=dict)\r\n    \r\n    # Visibility\r\n    visible_neurons: set = field(default_factory=set)\r\n    excluded_neurons: set = field(default_factory=set)\r\n    \r\n    # Animation state\r\n    link_opacities: Dict[Tuple[str, str], float] = field(default_factory=dict)\r\n    animation_time: float = 0.0\r\n    \r\n    # [NEW] Active weight animations for Hebbian learning\r\n    weight_animations: List[Dict] = field(default_factory=list)\r\n    \r\n    # Display settings\r\n    show_weights: bool = False\r\n    is_tutorial_mode: bool = False\r\n    \r\n    # ===== ANIMATION STYLE PARAMETERS =====\r\n    # Base visual settings\r\n    anim_background_colour: Tuple[int, int, int] = (30, 30, 40)\r\n    anim_line_base_width: float = 1.0\r\n    anim_line_col_pos: Tuple[int, int, int] = (100, 255, 100)\r\n    anim_line_col_neg: Tuple[int, int, int] = (255, 100, 100)\r\n    anim_line_alpha: int = 180\r\n    anim_line_style: int = 0  # Qt.SolidLine\r\n    \r\n    # Weight-based thickness\r\n    weight_thickness_enabled: bool = False\r\n    weight_thickness_min: float = 1.0\r\n    weight_thickness_max: float = 2.0\r\n    weight_thickness_power: float = 1.0\r\n    \r\n    # Scroll settings\r\n    scroll_enabled: bool = False\r\n    scroll_dot_count: int = 3\r\n    scroll_dot_size: float = 6.0\r\n    scroll_dot_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    scroll_dot_alpha: int = 200\r\n    scroll_speed_range: Tuple[float, float] = (1.5, 4.0)\r\n    \r\n    # Pulse effects\r\n    anim_pulse_enabled: bool = True\r\n    anim_pulse_colour: Tuple[int, int, int] = (255, 255, 255)\r\n    anim_pulse_alpha: int = 180\r\n    anim_pulse_diameter: float = 8.0\r\n    \r\n    # Glow effects\r\n    anim_glow_enabled: bool = True\r\n    anim_glow_colour: Tuple[int, int, int] = (255, 255, 200)\r\n    anim_glow_alpha: int = 60\r\n    anim_glow_fade_threshold: float = 0.5\r\n    \r\n    # Communication glow\r\n    anim_comm_glow_enabled: bool = False\r\n    \r\n    # Layers\r\n    layers: List[Dict] = field(default_factory=list)\r\n    \r\n    # Widget size\r\n    width: int = 1024\r\n    height: int = 768\r\n    \r\n    # Hover state\r\n    hovered_neuron: Optional[str] = None\r\n    hover_value_display_active: bool = False\r\n    \r\n    # Font settings\r\n    neuron_label_font_size: int = 6\r\n    \r\n    # Timestamp for cache invalidation\r\n    timestamp: float = field(default_factory=time.time)\r\n\r\n\r\nclass BrainRenderWorker(QThread):\r\n    \"\"\"\r\n    Background worker that renders brain visualization to QImage.\r\n    \r\n    Signals:\r\n        render_complete: Emitted when a new frame is ready\r\n            - QImage: The rendered frame\r\n            - float: Render time in ms\r\n    \"\"\"\r\n    \r\n    render_complete = pyqtSignal(QImage, float)\r\n    \r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        \r\n        # Thread control\r\n        self._running = True\r\n        self._render_requested = False\r\n        \r\n        # State mutex\r\n        self._state_mutex = QMutex()\r\n        self._render_condition = QWaitCondition()\r\n        \r\n        # Current render state\r\n        self._render_state: Optional[RenderState] = None\r\n        \r\n        # Cached image\r\n        self._cached_image: Optional[QImage] = None\r\n        self._last_render_time = 0.0\r\n        \r\n        # Rendering frequency control\r\n        self._min_render_interval = 1.0 / 10.0  # 10 FPS max\r\n        self._last_render_request = 0.0\r\n        \r\n        # Performance stats\r\n        self._render_count = 0\r\n        self._total_render_time = 0.0\r\n    \r\n    def stop(self):\r\n        \"\"\"Stop the worker thread gracefully\"\"\"\r\n        self._running = False\r\n        self._state_mutex.lock()\r\n        self._render_condition.wakeAll()\r\n        self._state_mutex.unlock()\r\n    \r\n    def request_render(self, state: RenderState):\r\n        \"\"\"\r\n        Request a new render with the given state.\r\n        \r\n        Throttles requests to prevent overwhelming the thread.\r\n        \"\"\"\r\n        current_time = time.time()\r\n        \r\n        # Throttle render requests\r\n        if current_time - self._last_render_request < self._min_render_interval:\r\n            return\r\n        \r\n        self._last_render_request = current_time\r\n        \r\n        with QMutexLocker(self._state_mutex):\r\n            self._render_state = state\r\n            self._render_requested = True\r\n            self._render_condition.wakeOne()\r\n    \r\n    def get_cached_image(self) -> Optional[QImage]:\r\n        \"\"\"Get the most recently rendered image (thread-safe)\"\"\"\r\n        with QMutexLocker(self._state_mutex):\r\n            return self._cached_image\r\n    \r\n    def get_stats(self) -> Dict[str, Any]:\r\n        \"\"\"Get rendering statistics\"\"\"\r\n        avg_time = self._total_render_time / max(1, self._render_count)\r\n        return {\r\n            'render_count': self._render_count,\r\n            'avg_render_time_ms': avg_time,\r\n            'last_render_time_ms': self._last_render_time\r\n        }\r\n    \r\n    def run(self):\r\n        \"\"\"Main worker loop\"\"\"\r\n        print(\"🧠 BrainRenderWorker started\")\r\n        \r\n        while self._running:\r\n            state_to_render = None\r\n            \r\n            # Wait for render request\r\n            self._state_mutex.lock()\r\n            try:\r\n                if not self._render_requested and self._running:\r\n                    # Wait up to 100ms for a render request\r\n                    self._render_condition.wait(self._state_mutex, 100)\r\n                \r\n                if self._render_requested and self._render_state:\r\n                    state_to_render = self._render_state\r\n                    self._render_requested = False\r\n            finally:\r\n                self._state_mutex.unlock()\r\n            \r\n            # Perform render if we have state\r\n            if state_to_render:\r\n                start_time = time.perf_counter()\r\n                \r\n                try:\r\n                    image = self._render_frame(state_to_render)\r\n                    \r\n                    # Cache the image\r\n                    with QMutexLocker(self._state_mutex):\r\n                        self._cached_image = image\r\n                    \r\n                    # Calculate render time\r\n                    render_time = (time.perf_counter() - start_time) * 1000\r\n                    self._last_render_time = render_time\r\n                    self._render_count += 1\r\n                    self._total_render_time += render_time\r\n                    \r\n                    # Emit signal with rendered image\r\n                    self.render_complete.emit(image, render_time)\r\n                    \r\n                except Exception as e:\r\n                    print(f\"🧠 Render error: {e}\")\r\n                    import traceback\r\n                    traceback.print_exc()\r\n        \r\n        print(\"🧠 BrainRenderWorker stopped\")\r\n    \r\n    def _render_frame(self, state: RenderState) -> QImage:\r\n        \"\"\"Render a complete frame to QImage\"\"\"\r\n        # Create image with proper size\r\n        image = QImage(state.width, state.height, QImage.Format_ARGB32)\r\n        image.fill(QColor(*state.anim_background_colour))\r\n        \r\n        painter = QPainter(image)\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        painter.setRenderHint(QPainter.TextAntialiasing)\r\n        \r\n        try:\r\n            # Calculate scaling (same logic as brain_widget)\r\n            indicator_space = 0  # No indicator pills\r\n            base_width = 1024\r\n            base_height = 768 - indicator_space\r\n            \r\n            scale_x = state.width / base_width\r\n            scale_y = (state.height - indicator_space) / max(1, base_height)\r\n            scale = max(0.01, min(scale_x, scale_y))\r\n            \r\n            # Center horizontally\r\n            offset_x = 0\r\n            if scale_x > scale_y:\r\n                content_width = base_width * scale\r\n                offset_x = (state.width - content_width) / 2\r\n            \r\n            painter.translate(offset_x, indicator_space)\r\n            painter.scale(scale, scale)\r\n            \r\n            # Draw layers\r\n            self._draw_layers(painter, state, 1.0)\r\n            \r\n            # Draw connections\r\n            self._draw_connections(painter, state, scale)\r\n            \r\n            # Draw neurons\r\n            self._draw_neurons(painter, state, scale)\r\n            \r\n        finally:\r\n            painter.end()\r\n        \r\n        return image\r\n    \r\n    def _draw_layers(self, painter: QPainter, state: RenderState, scale: float):\r\n        \"\"\"Draw layer background rectangles\"\"\"\r\n        if not state.layers:\r\n            return\r\n        \r\n        for layer in state.layers:\r\n            y_pos = layer.get('y_position', 0)\r\n            name = layer.get('name', 'Layer')\r\n            layer_type = layer.get('layer_type', 'hidden')\r\n            \r\n            rect_height = 120\r\n            rect_top = y_pos - rect_height / 2\r\n            rect_left = -200\r\n            rect_width = 2000\r\n            \r\n            # Layer colors\r\n            if layer_type == 'input':\r\n                color = QColor(220, 255, 220, 30)\r\n                border = QColor(180, 220, 180, 60)\r\n            elif layer_type == 'output':\r\n                color = QColor(255, 220, 220, 30)\r\n                border = QColor(220, 180, 180, 60)\r\n            else:\r\n                color = QColor(230, 230, 255, 40)\r\n                border = QColor(200, 200, 240, 60)\r\n            \r\n            painter.setBrush(QBrush(color))\r\n            painter.setPen(QPen(border, 1, Qt.DashLine))\r\n            painter.drawRect(QRectF(rect_left, rect_top, rect_width, rect_height))\r\n    \r\n    def _get_neuron_animation_color(self, state: RenderState, neuron_name: str, current_time: float):\r\n        \"\"\"\r\n        Check if a neuron is currently involved in an active weight animation.\r\n        Returns a QColor with pulsing alpha if active, None otherwise.\r\n        \"\"\"\r\n        for anim in state.weight_animations:\r\n            # Check if this neuron is part of the animation pair\r\n            if (anim.get('neuron1') == neuron_name or anim.get('neuron2') == neuron_name):\r\n                elapsed = current_time - anim['start_time']\r\n                duration = anim['duration']\r\n                \r\n                if 0 <= elapsed < duration:\r\n                    progress = elapsed / duration\r\n                    \r\n                    # Create pulsing alpha effect (fade in and out)\r\n                    # Use sine wave for smooth pulsing\r\n                    pulse_factor = math.sin(progress * math.pi)\r\n                    \r\n                    # Get the animation color\r\n                    r, g, b = anim['color']\r\n                    \r\n                    # Set alpha based on pulse factor (0-255 range)\r\n                    alpha = int(255 * pulse_factor)\r\n                    \r\n                    return QColor(r, g, b, alpha)\r\n        \r\n        return None\r\n    \r\n    def _draw_connections(self, painter: QPainter, state: RenderState, scale: float):\r\n        \"\"\"\r\n        Draw all neural connections with scrolling arrow animations for Hebbian learning.\r\n        Includes specific coloring for excitatory (green) vs inhibitory (red) weights\r\n        and weight-based thickness clamping (max 15px or style-defined).\r\n        \"\"\"\r\n        current_time = state.animation_time\r\n        \r\n        for (src, dst), weight in state.weights.items():\r\n            # Skip if neurons not visible or excluded\r\n            if src not in state.visible_neurons or dst not in state.visible_neurons:\r\n                continue\r\n            if src in state.excluded_neurons or dst in state.excluded_neurons:\r\n                continue\r\n            if src not in state.neuron_positions or dst not in state.neuron_positions:\r\n                continue\r\n            \r\n            # Get positions\r\n            src_pos = state.neuron_positions[src]\r\n            dst_pos = state.neuron_positions[dst]\r\n            \r\n            start = QPointF(src_pos[0], src_pos[1])\r\n            end = QPointF(dst_pos[0], dst_pos[1])\r\n            \r\n            # Get link opacity\r\n            key = (src, dst)\r\n            opacity = state.link_opacities.get(key, 1.0)\r\n            if opacity < 0.01:\r\n                continue\r\n            \r\n            # ===== CHECK FOR ACTIVE HEBBIAN ANIMATION =====\r\n            active_anim = None\r\n            active_anim_progress = 0.0\r\n            \r\n            for anim in state.weight_animations:\r\n                # Check match (undirected)\r\n                if anim['pair'] == (src, dst) or anim['pair'] == (dst, src):\r\n                    elapsed = current_time - anim['start_time']\r\n                    duration = anim['duration']\r\n                    \r\n                    if 0 <= elapsed < duration:\r\n                        active_anim = anim\r\n                        active_anim_progress = elapsed / duration\r\n                        break\r\n            \r\n            # ===== WEIGHT-BASED THICKNESS & COLOR =====\r\n            abs_weight = abs(weight)\r\n            \r\n            # 1. Base thickness calculation:\r\n            # Scale weight (0.0 to 1.0) to a range\r\n            # Define max pixel thickness for strongest connections based on style\r\n            if state.weight_thickness_enabled:\r\n                MAX_THICKNESS = state.weight_thickness_max\r\n            else:\r\n                MAX_THICKNESS = 15.0\r\n            \r\n            # Calculate thickness: Base + (Weight * Scalar), clamped to MAX_THICKNESS\r\n            # Ensure we don't have negative range if base > max\r\n            thickness_range = max(0.0, MAX_THICKNESS - state.anim_line_base_width)\r\n            calculated_thickness = state.anim_line_base_width + (abs_weight * thickness_range)\r\n            \r\n            # Clamp rigidly to MAX_THICKNESS\r\n            base_thickness = min(calculated_thickness, MAX_THICKNESS)\r\n            \r\n            # 2. Determine Color (Excitatory vs Inhibitory)\r\n            if weight >= 0:\r\n                # Excitatory = Green (using positive color from state)\r\n                base_color = QColor(*state.anim_line_col_pos)\r\n            else:\r\n                # Inhibitory = Red (using negative color from state)\r\n                base_color = QColor(*state.anim_line_col_neg)\r\n            \r\n            # Apply Opacity\r\n            base_color.setAlpha(int(state.anim_line_alpha * opacity))\r\n\r\n            # Base styling\r\n            line_width = base_thickness * scale\r\n            pen_style = Qt.SolidLine \r\n            \r\n            # Dashed line for negative, Dotted for very weak (optional visual aid)\r\n            if weight < 0:\r\n                pen_style = Qt.DashLine\r\n            if abs_weight < 0.1:\r\n                pen_style = Qt.DotLine\r\n            \r\n            # ===== ANIMATED LINE STYLING (during Hebbian cycle) =====\r\n            # Initialize current_thickness with base value\r\n            current_thickness = base_thickness\r\n            line_color = base_color\r\n            \r\n            if active_anim:\r\n                # Connection becomes bright during Hebbian cycle\r\n                r, g, b = active_anim['color']\r\n                line_color = QColor(r, g, b, 255)\r\n                \r\n                # Line thickness pulses during animation\r\n                anim_max_thickness = (MAX_THICKNESS + 3.0) * scale\r\n                \r\n                pulse_factor = math.sin(active_anim_progress * math.pi)\r\n                current_thickness = base_thickness + (pulse_factor * (anim_max_thickness - base_thickness))\r\n                \r\n                pen_style = Qt.SolidLine  # Always solid during animation\r\n            \r\n            line_width = max(1, int(current_thickness * scale))\r\n            \r\n            painter.setPen(QPen(line_color, line_width, pen_style, Qt.RoundCap))\r\n            painter.drawLine(start, end)\r\n            \r\n            # ===== DRAW SCROLLING ARROWS FOR MOVEMENT ILLUSION =====\r\n            if active_anim and active_anim_progress < 1.0:\r\n                # Calculate direction vector and angle\r\n                dx = end.x() - start.x()\r\n                dy = end.y() - start.y()\r\n                length = math.sqrt(dx*dx + dy*dy)\r\n                \r\n                if length > 10:  # Only draw arrows on longer connections\r\n                    # Normalize direction\r\n                    dir_x = dx / length\r\n                    dir_y = dy / length\r\n                    \r\n                    # Arrow size\r\n                    arrow_size = 12.0 * scale\r\n                    \r\n                    # Draw 4 arrows at different positions (trail effect)\r\n                    arrow_positions = [0.20, 0.40, 0.60, 0.80]\r\n                    \r\n                    # Offset based on animation progress to create movement\r\n                    progress_offset = active_anim_progress * 0.8\r\n                    \r\n                    for i, base_pos in enumerate(arrow_positions):\r\n                        # Calculate actual position offset by progress\r\n                        pos_offset = (base_pos + progress_offset) % 1.0\r\n                        \r\n                        # Arrow center point\r\n                        center_x = start.x() + pos_offset * dx\r\n                        center_y = start.y() + pos_offset * dy\r\n                        \r\n                        # Alpha fades for trail effect (255, 191, 128, 64)\r\n                        alpha = int(255 * (1.0 - (i * 0.25)))\r\n                        \r\n                        # Create arrow polygon (pointing along the line)\r\n                        arrow = QPolygonF()\r\n                        \r\n                        # Arrow points in direction of connection\r\n                        # Front point\r\n                        point_x = center_x + dir_x * arrow_size\r\n                        point_y = center_y + dir_y * arrow_size\r\n                        arrow.append(QPointF(point_x, point_y))\r\n                        \r\n                        # Back left\r\n                        back_x = center_x - dir_x * arrow_size * 0.5\r\n                        back_y = center_y - dir_y * arrow_size * 0.5\r\n                        left_x = back_x - dir_y * arrow_size * 0.5\r\n                        left_y = back_y + dir_x * arrow_size * 0.5\r\n                        arrow.append(QPointF(left_x, left_y))\r\n                        \r\n                        # Back right\r\n                        right_x = back_x + dir_y * arrow_size * 0.5\r\n                        right_y = back_y - dir_x * arrow_size * 0.5\r\n                        arrow.append(QPointF(right_x, right_y))\r\n                        \r\n                        # Fill arrow with color\r\n                        arrow_color = QColor(line_color)\r\n                        arrow_color.setAlpha(alpha)\r\n                        painter.setBrush(QBrush(arrow_color))\r\n                        painter.setPen(QPen(arrow_color, 1))\r\n                        painter.drawPolygon(arrow)\r\n            \r\n            # ===== DRAW WEIGHT TEXT =====\r\n            if state.show_weights and abs_weight > 0.1:\r\n                # Calculate angle for rotation\r\n                dx = end.x() - start.x()\r\n                dy = end.y() - start.y()\r\n                angle_deg = math.degrees(math.atan2(dy, dx))\r\n                \r\n                # Ensure text is readable\r\n                if angle_deg > 90:\r\n                    angle_deg -= 180\r\n                elif angle_deg < -90:\r\n                    angle_deg += 180\r\n                \r\n                midpoint = QPointF((start.x() + end.x()) / 2, (start.y() + end.y()) / 2)\r\n                text_str = f\"{weight:.2f}\"\r\n                \r\n                font_size = max(7, int(8 * scale))\r\n                padding = 4 * scale\r\n                \r\n                font = painter.font()\r\n                font.setPointSize(font_size)\r\n                font.setBold(True)\r\n                painter.setFont(font)\r\n                \r\n                fm = painter.fontMetrics()\r\n                text_w = fm.horizontalAdvance(text_str)\r\n                text_h = fm.height()\r\n                \r\n                painter.save()\r\n                painter.translate(midpoint)\r\n                painter.rotate(angle_deg)\r\n                \r\n                rect = QRectF(-text_w/2 - padding, -text_h/2, \r\n                            text_w + padding*2, text_h)\r\n                \r\n                painter.setBrush(QBrush(QColor(255, 255, 255, 220)))\r\n                painter.setPen(QPen(QColor(100, 100, 100, 150), 1))\r\n                painter.drawRoundedRect(rect, 4, 4)\r\n                \r\n                # Text color matches line color logic\r\n                text_color = QColor(0, 100, 0) if weight >= 0 else QColor(150, 0, 0)\r\n                painter.setPen(text_color)\r\n                painter.drawText(rect, Qt.AlignCenter, text_str)\r\n                \r\n                painter.restore()\r\n                \r\n    \r\n    def _draw_neurons(self, painter: QPainter, state: RenderState, scale: float):\r\n        \"\"\"Draw all neurons with localized labels, connector relay animations, and Hebbian pulse effects\"\"\"\r\n        from .brain_constants import BINARY_NEURONS\r\n        \r\n        # Hardcoded list of core neurons to determine font sizing\r\n        CORE_NEURONS = {\"hunger\", \"happiness\", \"cleanliness\", \"sleepiness\", \r\n                        \"satisfaction\", \"anxiety\", \"curiosity\", \"can_see_food\"}\r\n\r\n        # Set up default font\r\n        font = QFont(\"Arial\", state.neuron_label_font_size)\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        fm = painter.fontMetrics()\r\n\r\n        radius = 20 * scale\r\n        current_time = state.animation_time\r\n\r\n        for name, pos in state.neuron_positions.items():\r\n            if name in state.excluded_neurons:\r\n                continue\r\n            if name not in state.visible_neurons:\r\n                continue\r\n\r\n            x, y = pos\r\n            raw_value = state.neuron_states.get(name, 50)\r\n            shape = state.neuron_shapes.get(name, 'circle')\r\n            \r\n            # ========== CHECK FOR ACTIVE HEBBIAN ANIMATION ==========\r\n            animation_color = self._get_neuron_animation_color(state, name, current_time)\r\n            \r\n            # Check if this neuron is currently being used as a relay in any animation\r\n            is_active_connector = False\r\n            connector_pulse_alpha = 0\r\n            pulse_size_multiplier = 1.0\r\n\r\n            for anim in state.weight_animations:\r\n                if anim.get('is_segment') and anim.get('final_target'):\r\n                    # Check if this neuron is the connector in a staggered animation\r\n                    connector_in_anim = (anim['neuron1'] == name or anim['neuron2'] == name)\r\n                    \r\n                    if connector_in_anim:\r\n                        elapsed = current_time - anim['start_time']\r\n                        if 0 <= elapsed < anim['duration']:\r\n                            # Connector becomes bright white when relaying\r\n                            pulse_phase = elapsed / anim['duration']\r\n                            if pulse_phase > 0.7:  # Last part of segment - relay burst\r\n                                connector_pulse_alpha = int(255 * (1.0 - pulse_phase))\r\n                                pulse_size_multiplier = 1.0 + (1.0 - pulse_phase) * 0.5  # Grow 50% at burst\r\n                                is_active_connector = True\r\n                                break\r\n\r\n            # ---------- BINARY NEURONS ----------\r\n            if name in BINARY_NEURONS:\r\n                value = 100.0 if float(raw_value) > 50 else 0.0\r\n                is_active = value > 50\r\n                \r\n                # Apply animation color if active\r\n                if animation_color:\r\n                    color = animation_color\r\n                else:\r\n                    color = QColor(0, 255, 0) if is_active else QColor(255, 0, 0)\r\n\r\n                painter.setBrush(QBrush(color))\r\n                painter.setPen(QPen(QColor(0, 0, 0), max(1, int(2 * scale))))\r\n                size = radius * 1.8\r\n                rect = QRectF(x - size/2, y - size/2, size, size)\r\n                painter.drawRect(rect)\r\n\r\n                # [NEW] Draw Symbol inside Binary Neuron\r\n                symbol = \"✓\" if is_active else \"✗\"\r\n                painter.save()\r\n                symbol_font = QFont(\"Arial\", int(size * 0.7))\r\n                symbol_font.setBold(True)\r\n                painter.setFont(symbol_font)\r\n                painter.setPen(QColor(0, 0, 0))\r\n                painter.drawText(rect, Qt.AlignCenter, symbol)\r\n                painter.restore()\r\n\r\n                if name == 'can_see_food':\r\n                    display_name = state.neuron_labels.get(name, name)\r\n                    \r\n                    # Smaller Font\r\n                    small_font = QFont(font)\r\n                    small_font.setPointSize(max(4, int(state.neuron_label_font_size * 0.75 * scale)))\r\n                    painter.setFont(small_font)\r\n                    sfm = painter.fontMetrics()\r\n\r\n                    # Calculate Dimensions\r\n                    text_width = sfm.horizontalAdvance(display_name)\r\n                    padding = 4 * scale\r\n                    rect_width = text_width + padding * 2\r\n                    rect_height = sfm.height() + 2\r\n\r\n                    text_rect = QRectF(\r\n                        x - rect_width / 2,\r\n                        y + size/2 + 3 * scale,\r\n                        rect_width,\r\n                        rect_height\r\n                    )\r\n\r\n                    # Draw Black Background\r\n                    painter.setBrush(QBrush(QColor(0, 0, 0)))\r\n                    painter.setPen(Qt.NoPen)\r\n                    painter.drawRoundedRect(text_rect, 2, 2)\r\n\r\n                    # Draw White Text\r\n                    painter.setPen(QColor(255, 255, 255))\r\n                    painter.drawText(text_rect, Qt.AlignCenter, display_name)\r\n                    \r\n                    # Restore standard font\r\n                    painter.setFont(font)\r\n\r\n                continue\r\n\r\n            # ---------- DIAMOND ----------\r\n            if shape == 'diamond':\r\n                # Apply animation color if active\r\n                if animation_color:\r\n                    color = animation_color\r\n                else:\r\n                    color = QColor(*state.state_colors.get(name, (152, 251, 152)))\r\n                self._draw_polygon(painter, x, y, 4, radius, color, rotation=0)\r\n\r\n            # ---------- SQUARE ----------\r\n            elif shape == 'square':\r\n                # Apply animation color if active\r\n                if animation_color:\r\n                    color = animation_color\r\n                else:\r\n                    color = QColor(*state.state_colors.get(name, (152, 251, 152)))\r\n                self._draw_polygon(painter, x, y, 4, radius, color, rotation=45)\r\n\r\n            # ---------- TRIANGLE ----------\r\n            elif shape == 'triangle':\r\n                # Apply animation color if active\r\n                if animation_color:\r\n                    color = animation_color\r\n                else:\r\n                    color = QColor(*state.state_colors.get(name, (255, 255, 150)))\r\n                self._draw_polygon(painter, x, y, 3, radius, color)\r\n\r\n            # ---------- CONNECTOR (HEXAGON) ----------\r\n            elif shape == 'hexagon' or name.startswith('connector_'):\r\n                # Use bright purple base, but pulse white during animation or apply animation color\r\n                if animation_color:\r\n                    # Hebbian animation takes priority\r\n                    color = animation_color\r\n                    self._draw_polygon(painter, x, y, 6, radius * pulse_size_multiplier, \r\n                                    color, rotation=0)\r\n                elif is_active_connector:\r\n                    # Pulse with white color during relay\r\n                    pulse_color = QColor(255, 255, 255, connector_pulse_alpha)\r\n                    self._draw_polygon(painter, x, y, 6, radius * pulse_size_multiplier, \r\n                                    pulse_color, rotation=0)\r\n                    \r\n                    # Add extra glow ring during burst\r\n                    if connector_pulse_alpha > 128:\r\n                        glow_pen = QPen(QColor(255, 255, 255, connector_pulse_alpha // 2), 3)\r\n                        painter.setPen(glow_pen)\r\n                        painter.setBrush(Qt.NoBrush)\r\n                        painter.drawEllipse(QPointF(x, y), radius * 1.5, radius * 1.5)\r\n                else:\r\n                    # Normal black connector appearance\r\n                    color = QColor(0, 0, 0)\r\n                    self._draw_polygon(painter, x, y, 6, radius, color, rotation=0)\r\n\r\n                painter.save()\r\n                c_font = QFont(\"Arial\", int(14 * scale))\r\n                c_font.setBold(True)\r\n                painter.setFont(c_font)\r\n                \r\n                # Use white text, but make it brighter during pulse\r\n                if (is_active_connector and connector_pulse_alpha > 200) or animation_color:\r\n                    painter.setPen(QColor(255, 255, 255, 255))\r\n                else:\r\n                    painter.setPen(QColor(255, 255, 255, 200))\r\n\r\n                rect = QRectF(x - radius, y - radius, radius * 2, radius * 2)\r\n                painter.drawText(rect, Qt.AlignCenter, \"c\")\r\n                painter.restore()\r\n\r\n                # IMPORTANT: connector neurons do NOT draw external labels\r\n                continue\r\n\r\n            # ---------- DEFAULT CIRCLE ----------\r\n            else:\r\n                # Apply animation color if active\r\n                if animation_color:\r\n                    color = animation_color\r\n                elif name in state.state_colors:\r\n                    color = QColor(*state.state_colors[name])\r\n                else:\r\n                    color = QColor(64, 64, 64)\r\n\r\n                painter.setBrush(QBrush(color))\r\n                painter.setPen(QPen(QColor(0, 0, 0), max(1, int(2 * scale))))\r\n                painter.drawEllipse(QPointF(x, y), radius, radius)\r\n\r\n            # ---------- LABEL ----------\r\n            display_name = state.neuron_labels.get(\r\n                name, name.replace(\"_\", \" \").title()\r\n            )\r\n\r\n            # [NEW] Font Scaling Logic\r\n            is_neurogenesis = name not in CORE_NEURONS\r\n            effective_size = state.neuron_label_font_size * 0.75 if is_neurogenesis else state.neuron_label_font_size\r\n            \r\n            # Apply font size for this label\r\n            label_font = QFont(\"Arial\", int(effective_size * scale))\r\n            label_font.setBold(True)\r\n            painter.setFont(label_font)\r\n            local_fm = painter.fontMetrics()\r\n\r\n            text_width = local_fm.horizontalAdvance(display_name)\r\n            padding = 10 * scale\r\n            rect_width = text_width + padding * 2\r\n            rect_height = local_fm.height() + 4\r\n\r\n            text_rect = QRectF(\r\n                x - rect_width / 2,\r\n                y + radius + 5 * scale,\r\n                rect_width,\r\n                rect_height\r\n            )\r\n\r\n            painter.setBrush(QBrush(QColor(26, 26, 26, 200)))\r\n            painter.setPen(Qt.NoPen)\r\n            painter.drawRoundedRect(text_rect, 4, 4)\r\n\r\n            painter.setPen(QColor(224, 224, 224))\r\n            painter.drawText(text_rect, Qt.AlignCenter, display_name)\r\n            \r\n            # Restore base font for next iteration\r\n            painter.setFont(font)\r\n\r\n    \r\n    def _draw_polygon(self, painter: QPainter, x: float, y: float, \r\n                      sides: int, radius: float, color: QColor, rotation: float = 0):\r\n        \"\"\"Draw a polygon neuron shape\"\"\"\r\n        painter.save()\r\n        painter.translate(x, y)\r\n        painter.rotate(rotation)\r\n        \r\n        painter.setBrush(QBrush(color))\r\n        painter.setPen(QPen(QColor(0, 0, 0)))\r\n        \r\n        polygon = QPolygonF()\r\n        angle_step = 360.0 / sides\r\n        for i in range(sides):\r\n            angle = math.radians(i * angle_step - 90)\r\n            polygon.append(QPointF(radius * math.cos(angle), radius * math.sin(angle)))\r\n        \r\n        painter.drawPolygon(polygon)\r\n        painter.restore()\r\n\r\n\r\ndef create_render_state_from_widget(brain_widget) -> RenderState:\r\n    \"\"\"\r\n    Helper function to create a RenderState from a BrainWidget instance.\r\n    Call this from the main thread before requesting a render.\r\n    \"\"\"\r\n    # Import Localisation here to avoid circular imports at module level\r\n    from .localisation import Localisation\r\n    loc = Localisation.instance()\r\n\r\n    state = RenderState()\r\n    \r\n    # Copy neuron data\r\n    state.neuron_positions = dict(brain_widget.neuron_positions)\r\n    state.neuron_states = dict(brain_widget.state)\r\n    state.state_colors = dict(getattr(brain_widget, 'state_colors', {}))\r\n    state.neuron_shapes = dict(getattr(brain_widget, 'neuron_shapes', {}))\r\n    \r\n    # --- Pre-calculate Localized Labels on Main Thread ---\r\n    labels = {}\r\n    \r\n    # We only need to calculate labels for neurons that might be drawn\r\n    visible = getattr(brain_widget, 'visible_neurons', brain_widget.neuron_positions.keys())\r\n    excluded = getattr(brain_widget, 'excluded_neurons', set())\r\n    \r\n    for name in state.neuron_positions.keys():\r\n        if name in excluded:\r\n            continue\r\n            \r\n        # 1. Try exact key lookup\r\n        display_name = loc.get(name)\r\n        \r\n        # 2. Fallback: space-separated key\r\n        if display_name == name:\r\n            space_key = name.replace(\"_\", \" \")\r\n            display_name = loc.get(space_key)\r\n            if display_name == space_key:\r\n                display_name = None # Mark as not found yet\r\n        \r\n        # 3. Fallback: Neurogenesis pattern (e.g., novelty_1 -> Novelty 1)\r\n        if not display_name:\r\n            match = re.match(r\"^([a-z_]+)_(\\d+)$\", name)\r\n            if match:\r\n                base = match.group(1)\r\n                idx = match.group(2)\r\n                base_loc = loc.get(base)\r\n                if base_loc != base:\r\n                    display_name = f\"{base_loc} {idx}\"\r\n                else:\r\n                    display_name = f\"{base.replace('_', ' ').title()} {idx}\"\r\n        \r\n        # 4. Final Fallback: Title Case\r\n        if not display_name:\r\n            display_name = name.replace(\"_\", \" \").title()\r\n            \r\n        labels[name] = display_name\r\n    \r\n    state.neuron_labels = labels\r\n    # -----------------------------------------------------\r\n\r\n    # Copy connection data\r\n    state.weights = dict(brain_widget.weights)\r\n    state.communication_events = dict(getattr(brain_widget, 'communication_events', {}))\r\n    \r\n    # Copy visibility\r\n    state.visible_neurons = set(visible)\r\n    state.excluded_neurons = set(excluded)\r\n    \r\n    # Copy animation state\r\n    state.link_opacities = dict(getattr(brain_widget, '_link_opacities', {}))\r\n    state.animation_time = time.time()\r\n    \r\n    # [NEW] Copy active weight animations for Hebbian learning\r\n    state.weight_animations = [dict(anim) for anim in getattr(brain_widget, 'weight_animations', [])]\r\n    \r\n    # Copy display settings\r\n    state.show_weights = getattr(brain_widget, 'show_weights', False)\r\n    state.is_tutorial_mode = getattr(brain_widget, 'is_tutorial_mode', False)\r\n    \r\n    # ===== ANIMATION STYLE PARAMETERS =====\r\n    # Base visual settings\r\n    state.anim_background_colour = getattr(brain_widget, 'anim_background_colour', (30, 30, 40))\r\n    state.anim_line_base_width = getattr(brain_widget, 'anim_line_base_width', 1.0)\r\n    state.anim_line_col_pos = getattr(brain_widget, 'anim_line_col_pos', (100, 255, 100))\r\n    state.anim_line_col_neg = getattr(brain_widget, 'anim_line_col_neg', (255, 100, 100))\r\n    state.anim_line_alpha = getattr(brain_widget, 'anim_line_alpha', 180)\r\n    state.anim_line_style = getattr(brain_widget, 'anim_line_style', 0)  # Qt.SolidLine\r\n    \r\n    # Weight-based thickness\r\n    state.weight_thickness_enabled = getattr(brain_widget, 'weight_thickness_enabled', False)\r\n    state.weight_thickness_min = getattr(brain_widget, 'weight_thickness_min', 1.0)\r\n    state.weight_thickness_max = getattr(brain_widget, 'weight_thickness_max', 2.0)\r\n    state.weight_thickness_power = getattr(brain_widget, 'weight_thickness_power', 1.0)\r\n    \r\n    # Scroll settings\r\n    state.scroll_enabled = getattr(brain_widget, 'anim_scroll_enabled', False)\r\n    state.scroll_dot_count = getattr(brain_widget, 'anim_scroll_dot_count', 3)\r\n    state.scroll_dot_size = getattr(brain_widget, 'anim_scroll_dot_size', 6.0)\r\n    state.scroll_dot_colour = getattr(brain_widget, 'anim_scroll_dot_colour', (255, 255, 255))\r\n    state.scroll_dot_alpha = getattr(brain_widget, 'anim_scroll_dot_alpha', 200)\r\n    state.scroll_speed_range = getattr(brain_widget, 'anim_scroll_speed_range', (1.5, 4.0))\r\n    \r\n    # Pulse effects\r\n    state.anim_pulse_enabled = getattr(brain_widget, 'anim_pulse_enabled', True)\r\n    state.anim_pulse_colour = getattr(brain_widget, 'anim_pulse_colour', (255, 255, 255))\r\n    state.anim_pulse_alpha = getattr(brain_widget, 'anim_pulse_alpha', 180)\r\n    state.anim_pulse_diameter = getattr(brain_widget, 'anim_pulse_diameter', 8.0)\r\n    \r\n    # Glow effects\r\n    state.anim_glow_enabled = getattr(brain_widget, 'anim_glow_enabled', True)\r\n    state.anim_glow_colour = getattr(brain_widget, 'anim_glow_colour', (255, 255, 200))\r\n    state.anim_glow_alpha = getattr(brain_widget, 'anim_glow_alpha', 60)\r\n    state.anim_glow_fade_threshold = getattr(brain_widget, 'anim_glow_fade_threshold', 0.5)\r\n    \r\n    # Communication glow\r\n    state.anim_comm_glow_enabled = getattr(brain_widget, 'anim_comm_glow_enabled', False)\r\n    \r\n    # Layers\r\n    state.layers = list(getattr(brain_widget, 'layers', []))\r\n    \r\n    # Widget size\r\n    state.width = brain_widget.width()\r\n    state.height = brain_widget.height()\r\n    \r\n    # Hover state\r\n    state.hovered_neuron = getattr(brain_widget, 'hovered_neuron', None)\r\n    state.hover_value_display_active = getattr(brain_widget, 'hover_value_display_active', False)\r\n    \r\n    # Font settings\r\n    state.neuron_label_font_size = getattr(brain_widget, 'neuron_label_font_size', 6)\r\n    \r\n    return state"
  },
  {
    "path": "src/brain_state_bridge.py",
    "content": "\"\"\"\r\nBrain State Bridge - Communication layer between running game and Brain Designer\r\n\r\nThis module provides a mechanism for the Brain Designer to detect when\r\nthe main Dosidicus game is running and import the exact brain state\r\nbeing displayed in the game's brain widget.\r\n\r\nThe game writes its current brain state to a shared file, and the designer\r\nreads from it on startup.\r\n\"\"\"\r\n\r\nimport json\r\nimport os\r\nimport time\r\nfrom pathlib import Path\r\nfrom typing import Dict, List, Tuple, Optional, Any\r\nfrom dataclasses import dataclass\r\n\r\n\r\n# Shared state file location - in user's home directory\r\ndef get_bridge_directory() -> Path:\r\n    \"\"\"Get the directory for bridge files.\"\"\"\r\n    # Use a hidden directory in the user's home folder\r\n    bridge_dir = Path.home() / \".dosidicus\" / \"bridge\"\r\n    bridge_dir.mkdir(parents=True, exist_ok=True)\r\n    return bridge_dir\r\n\r\n\r\ndef get_state_file_path() -> Path:\r\n    \"\"\"Get the path to the shared brain state file.\"\"\"\r\n    return get_bridge_directory() / \"active_brain_state.json\"\r\n\r\n\r\ndef get_lock_file_path() -> Path:\r\n    \"\"\"Get the path to the game running lock file.\"\"\"\r\n    return get_bridge_directory() / \"game_running.lock\"\r\n\r\n\r\n# ============================================================================\r\n# GAME-SIDE FUNCTIONS (called by brain_widget / brain_tool)\r\n# ============================================================================\r\n\r\ndef export_brain_state(\r\n    neuron_positions: Dict[str, Tuple[float, float]],\r\n    weights: Dict[Tuple[str, str], float],\r\n    state: Dict[str, Any],\r\n    visible_neurons: set = None,\r\n    excluded_neurons: list = None,\r\n    layers: list = None,\r\n    output_bindings: list = None\r\n) -> bool:\r\n    \"\"\"\r\n    Export the current brain state from the game to the shared file.\r\n    \r\n    This should be called by the game periodically or on state changes.\r\n    \r\n    Args:\r\n        neuron_positions: Dict mapping neuron name to (x, y) position\r\n        weights: Dict mapping (source, target) tuple to weight value\r\n        state: Dict mapping neuron name to current activation value\r\n        visible_neurons: Set of currently visible neuron names\r\n        excluded_neurons: List of neurons excluded from display\r\n        layers: Optional layer structure\r\n        output_bindings: Optional output bindings list\r\n        \r\n    Returns:\r\n        True if export was successful, False otherwise\r\n    \"\"\"\r\n    try:\r\n        # Convert weights dict with tuple keys to serializable format\r\n        serializable_weights = {}\r\n        for (src, dst), weight in weights.items():\r\n            key = f\"{src}->{dst}\"\r\n            serializable_weights[key] = weight\r\n        \r\n        export_data = {\r\n            \"version\": \"1.0\",\r\n            \"format\": \"live_brain_state\",\r\n            \"timestamp\": time.time(),\r\n            \"pid\": os.getpid(),\r\n            \"neurons\": {},\r\n            \"connections\": serializable_weights,\r\n            \"state\": {},\r\n            \"excluded_neurons\": list(excluded_neurons) if excluded_neurons else [],\r\n            \"layers\": layers or [],\r\n            \"output_bindings\": output_bindings or []\r\n        }\r\n        \r\n        # Build neurons dict with position and state info\r\n        for name, pos in neuron_positions.items():\r\n            if visible_neurons is not None and name not in visible_neurons:\r\n                continue  # Skip non-visible neurons\r\n            \r\n            export_data[\"neurons\"][name] = {\r\n                \"position\": list(pos) if isinstance(pos, tuple) else pos,\r\n                \"activation\": state.get(name, 0)\r\n            }\r\n        \r\n        # Copy state values\r\n        export_data[\"state\"] = dict(state)\r\n        \r\n        # Write to file atomically (write to temp then rename)\r\n        state_file = get_state_file_path()\r\n        temp_file = state_file.with_suffix('.tmp')\r\n        \r\n        with open(temp_file, 'w') as f:\r\n            json.dump(export_data, f, indent=2)\r\n        \r\n        # Atomic rename\r\n        temp_file.replace(state_file)\r\n        \r\n        return True\r\n        \r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error exporting brain state: {e}\")\r\n        return False\r\n\r\n\r\ndef set_game_running(running: bool = True) -> None:\r\n    \"\"\"\r\n    Set/clear the game running flag.\r\n    \r\n    Call with running=True when game starts, running=False when game closes.\r\n    \"\"\"\r\n    lock_file = get_lock_file_path()\r\n    \r\n    if running:\r\n        # Create lock file with PID and timestamp\r\n        lock_data = {\r\n            \"pid\": os.getpid(),\r\n            \"timestamp\": time.time()\r\n        }\r\n        with open(lock_file, 'w') as f:\r\n            json.dump(lock_data, f)\r\n    else:\r\n        # Remove lock file\r\n        try:\r\n            lock_file.unlink(missing_ok=True)\r\n        except Exception:\r\n            pass\r\n        \r\n        # Also remove state file\r\n        try:\r\n            get_state_file_path().unlink(missing_ok=True)\r\n        except Exception:\r\n            pass\r\n\r\n\r\ndef update_brain_state_from_widget(brain_widget) -> bool:\r\n    \"\"\"\r\n    Convenience function to export brain state directly from a BrainWidget instance.\r\n    \r\n    Args:\r\n        brain_widget: BrainWidget instance from the game\r\n        \r\n    Returns:\r\n        True if export successful\r\n    \"\"\"\r\n    try:\r\n        return export_brain_state(\r\n            neuron_positions=brain_widget.neuron_positions,\r\n            weights=brain_widget.weights,\r\n            state=brain_widget.state,\r\n            visible_neurons=getattr(brain_widget, 'visible_neurons', None),\r\n            excluded_neurons=getattr(brain_widget, 'excluded_neurons', []),\r\n            layers=getattr(brain_widget, 'layers', []),\r\n            output_bindings=[]\r\n        )\r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error updating from widget: {e}\")\r\n        return False\r\n\r\n\r\n# ============================================================================\r\n# DESIGNER-SIDE FUNCTIONS (called by brain_designer / designer_window)\r\n# ============================================================================\r\n\r\ndef is_game_running() -> bool:\r\n    \"\"\"\r\n    Check if the main Dosidicus game is currently running.\r\n    We do this because the designer can be run standalone (python main.py -designer)\r\n    \r\n    Returns:\r\n        True if game appears to be running and has exported brain state\r\n    \"\"\"\r\n    lock_file = get_lock_file_path()\r\n    state_file = get_state_file_path()\r\n    \r\n    if not lock_file.exists():\r\n        return False\r\n    \r\n    if not state_file.exists():\r\n        return False\r\n    \r\n    try:\r\n        # Check if lock file is recent (within last 60 seconds)\r\n        with open(lock_file, 'r') as f:\r\n            lock_data = json.load(f)\r\n        \r\n        lock_time = lock_data.get('timestamp', 0)\r\n        if time.time() - lock_time > 60:\r\n            # Lock file is stale, game probably crashed\r\n            return False\r\n        \r\n        # Check if state file is recent\r\n        state_mtime = state_file.stat().st_mtime\r\n        if time.time() - state_mtime > 30:\r\n            # State file is stale\r\n            return False\r\n        \r\n        return True\r\n        \r\n    except Exception:\r\n        return False\r\n\r\n\r\ndef import_brain_state_for_designer() -> Optional[Dict]:\r\n    \"\"\"\r\n    Import brain state from the running game for use in the designer.\r\n    \r\n    Returns:\r\n        Dict with brain state data if game is running, None otherwise\r\n    \"\"\"\r\n    if not is_game_running():\r\n        return None\r\n    \r\n    try:\r\n        state_file = get_state_file_path()\r\n        \r\n        with open(state_file, 'r') as f:\r\n            data = json.load(f)\r\n        \r\n        # Verify format\r\n        if data.get('format') != 'live_brain_state':\r\n            return None\r\n        \r\n        return data\r\n        \r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error importing brain state: {e}\")\r\n        return None\r\n\r\n\r\ndef convert_to_brain_design(live_state: Dict) -> Optional['BrainDesign']:\r\n    \"\"\"\r\n    Convert imported live brain state to a BrainDesign object.\r\n    \r\n    This creates a BrainDesign that mirrors the exact state of the running game.\r\n    \r\n    Args:\r\n        live_state: Dict from import_brain_state_for_designer()\r\n        \r\n    Returns:\r\n        BrainDesign instance or None if conversion fails\r\n    \"\"\"\r\n    # Import here to avoid circular imports\r\n    try:\r\n        from designer_core import BrainDesign, DesignerNeuron, DesignerConnection\r\n        from designer_constants import (\r\n            NeuronType, is_core_neuron, is_input_sensor, is_binary_neuron,\r\n            DEFAULT_COLORS\r\n        )\r\n    except ImportError:\r\n        print(\"[BrainBridge] Could not import designer modules\")\r\n        return None\r\n    \r\n    try:\r\n        design = BrainDesign()\r\n        \r\n        # Set metadata\r\n        design.metadata = {\r\n            'name': 'Imported from Running Game',\r\n            'description': 'Brain state imported from active Dosidicus game',\r\n            'author': 'Live Import',\r\n            'version': '1.0',\r\n            'created': '',\r\n            'modified': ''\r\n        }\r\n        \r\n        neurons_data = live_state.get('neurons', {})\r\n        connections_data = live_state.get('connections', {})\r\n        state_data = live_state.get('state', {})\r\n        \r\n        # Create neurons\r\n        for name, neuron_info in neurons_data.items():\r\n            pos = neuron_info.get('position', [0, 0])\r\n            \r\n            # Determine neuron type\r\n            if is_core_neuron(name):\r\n                ntype = NeuronType.CORE\r\n                color = DEFAULT_COLORS.get('core', (150, 150, 220))\r\n            elif is_input_sensor(name) or name == 'can_see_food':\r\n                ntype = NeuronType.SENSOR\r\n                color = DEFAULT_COLORS.get('sensor', (150, 200, 220))\r\n            else:\r\n                ntype = NeuronType.HIDDEN\r\n                color = DEFAULT_COLORS.get('hidden', (180, 180, 200))\r\n            \r\n            neuron = DesignerNeuron(\r\n                name=name,\r\n                neuron_type=ntype,\r\n                position=tuple(pos) if isinstance(pos, list) else pos,\r\n                color=color,\r\n                is_binary=is_binary_neuron(name)\r\n            )\r\n            design.add_neuron(neuron)\r\n        \r\n        # Create connections - Handle both List (new) and Dict (legacy) formats\r\n        if isinstance(connections_data, list):\r\n            # NEW: Handle list of dicts [{'source': 'A', 'target': 'B', 'weight': 0.5}, ...]\r\n            for conn in connections_data:\r\n                source = conn.get('source')\r\n                target = conn.get('target')\r\n                weight = conn.get('weight', 0.5)\r\n                \r\n                if source and target and source in design.neurons and target in design.neurons:\r\n                    # Avoid duplicates\r\n                    existing = design.get_connection(source, target)\r\n                    if not existing:\r\n                        new_conn = DesignerConnection(source=source, target=target, weight=weight)\r\n                        design.connections.append(new_conn)\r\n\r\n        elif isinstance(connections_data, dict):\r\n            # LEGACY: Handle dict {'A->B': 0.5, ...}\r\n            for conn_key, weight in connections_data.items():\r\n                if '->' in conn_key:\r\n                    source, target = conn_key.split('->')\r\n                elif '|' in conn_key:\r\n                    source, target = conn_key.split('|')\r\n                else:\r\n                    continue\r\n                \r\n                source = source.strip()\r\n                target = target.strip()\r\n                \r\n                # Only add if both neurons exist\r\n                if source in design.neurons and target in design.neurons:\r\n                    conn = DesignerConnection(source=source, target=target, weight=weight)\r\n                    design.connections.append(conn)\r\n        \r\n        # Load layers if present\r\n        for layer_data in live_state.get('layers', []):\r\n            from designer_core import DesignerLayer\r\n            design.layers.append(DesignerLayer.from_dict(layer_data))\r\n        \r\n        # Load output bindings if present\r\n        design.output_bindings = live_state.get('output_bindings', [])\r\n        \r\n        return design\r\n        \r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error converting to BrainDesign: {e}\")\r\n        import traceback\r\n        traceback.print_exc()\r\n        return None\r\n\r\n\r\ndef get_import_file_path() -> Path:\r\n    \"\"\"Get the path for designs waiting to be imported by the game.\"\"\"\r\n    return get_bridge_directory() / \"pending_import.json\"\r\n\r\n\r\ndef export_design_to_game(design_data: Dict) -> bool:\r\n    \"\"\"\r\n    Export a design from the Designer to the running game.\r\n    \r\n    Args:\r\n        design_data: The dictionary returned by to_dosidicus_format()\r\n    \"\"\"\r\n    try:\r\n        import_file = get_import_file_path()\r\n        temp_file = import_file.with_suffix('.tmp')\r\n        \r\n        # Ensure it's marked\r\n        design_data['format'] = 'pending_import'\r\n        \r\n        with open(temp_file, 'w') as f:\r\n            json.dump(design_data, f, indent=2)\r\n            \r\n        # Atomic rename to ensure game reads complete file\r\n        temp_file.replace(import_file)\r\n        return True\r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error exporting to game: {e}\")\r\n        return False\r\n\r\n\r\ndef consume_pending_import() -> Optional[Dict]:\r\n    \"\"\"\r\n    Check for and retrieve a pending design import.\r\n    Removes the file after reading to ensure it's processed only once.\r\n    \"\"\"\r\n    import_file = get_import_file_path()\r\n    if not import_file.exists():\r\n        return None\r\n        \r\n    try:\r\n        # Ignore files older than 10 seconds (stale exports)\r\n        if time.time() - import_file.stat().st_mtime > 10:\r\n            return None \r\n            \r\n        with open(import_file, 'r') as f:\r\n            data = json.load(f)\r\n            \r\n        # Clean up immediately\r\n        try:\r\n            import_file.unlink()\r\n        except:\r\n            pass\r\n            \r\n        return data\r\n    except Exception as e:\r\n        print(f\"[BrainBridge] Error reading pending import: {e}\")\r\n        return None\r\n\r\n\r\n# ============================================================================\r\n# CLEANUP\r\n# ============================================================================\r\n\r\ndef cleanup_bridge_files() -> None:\r\n    \"\"\"Remove all bridge files. Called when game exits cleanly.\"\"\"\r\n    set_game_running(False)\r\n"
  },
  {
    "path": "src/brain_statistics_tab.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom .brain_base_tab import BrainBaseTab\r\nfrom .display_scaling import DisplayScaling\r\nfrom .localisation import Localisation\r\nimport time\r\n\r\nclass StatisticsTab(BrainBaseTab):\r\n    def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False):\r\n        super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode)\r\n        self.initialize_ui()\r\n\r\n        # Statistics tracking\r\n        self.statistics = {\r\n            'distance_swam': 0,\r\n            'cheese_eaten': 0,\r\n            'sushi_eaten': 0,\r\n            'poops_created': 0,\r\n            'max_poops_cleaned': 0,\r\n            'startles_experienced': 0,\r\n            'ink_clouds_created': 0,\r\n            'times_colour_changed': 0,\r\n            'rocks_thrown': 0,\r\n            'plants_interacted': 0,\r\n            'total_sleep_time': 0,\r\n            'sickness_episodes': 0,\r\n            'novelty_neurons_created': 0,\r\n            'stress_neurons_created': 0,\r\n            'reward_neurons_created': 0,\r\n            'current_neurons': 7,\r\n            'squid_age_minutes': 0,\r\n            'last_position': None,\r\n            'last_update_time': time.time()\r\n        }\r\n\r\n        # Load existing statistics if available\r\n        self.load_statistics()\r\n\r\n        # Setup update timer\r\n        self.update_timer = QtCore.QTimer(self)\r\n        self.update_timer.timeout.connect(self.update_statistics)\r\n        self.update_timer.start(1000)  # Update every second\r\n\r\n        self.is_visible = False\r\n        self.pending_distance = 0  # Store distance when tab not visible\r\n\r\n        # Connect to neuron creation events for real-time updates\r\n        if self.brain_widget and hasattr(self.brain_widget, 'neuronCreated'):\r\n            self.brain_widget.neuronCreated.connect(self._on_neuron_created_update_stats)\r\n\r\n    def showEvent(self, event):\r\n        \"\"\"Called when tab becomes visible\"\"\"\r\n        super().showEvent(event)\r\n        self.is_visible = True\r\n        # Apply any pending distance\r\n        if self.pending_distance > 0:\r\n            self.statistics['distance_swam'] += self.pending_distance\r\n            self.pending_distance = 0\r\n            self.update_display()\r\n\r\n    def hideEvent(self, event):\r\n        \"\"\"Called when tab becomes hidden\"\"\"\r\n        super().hideEvent(event)\r\n        self.is_visible = False\r\n\r\n    def set_logic(self, logic):\r\n        \"\"\"Called by main window after TamagotchiLogic (and squid) exist.\"\"\"\r\n        self.tamagotchi_logic = logic\r\n\r\n    def _sync_from_squid_statistics(self):\r\n        \"\"\"Mirror the persistent squid statistics object into the tab state.\"\"\"\r\n        if not self.tamagotchi_logic or not getattr(self.tamagotchi_logic, 'squid', None):\r\n            return\r\n\r\n        squid = self.tamagotchi_logic.squid\r\n        squid_stats = getattr(squid, 'statistics', None)\r\n        if not squid_stats:\r\n            return\r\n\r\n        self.statistics['distance_swam'] = getattr(squid_stats, 'distance_swam', self.statistics['distance_swam'])\r\n        self.statistics['cheese_eaten'] = getattr(squid_stats, 'cheese_consumed', self.statistics['cheese_eaten'])\r\n        self.statistics['sushi_eaten'] = getattr(squid_stats, 'sushi_consumed', self.statistics['sushi_eaten'])\r\n        self.statistics['poops_created'] = getattr(squid_stats, 'poops_created', self.statistics['poops_created'])\r\n        self.statistics['max_poops_cleaned'] = getattr(squid_stats, 'max_poops_cleaned', self.statistics['max_poops_cleaned'])\r\n        self.statistics['startles_experienced'] = getattr(squid_stats, 'startles_experienced', self.statistics['startles_experienced'])\r\n        self.statistics['ink_clouds_created'] = getattr(squid_stats, 'ink_clouds_created', self.statistics['ink_clouds_created'])\r\n        self.statistics['times_colour_changed'] = getattr(squid_stats, 'times_colour_changed', self.statistics['times_colour_changed'])\r\n        self.statistics['rocks_thrown'] = getattr(squid_stats, 'total_rocks_thrown', self.statistics['rocks_thrown'])\r\n        self.statistics['plants_interacted'] = getattr(squid_stats, 'plants_interacted', self.statistics['plants_interacted'])\r\n        self.statistics['total_sleep_time'] = getattr(squid_stats, 'time_spent_asleep', self.statistics['total_sleep_time'])\r\n        self.statistics['sickness_episodes'] = getattr(squid_stats, 'sickness_episodes', self.statistics['sickness_episodes'])\r\n        self.statistics['novelty_neurons_created'] = getattr(squid_stats, 'novelty_neurons_created', self.statistics['novelty_neurons_created'])\r\n        self.statistics['stress_neurons_created'] = getattr(squid_stats, 'stress_neurons_created', self.statistics['stress_neurons_created'])\r\n        self.statistics['reward_neurons_created'] = getattr(squid_stats, 'reward_neurons_created', self.statistics['reward_neurons_created'])\r\n        self.statistics['current_neurons'] = getattr(squid_stats, 'max_neurons_reached', self.statistics['current_neurons'])\r\n        self.statistics['squid_age_minutes'] = int(getattr(squid_stats, 'get_total_age_seconds', lambda: 0)() // 60)\r\n\r\n    def _increment_squid_stat(self, stat_name, amount=1):\r\n        \"\"\"Increment the canonical squid statistics attribute for a tab stat key.\"\"\"\r\n        if not self.tamagotchi_logic or not getattr(self.tamagotchi_logic, 'squid', None):\r\n            return False\r\n\r\n        squid_stats = getattr(self.tamagotchi_logic.squid, 'statistics', None)\r\n        if not squid_stats:\r\n            return False\r\n\r\n        attr_map = {\r\n            'cheese_eaten': 'cheese_consumed',\r\n            'sushi_eaten': 'sushi_consumed',\r\n            'poops_created': 'poops_created',\r\n            'poops_thrown': 'total_poops_thrown',\r\n            'max_poops_cleaned': 'max_poops_cleaned',\r\n            'startles_experienced': 'startles_experienced',\r\n            'ink_clouds_created': 'ink_clouds_created',\r\n            'times_colour_changed': 'times_colour_changed',\r\n            'rocks_thrown': 'total_rocks_thrown',\r\n            'plants_interacted': 'plants_interacted',\r\n            'novelty_neurons_created': 'novelty_neurons_created',\r\n            'stress_neurons_created': 'stress_neurons_created',\r\n            'reward_neurons_created': 'reward_neurons_created',\r\n        }\r\n\r\n        attr_name = attr_map.get(stat_name)\r\n        if not attr_name or not hasattr(squid_stats, attr_name):\r\n            return False\r\n\r\n        current_value = getattr(squid_stats, attr_name, 0)\r\n        setattr(squid_stats, attr_name, current_value + amount)\r\n        return True\r\n\r\n    def update_current_neurons(self, count):\r\n        \"\"\"Update the current neuron count in the UI, enforcing only-count-up logic.\r\n        \r\n        This ensures the max neurons stat only ever increases, never decreases.\r\n        \"\"\"\r\n        if hasattr(self, 'stat_labels') and 'current_neurons' in self.stat_labels:\r\n            current_max = self.statistics.get('current_neurons', 0)\r\n            if count > current_max:\r\n                self.statistics['current_neurons'] = count\r\n                self.stat_labels['current_neurons'].setText(str(count))\r\n\r\n                if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'squid'):\r\n                    squid = self.tamagotchi_logic.squid\r\n                    if hasattr(squid, 'statistics') and hasattr(squid.statistics, 'max_neurons_reached'):\r\n                        squid.statistics.max_neurons_reached = count\r\n                        squid.statistics.current_neurons = count\r\n\r\n    def _on_neuron_created_update_stats(self, neuron_name: str):\r\n        \"\"\"Update current neuron count when a new neuron is created\"\"\"\r\n        self.update_statistics()\r\n\r\n    def track_distance(self, distance):\r\n        \"\"\"Track distance swam - only updates if tab is visible\"\"\"\r\n        if self.is_visible:\r\n            self.statistics['distance_swam'] += distance\r\n            self.update_display()\r\n        else:\r\n            self.pending_distance += distance\r\n\r\n    def initialize_ui(self):\r\n        \"\"\"Build the statistics tab interface with DPI scaling\"\"\"\r\n        loc = Localisation.instance()\r\n        \r\n        self.layout.setContentsMargins(\r\n            DisplayScaling.scale(15),\r\n            DisplayScaling.scale(15),\r\n            DisplayScaling.scale(15),\r\n            DisplayScaling.scale(15),\r\n        )\r\n        self.layout.setSpacing(DisplayScaling.scale(10))\r\n\r\n        title_label = QtWidgets.QLabel(\"\")\r\n        title_font = QtGui.QFont()\r\n        title_font.setPointSize(DisplayScaling.font_size(12))\r\n        title_font.setBold(True)\r\n        title_label.setFont(title_font)\r\n        title_label.setStyleSheet(\"color: #2c3e50;\")\r\n        self.layout.addWidget(title_label)\r\n\r\n        stats_container = QtWidgets.QWidget()\r\n        stats_container.setObjectName(\"statsContainer\")\r\n        stats_container.setStyleSheet(\r\n            \"\"\"\r\n            #statsContainer {\r\n                background-color: #f8f9fa;\r\n                border-radius: 10px;\r\n                padding: 10px;\r\n            }\r\n            \"\"\"\r\n        )\r\n        stats_layout = QtWidgets.QFormLayout(stats_container)\r\n        stats_layout.setSpacing(DisplayScaling.scale(10))\r\n\r\n        stat_items = [\r\n            ('squid_age_minutes', loc.get('stat_squid_age')),\r\n            ('distance_swam', loc.get('stat_distance')),\r\n            ('cheese_eaten', loc.get('stat_cheese')),\r\n            ('sushi_eaten', loc.get('stat_sushi')),\r\n            ('poops_created', loc.get('stat_poops')),\r\n            ('max_poops_cleaned', loc.get('stat_max_poops')),\r\n            ('startles_experienced', loc.get('stat_startles')),\r\n            ('ink_clouds_created', loc.get('stat_ink')),\r\n            ('times_colour_changed', loc.get('stat_colour_change')),\r\n            ('rocks_thrown', loc.get('stat_rocks')),\r\n            ('plants_interacted', loc.get('stat_plants')),\r\n            ('total_sleep_time', loc.get('stat_sleep')),\r\n            ('sickness_episodes', loc.get('stat_sickness')),\r\n            ('novelty_neurons_created', loc.get('stat_novelty_neurons')),\r\n            ('stress_neurons_created', loc.get('stat_stress_neurons')),\r\n            ('reward_neurons_created', loc.get('stat_reward_neurons')),\r\n            ('current_neurons', \"Max neurons\"),\r\n        ]\r\n\r\n        if not hasattr(self, 'stat_labels'):\r\n            self.stat_labels = {}\r\n\r\n        for key, label in stat_items:\r\n            if key not in self.stat_labels:\r\n                lbl = QtWidgets.QLabel(f\"{label}:\")\r\n                font = QtGui.QFont()\r\n                font.setPointSize(DisplayScaling.font_size(10))\r\n                lbl.setFont(font)\r\n\r\n                val = QtWidgets.QLabel(\"0\")\r\n                val_font = QtGui.QFont()\r\n                val_font.setPointSize(DisplayScaling.font_size(12))\r\n                val_font.setBold(True)\r\n                val.setFont(val_font)\r\n                val.setStyleSheet(\"color: #495057;\")\r\n\r\n                self.stat_labels[key] = val\r\n                stats_layout.addRow(lbl, val)\r\n\r\n        self.layout.addWidget(stats_container)\r\n        self.layout.addStretch()\r\n\r\n    def update_from_brain_state(self, state):\r\n        \"\"\"Update tab based on brain state\"\"\"\r\n        if not self.tamagotchi_logic or not self.tamagotchi_logic.squid:\r\n            return\r\n\r\n        squid = self.tamagotchi_logic.squid\r\n\r\n        current_pos = (squid.squid_x, squid.squid_y)\r\n        if self.statistics['last_position'] is not None:\r\n            last_pos = self.statistics['last_position']\r\n            distance = ((current_pos[0] - last_pos[0])**2 + (current_pos[1] - last_pos[1])**2)**0.5\r\n            self.statistics['distance_swam'] += distance\r\n\r\n        self.statistics['last_position'] = current_pos\r\n\r\n        if squid.is_sleeping:\r\n            current_time = time.time()\r\n            time_elapsed = current_time - self.statistics['last_update_time']\r\n            self.statistics['total_sleep_time'] += time_elapsed\r\n\r\n        self.statistics['last_update_time'] = time.time()\r\n        self._sync_from_squid_statistics()\r\n        self.update_display()\r\n\r\n    def update_statistics(self):\r\n        \"\"\"Update statistics from squid state\"\"\"\r\n        if not self.tamagotchi_logic or not self.tamagotchi_logic.squid:\r\n            return\r\n\r\n        squid = self.tamagotchi_logic.squid\r\n\r\n        if not self.brain_widget:\r\n            parent = self.parent()\r\n            if hasattr(parent, 'brain_widget'):\r\n                self.brain_widget = parent.brain_widget\r\n\r\n        if self.brain_widget and hasattr(self.brain_widget, 'neuron_positions'):\r\n            real_current_count = len(self.brain_widget.neuron_positions)\r\n            stored_count = self.statistics.get('current_neurons', 0)\r\n\r\n            if real_current_count > stored_count:\r\n                self.statistics['current_neurons'] = real_current_count\r\n\r\n                if hasattr(squid, 'statistics') and hasattr(squid.statistics, 'max_neurons_reached'):\r\n                    if real_current_count > squid.statistics.max_neurons_reached:\r\n                        squid.statistics.max_neurons_reached = real_current_count\r\n                        squid.statistics.current_neurons = real_current_count\r\n\r\n        self._sync_from_squid_statistics()\r\n        self.statistics['last_update_time'] = time.time()\r\n        self.update_display()\r\n\r\n    def update_display(self):\r\n        \"\"\"Update the statistics display\"\"\"\r\n        for key, label in self.stat_labels.items():\r\n            if key == 'distance_swam':\r\n                if (self.tamagotchi_logic and \r\n                    self.tamagotchi_logic.squid and \r\n                    hasattr(self.tamagotchi_logic.squid, 'statistics')):\r\n                    distance_str = self.tamagotchi_logic.squid.statistics.get_distance_display()\r\n                    label.setText(distance_str)\r\n                else:\r\n                    value = self.statistics.get(key, 0)\r\n                    label.setText(f\"{int(value):,}\")\r\n            elif key == 'total_sleep_time':\r\n                value = self.statistics.get(key, 0)\r\n                label.setText(f\"{int(value)}\")\r\n            else:\r\n                value = self.statistics.get(key, 0)\r\n                label.setText(str(int(value)))\r\n\r\n    def increment_stat(self, stat_name, amount=1):\r\n        \"\"\"Increment a specific statistic\"\"\"\r\n        updated_canonical = self._increment_squid_stat(stat_name, amount)\r\n\r\n        if stat_name in self.statistics:\r\n            self.statistics[stat_name] += amount\r\n        elif not updated_canonical:\r\n            return\r\n\r\n        self._sync_from_squid_statistics()\r\n        self.update_display()\r\n        self.save_statistics()\r\n\r\n    def reset_statistics(self):\r\n        \"\"\"Reset all statistics to zero\"\"\"\r\n        loc = Localisation.instance()\r\n        reply = QtWidgets.QMessageBox.question(\r\n            self, loc.get(\"reset_stats_title\"), \r\n            loc.get(\"reset_stats_msg\"),\r\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No\r\n        )\r\n\r\n        if reply == QtWidgets.QMessageBox.Yes:\r\n            for key in self.statistics:\r\n                if key not in ['last_position', 'last_update_time']:\r\n                    self.statistics[key] = 0\r\n            self.statistics['last_update_time'] = time.time()\r\n            self.update_display()\r\n            self.save_statistics()\r\n\r\n    def export_statistics(self):\r\n        \"\"\"Export statistics to a file\"\"\"\r\n        loc = Localisation.instance()\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(\r\n            self, loc.get(\"export_stats_title\"), \"\", loc.get(\"export_file_type\")\r\n        )\r\n\r\n        if file_name:\r\n            try:\r\n                with open(file_name, 'w') as f:\r\n                    f.write(f\"{loc.get('export_header')}\\n\")\r\n                    f.write(\"=\" * 30 + \"\\n\")\r\n                    from datetime import datetime\r\n                    export_time = datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')\r\n                    f.write(f\"{loc.get('export_time')}: {export_time}\\n\\n\")\r\n\r\n                    f.write(f\"{loc.get('export_activity_section')}:\\n\")\r\n                    f.write(f\"{loc.get('stat_distance')}: {int(self.statistics['distance_swam'])}\\n\")\r\n                    f.write(f\"{loc.get('stat_cheese')}: {self.statistics['cheese_eaten']}\\n\")\r\n                    f.write(f\"{loc.get('stat_sushi')}: {self.statistics['sushi_eaten']}\\n\")\r\n                    f.write(f\"{loc.get('stat_poops')}: {self.statistics['poops_created']}\\n\")\r\n                    f.write(f\"{loc.get('stat_max_poops')}: {self.statistics['max_poops_cleaned']}\\n\")\r\n                    f.write(f\"{loc.get('stat_rocks')}: {self.statistics['rocks_thrown']}\\n\")\r\n                    f.write(f\"{loc.get('stat_plants')}: {self.statistics['plants_interacted']}\\n\")\r\n                    f.write(f\"{loc.get('stat_startles')}: {self.statistics['startles_experienced']}\\n\")\r\n                    f.write(f\"{loc.get('stat_sleep')}: {int(self.statistics['total_sleep_time'])}\\n\")\r\n                    f.write(f\"{loc.get('stat_sickness')}: {self.statistics['sickness_episodes']}\\n\")\r\n                    f.write(f\"{loc.get('stat_squid_age')}: {int(self.statistics['squid_age_minutes'])}\\n\")\r\n                    f.write(f\"Max Neurons: {self.statistics.get('current_neurons', 7)}\\n\")\r\n                    f.write(\"\\n\" + \"=\" * 30 + \"\\n\")\r\n                    f.write(f\"{loc.get('export_end')}\\n\")\r\n\r\n                QtWidgets.QMessageBox.information(\r\n                    self, loc.get(\"export_success_title\"), \r\n                    loc.get(\"export_success_msg\", file_name=file_name)\r\n                )\r\n            except Exception as e:\r\n                QtWidgets.QMessageBox.critical(\r\n                    self, loc.get(\"export_error_title\"), \r\n                    loc.get(\"export_error_msg\", error=str(e))\r\n                )\r\n\r\n    def save_statistics(self):\r\n        \"\"\"Save statistics to file\"\"\"\r\n        if hasattr(self.tamagotchi_logic, 'save_manager'):\r\n            try:\r\n                self.tamagotchi_logic.save_manager.save_statistics(self.statistics)\r\n            except:\r\n                pass\r\n\r\n    def load_statistics(self):\r\n        \"\"\"Load statistics from file\"\"\"\r\n        if hasattr(self.tamagotchi_logic, 'save_manager'):\r\n            try:\r\n                loaded_stats = self.tamagotchi_logic.save_manager.load_statistics()\r\n                if loaded_stats:\r\n                    self.statistics.update(loaded_stats)\r\n            except:\r\n                pass\r\n"
  },
  {
    "path": "src/brain_tool.py",
    "content": "import sys\r\nimport csv\r\nimport os\r\nimport time\r\nimport json\r\nimport random\r\nimport numpy as np\r\nfrom datetime import datetime\r\nfrom PyQt5.QtWidgets import QSplitter\r\nfrom PyQt5.QtGui import QPixmap, QFont\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nfrom .config_manager import ConfigManager\r\nfrom .brain_widget import BrainWidget\r\nfrom .brain_worker import BrainWorker \r\nfrom .brain_dialogs import StimulateDialog, RecentThoughtsDialog, LogWindow, DiagnosticReportDialog\r\nfrom .brain_utils import ConsoleOutput\r\nfrom .personality import Personality\r\nfrom .learning import LearningConfig\r\nfrom .brain_network_tab import NetworkTab\r\nfrom .brain_about_tab import AboutTab\r\nfrom .brain_learning_tab import NeuralNetworkVisualizerTab\r\nfrom .brain_memory_tab import MemoryTab\r\nfrom .brain_decisions_tab import DecisionsTab\r\nfrom .brain_personality_tab import PersonalityTab\r\nfrom .brain_statistics_tab import StatisticsTab\r\nfrom .task_manager import TaskManagerWindow\r\nfrom .localisation import Localisation, set_language\r\n\r\n# 2.6.1.0 Robust import for Designer\r\n_DESIGNER_AVAILABLE = False\r\ntry:\r\n    from .designer_window import BrainDesignerWindow\r\n    _DESIGNER_AVAILABLE = True\r\nexcept Exception as e:\r\n    print(f\"Warning: BrainDesignerWindow could not be imported: {type(e).__name__}: {e}\")\r\n    try:\r\n        from src.designer_window import BrainDesignerWindow\r\n        _DESIGNER_AVAILABLE = True\r\n    except Exception as e2:\r\n        print(f\"Warning: src fallback also failed: {type(e2).__name__}: {e2}\")\r\n\r\n# 2.6.1.0 Define _HAS_BRAIN_BRIDGE variable\r\n_HAS_BRAIN_BRIDGE = False\r\ntry:\r\n    from .brain_state_bridge import (\r\n        is_game_running,\r\n        import_brain_state_for_designer\r\n    )\r\n    _HAS_BRAIN_BRIDGE = True\r\nexcept ImportError:\r\n    _HAS_BRAIN_BRIDGE = False\r\n    print(\"Warning: brain_state_bridge not found in brain_tool\")\r\n\r\nclass SquidBrainWindow(QtWidgets.QMainWindow):\r\n    def __init__(self, tamagotchi_logic, debug_mode=False, config=None, show_decorations_callback=None):\r\n        # Force language sync before any tabs are created\r\n        if config and hasattr(config, 'get_language'):\r\n            set_language(config.get_language())\r\n        \r\n        super().__init__()\r\n\r\n        # Initialize font size FIRST\r\n        from .display_scaling import DisplayScaling\r\n\r\n        self.base_font_size = DisplayScaling.font_size(10)\r\n        self.debug_mode = debug_mode\r\n        self.config_manager = ConfigManager()      # real manager\r\n        self.config        = self.config_manager   # keep old name for compat\r\n        self.tamagotchi_logic = tamagotchi_logic\r\n        self.initialized = False\r\n        self.is_paused = False\r\n        self.show_decorations_callback = show_decorations_callback\r\n\r\n        self.setWindowTitle(\"Brain Tool\")\r\n\r\n        # Get screen resolution and available geometry (excluding taskbars/docks)\r\n        screen = QtWidgets.QApplication.primaryScreen()\r\n        screen_geometry = screen.availableGeometry() # Use availableGeometry for usable screen space\r\n\r\n        # Define initial window dimensions directly, without DisplayScaling\r\n        # This will use absolute pixel values for the window size\r\n        final_width = 1400 # Direct pixel width\r\n        final_height = 900 # Direct pixel height\r\n\r\n        # Ensure the final window dimensions do not exceed the actual screen available geometry\r\n        final_width = min(final_width, screen_geometry.width())\r\n        final_height = min(final_height, screen_geometry.height())\r\n\r\n        self.resize(final_width, final_height) # Resize the window with clamped dimensions\r\n\r\n        # Position window properly in the top-right corner of the *available* screen geometry\r\n        # x-coordinate: right edge of available geometry minus window width, then shift left by 200 pixels\r\n        x_pos = screen_geometry.right() - final_width - 10 # Shift left by subtracting 1and using final_width\r\n        # y-coordinate: top edge of available geometry\r\n        y_pos = screen_geometry.top() # Use top of available geometry\r\n\r\n        self.move(x_pos, y_pos)\r\n\r\n        # Setup the central widget and main layout\r\n        self.central_widget = QtWidgets.QWidget()\r\n        self.setCentralWidget(self.central_widget)\r\n\r\n        self.layout = QtWidgets.QVBoxLayout()\r\n        self.central_widget.setLayout(self.layout)\r\n\r\n        # Create the brain widget first\r\n        self.brain_widget = BrainWidget(self.config_manager, self.debug_mode, tamagotchi_logic=tamagotchi_logic)\r\n        # Set parent so brain_widget can access network_tab via self.parent().network_tab\r\n        self.brain_widget.setParent(self)\r\n\r\n        # Initialize tab widget\r\n        self.init_tabs()\r\n\r\n        # Set up timers\r\n        self.init_timers()\r\n\r\n        # Initialize memory update timer if needed\r\n        if hasattr(self, 'memory_tab'):\r\n            self.memory_update_timer = QtCore.QTimer(self)\r\n            self.memory_update_timer.timeout.connect(self.update_memory_tab)\r\n            self.memory_update_timer.start(5000)  # PERFORMANCE FIX: Update every 5 secs (was 2)\r\n\r\n        \r\n        # Set up worker thread\r\n        self.brain_worker = BrainWorker(brain_widget=self.brain_widget)\r\n        self.brain_worker.start()\r\n        self._last_worker_activity = time.time()\r\n        self._worker_healthy = True\r\n\r\n        # Start periodic worker health monitoring\r\n        self.health_timer = QtCore.QTimer(self)\r\n        self.health_timer.timeout.connect(self.check_worker_health)\r\n        self.health_timer.start(10000)  # Every 10 seconds\r\n        \r\n        # Connect worker signals for health monitoring\r\n        self.brain_worker.neurogenesis_result.connect(self._on_worker_activity)\r\n        self.brain_worker.hebbian_result.connect(self._on_worker_activity)\r\n        self.brain_worker.state_update_result.connect(self._on_worker_activity)\r\n        self.brain_worker.error_occurred.connect(self._on_worker_error)\r\n\r\n        # Track pause state before entering designer\r\n        self._was_paused_before_designer = False\r\n        self.designer_view = None\r\n        \r\n        # ===== PERFORMANCE FIX: Share worker with brain_widget =====\r\n        if hasattr(self.brain_widget, 'set_brain_worker'):\r\n            self.brain_widget.set_brain_worker(self.brain_worker)\r\n            print(\"🔗 Shared BrainWorker with brain_widget\")\r\n            \r\n            # Immediately update cache with current state\r\n            self.brain_widget._update_worker_cache()\r\n            print(\"📦 Initial worker cache populated\")\r\n        \r\n        # Store reference to prevent garbage collection\r\n        self._brain_worker_ref = self.brain_worker\r\n        \r\n        # Ensure worker stays alive with initial work\r\n        QtCore.QTimer.singleShot(1000, lambda: self._keep_worker_alive())\r\n        \r\n        # Initialize task manager AFTER worker is fully set up\r\n        self.task_manager = TaskManagerWindow(self.brain_worker, parent=self)\r\n        \r\n        # Setup decorations window shortcut\r\n        self.setup_decorations_shortcut()\r\n\r\n        # Bridge Import Listener\r\n        # Polls for designs pushed from the Designer window\r\n        if _HAS_BRAIN_BRIDGE:\r\n            self._bridge_timer = QtCore.QTimer(self)\r\n            self._bridge_timer.timeout.connect(self._check_bridge_import)\r\n            self._bridge_timer.start(1000)  # Check every 1 second\r\n\r\n\r\n    def set_tamagotchi_logic(self, tamagotchi_logic):\r\n        \"\"\"Set the tamagotchi_logic reference and update all tabs\"\"\"\r\n        print(f\"SquidBrainWindow.set_tamagotchi_logic: {tamagotchi_logic is not None}\")\r\n        self.tamagotchi_logic = tamagotchi_logic\r\n        \r\n        # Update brain widget\r\n        if hasattr(self, 'brain_widget'):\r\n            self.brain_widget.tamagotchi_logic = tamagotchi_logic\r\n            \r\n            # 🔽 ADD THIS BLOCK: Load output bindings if they exist 🔽\r\n            if (hasattr(self.brain_widget, 'output_bindings') and \r\n                self.brain_widget.output_bindings and\r\n                hasattr(tamagotchi_logic, 'neuron_output_monitor') and\r\n                tamagotchi_logic.neuron_output_monitor):\r\n                \r\n                tamagotchi_logic.neuron_output_monitor.load_bindings_from_brain({\r\n                    'output_bindings': self.brain_widget.output_bindings\r\n                })\r\n                print(f\"  ✓ Loaded {len(self.brain_widget.output_bindings)} output bindings into Monitor\")\r\n        \r\n        # Update all tabs\r\n        for tab_attr in ['memory_tab', 'network_tab', 'learning_tab', 'decisions_tab', 'about_tab']:\r\n            if hasattr(self, tab_attr):\r\n                tab = getattr(self, tab_attr)\r\n                if hasattr(tab, 'set_tamagotchi_logic') and self.tamagotchi_logic:\r\n                    tab.set_tamagotchi_logic(tamagotchi_logic)\r\n\r\n    def _check_bridge_import(self):\r\n        \"\"\"Check if an external designer has pushed a new brain state.\"\"\"\r\n        # Don't import if we are currently IN the embedded designer mode\r\n        if self.designer_view:\r\n            return\r\n\r\n        try:\r\n            from .brain_state_bridge import consume_pending_import\r\n            data = consume_pending_import()\r\n            \r\n            if data:\r\n                print(\">> 📥 Received external brain import from Designer\")\r\n                \r\n                # Use the existing apply logic which handles neurons, connections, and bindings\r\n                self.apply_designer_state(data)\r\n                \r\n                # Visual feedback\r\n                if hasattr(self, 'status_bar'):\r\n                    self.status_bar.showMessage(\"✨ Brain updated from external Designer\", 5000)\r\n                else:\r\n                    print(\"✨ Brain updated successfully\")\r\n                    \r\n        except ImportError:\r\n            pass\r\n        except Exception as e:\r\n            print(f\"Error applying external import: {e}\")\r\n\r\n    def setup_decorations_shortcut(self):\r\n        \"\"\"Setup keyboard shortcut for decorations window (D key)\"\"\"\r\n        if self.show_decorations_callback:\r\n            self.decorations_shortcut = QtWidgets.QShortcut(\r\n                QtGui.QKeySequence(QtCore.Qt.Key_D), \r\n                self\r\n            )\r\n            self.decorations_shortcut.activated.connect(self.show_decorations_callback)\r\n\r\n    def show_decorations_window(self):\r\n        # Always show and activate the decorations window when D is pressed\r\n        self.decoration_window.show()\r\n        self.decoration_window.activateWindow()\r\n\r\n    def _keep_worker_alive(self):\r\n        \"\"\"Keep worker thread alive with periodic health checks\"\"\"\r\n        if self.brain_worker and self.brain_worker.isRunning():\r\n            # Queue a health check every 30 seconds\r\n            self.brain_worker.queue_state_update({'health_check': True})\r\n            QtCore.QTimer.singleShot(30000, self._keep_worker_alive)\r\n\r\n    def set_debug_mode(self, enabled):\r\n        \"\"\"Properly set debug mode for brain window and all tabs\"\"\"\r\n        self.debug_mode = enabled\r\n        \r\n        # Update brain widget's debug mode\r\n        if hasattr(self, 'brain_widget'):\r\n            self.brain_widget.debug_mode = enabled\r\n        \r\n        # Update all tabs\r\n        for tab_name in ['network_tab', 'nn_viz_tab', 'memory_tab', 'decisions_tab', 'about_tab']:\r\n            if hasattr(self, tab_name):\r\n                tab = getattr(self, tab_name)\r\n                if hasattr(tab, 'debug_mode'):\r\n                    tab.debug_mode = enabled\r\n        \r\n        print(f\"Brain window debug mode set to: {enabled}\")\r\n\r\n    def check_worker_health(self):\r\n        \"\"\"\r\n        Periodically check if the BrainWorker thread is alive and responsive.\r\n        If it's dead or completely stalled, restart it safely.\r\n        \"\"\"\r\n        if not hasattr(self, 'brain_worker') or not self.brain_worker:\r\n            print(\"BrainWorker missing – creating new one\")\r\n            self._restart_brain_worker()\r\n            return\r\n\r\n        # Case 1: Thread object exists but is not running\r\n        if not self.brain_worker.isRunning():\r\n            print(\"BrainWorker thread not running – restarting\")\r\n            self._restart_brain_worker()\r\n            return\r\n\r\n        # Case 2: Thread is running but has been completely silent for too long\r\n        if not self.is_worker_healthy():\r\n            print(\"BrainWorker stalled (no activity in 15s) – forcing restart\")\r\n            self._restart_brain_worker()\r\n            return\r\n\r\n        # Everything looks fine\r\n        # print(\"BrainWorker healthy\")  # Uncomment only for deep debugging\r\n\r\n    # SquidBrainWindow._restart_brain_worker\r\n    def _restart_brain_worker(self):\r\n        # --- stop old worker\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            print(\"Stopping old BrainWorker...\")\r\n            # Disconnect signals BEFORE stopping to prevent duplicate connections\r\n            try:\r\n                self.brain_worker.neurogenesis_result.disconnect(self._on_worker_activity)\r\n                self.brain_worker.hebbian_result.disconnect(self._on_worker_activity)\r\n                self.brain_worker.state_update_result.disconnect(self._on_worker_activity)\r\n                self.brain_worker.error_occurred.disconnect(self._on_worker_error)\r\n            except:\r\n                pass  # Ignore if signals weren't connected\r\n            \r\n            self.brain_worker.stop()\r\n            self.brain_worker.wait(3000)\r\n\r\n        # --- create & start new worker\r\n        self.brain_worker = BrainWorker(brain_widget=self.brain_widget)\r\n        \r\n        # Reconnect signals for new worker\r\n        self.brain_worker.neurogenesis_result.connect(self._on_worker_activity)\r\n        self.brain_worker.hebbian_result.connect(self._on_worker_activity)\r\n        self.brain_worker.state_update_result.connect(self._on_worker_activity)\r\n        self.brain_worker.error_occurred.connect(self._on_worker_error)\r\n        \r\n        self.brain_worker.start()\r\n\r\n        # Update cache for new worker\r\n        if hasattr(self.brain_widget, 'set_brain_worker'):\r\n            self.brain_widget.set_brain_worker(self.brain_worker)\r\n            self.brain_widget._update_worker_cache()\r\n        \r\n        # --- CRITICAL: Update TaskManager's worker reference ---\r\n        if hasattr(self, 'task_manager') and self.task_manager:\r\n            self.task_manager.update_worker_reference(self.brain_worker)\r\n        \r\n        # --- Keep-alive timer ---\r\n        QtCore.QTimer.singleShot(1000, lambda: self._keep_worker_alive())\r\n        print(\"BrainWorker successfully restarted and re-cached\")\r\n\r\n    def _on_worker_activity(self, result):\r\n        \"\"\"Update worker health when activity is detected\"\"\"\r\n        self._last_worker_activity = time.time()\r\n        self._worker_healthy = True\r\n        # print(f\"🧠 Worker activity detected: {time.time() - self._last_worker_activity:.1f}s ago\")\r\n\r\n    def _on_worker_error(self, error_msg):\r\n        \"\"\"Handle worker errors\"\"\"\r\n        print(f\"⚠️ Worker error: {error_msg}\")\r\n        self._worker_healthy = False\r\n\r\n    def is_worker_healthy(self):\r\n        \"\"\"Check if worker is healthy based on recent activity\"\"\"\r\n        if not self.brain_worker or not self.brain_worker.isRunning():\r\n            return False\r\n        \r\n        # Consider worker healthy if it had activity in last 10 seconds\r\n        time_since_activity = time.time() - self._last_worker_activity\r\n        return time_since_activity < 10.0 and self._worker_healthy\r\n\r\n    def on_neurogenesis_result(self, result):\r\n        \"\"\"Handle neurogenesis results from worker thread\"\"\"\r\n        if result.get('should_create'):\r\n            # This now runs on main thread - safe to modify brain_widget\r\n            self.brain_widget.handle_neurogenesis_result(result)\r\n\r\n    def on_hebbian_result(self, result):\r\n        \"\"\"Handle Hebbian learning results from worker thread\"\"\"\r\n        if result.get('updated_pairs'):\r\n            self.brain_widget.handle_hebbian_result(result)\r\n\r\n    def on_state_update_result(self, result):\r\n        \"\"\"Handle processed state updates\"\"\"\r\n        if result.get('processed_state'):\r\n            self.brain_widget.update_state(result['processed_state'])\r\n\r\n    def on_worker_error(self, error_msg):\r\n        \"\"\"Handle errors from worker thread\"\"\"\r\n        print(f\"⚠️ BrainWorker error: {error_msg}\")\r\n        # Optional: Show in UI\r\n        if hasattr(self, 'status_bar'):\r\n            self.status_bar.showMessage(f\"Worker error: {error_msg}\", 5000)\r\n\r\n    def closeEvent(self, event):\r\n        \"\"\"Stop worker and timers when window closes\"\"\"\r\n        if hasattr(self, 'health_timer') and self.health_timer.isActive():\r\n            self.health_timer.stop()\r\n\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            self.brain_worker.stop()\r\n            if self.brain_worker.isRunning():\r\n                self.brain_worker.wait(5000)\r\n        event.accept()\r\n\r\n    def on_hebbian_countdown_finished(self):\r\n        \"\"\"Called when the Hebbian learning countdown reaches zero\"\"\"\r\n        pass\r\n\r\n    def set_pause_state(self, is_paused):\r\n        \"\"\"Set pause state for the brain window and worker thread\"\"\"\r\n        self.is_paused = is_paused\r\n        \r\n        # Set brain widget pause state\r\n        if hasattr(self, 'brain_widget'):\r\n            self.brain_widget.is_paused = is_paused\r\n        \r\n        # Control worker thread - CRITICAL for freezing everything\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            if is_paused:\r\n                self.brain_worker.pause()\r\n                print(\"⏸️ BrainWorker paused\")\r\n            else:\r\n                self.brain_worker.resume()\r\n                print(\"▶️ BrainWorker resumed\")\r\n\r\n        # Manage timers based on pause state\r\n        if is_paused:\r\n            if hasattr(self, 'hebbian_timer'):\r\n                self.hebbian_timer.stop()\r\n        else:\r\n            if hasattr(self, 'hebbian_timer'):\r\n                self.hebbian_timer.start(self.config.hebbian['learning_interval'])\r\n\r\n    def switch_to_designer_mode(self):\r\n        \"\"\"\r\n        Replaces the main tab view with the Brain Designer embedded window.\r\n        Includes extensive fixes for blank canvas and missing connection issues.\r\n        \"\"\"\r\n        if not _DESIGNER_AVAILABLE:\r\n            QtWidgets.QMessageBox.warning(self, \"Error\", \"Brain Designer module not found.\")\r\n            return\r\n\r\n        if not hasattr(self, 'brain_widget'): \r\n            return\r\n\r\n        print(\">> Switching to Designer Mode...\")\r\n\r\n        # 1. Pause simulation\r\n        self._was_paused_before_designer = self.is_paused\r\n        \r\n        # Pause game logic via UI to ensure \"SIMULATION PAUSED\" overlay appears\r\n        if self.tamagotchi_logic:\r\n            # Check if we can route through UI for visual feedback\r\n            if hasattr(self.tamagotchi_logic, 'user_interface') and \\\r\n               hasattr(self.tamagotchi_logic.user_interface, 'set_simulation_speed'):\r\n                self.tamagotchi_logic.user_interface.set_simulation_speed(0)\r\n            else:\r\n                # Fallback to logic-only pause (no overlay)\r\n                self.tamagotchi_logic.set_simulation_speed(0)\r\n        else:\r\n            self.set_pause_state(True)\r\n        \r\n        # 2. Capture state\r\n        current_state = self.get_brain_state()\r\n        \r\n        # The designer needs a rich 'neurons' dictionary with types and positions\r\n        if 'neurons' not in current_state:\r\n            current_state['neurons'] = {}\r\n            \r\n            # Helper sets for identifying neuron types\r\n            core_neurons = {'hunger', 'happiness', 'cleanliness', 'sleepiness', \r\n                           'satisfaction', 'anxiety', 'curiosity'}\r\n            \r\n            for name, pos in self.brain_widget.neuron_positions.items():\r\n                # Determine type\r\n                if name in core_neurons:\r\n                    ntype = 'core'\r\n                elif name == 'can_see_food' or name in getattr(self.brain_widget, 'input_sensors', []):\r\n                    ntype = 'sensor'\r\n                elif name.startswith('connector') or self.brain_widget.is_connector_neuron(name):\r\n                    ntype = 'connector'\r\n                else:\r\n                    ntype = 'hidden' \r\n                \r\n                # Get existing color\r\n                color = self.brain_widget.state_colors.get(name, (200, 200, 200))\r\n                \r\n                # Build the rich object\r\n                current_state['neurons'][name] = {\r\n                    'name': name,\r\n                    'position': pos,\r\n                    'neuron_type': ntype, # Critical for Designer validation\r\n                    'type': ntype,\r\n                    'is_binary': self.brain_widget.is_binary_neuron(name),\r\n                    'color': color,\r\n                    'activation': self.brain_widget.state.get(name, 0.0)\r\n                }\r\n            print(f\"   [Fix] Constructed {len(current_state['neurons'])} neuron records\")\r\n\r\n        # The designer prefers a 'connections' list of dicts.\r\n        # We build this explicitly from the brain_widget weights to ensure links appear.\r\n        \r\n        connections_list = []\r\n        \r\n        # Source: Direct from brain_widget weights\r\n        # We bypass 'weights_list' to ensure we get the most up-to-date dict structure\r\n        if hasattr(self.brain_widget, 'weights'):\r\n            for (src, dst), weight in self.brain_widget.weights.items():\r\n                connections_list.append({\r\n                    'source': str(src),\r\n                    'target': str(dst),\r\n                    'weight': float(weight)\r\n                })\r\n\r\n        # Inject into state\r\n        current_state['connections'] = connections_list\r\n        print(f\"   [Fix] Constructed {len(connections_list)} connection records\")\r\n\r\n        # 3. Hide game tabs\r\n        self.tabs.hide()\r\n        \r\n        # 4. Initialize Designer\r\n        self.designer_view = BrainDesignerWindow(self, embedded_mode=True)\r\n        self.designer_view.setWindowFlags(QtCore.Qt.Widget)\r\n        \r\n        # 5. Load State\r\n        loaded = False\r\n        if hasattr(self.designer_view, 'load_from_brain_widget_state'):\r\n            try:\r\n                # This calls converter which now sees our nice 'neurons' and 'connections' keys\r\n                loaded = self.designer_view.load_from_brain_widget_state(current_state)\r\n            except Exception as e:\r\n                print(f\"Standard load failed: {e}\")\r\n        \r\n        if not loaded:\r\n            # Fallback manual loading\r\n            try:\r\n                from designer_core import BrainDesign\r\n                design = BrainDesign.from_dosidicus_format(current_state)\r\n                self.designer_view.design = design\r\n                self.designer_view.refresh_all()\r\n                loaded = True\r\n            except Exception as e:\r\n                print(f\"Critical: Failed to load brain state into designer: {e}\")\r\n\r\n        # 6. Setup Return Signal & Layout\r\n        self.designer_view.exitRequested.connect(self.switch_to_game_mode)\r\n        self.layout.addWidget(self.designer_view)\r\n        \r\n        # [FIX] Force layout update and delayed refresh to ensure canvas draws\r\n        self.central_widget.updateGeometry()\r\n        \r\n        # Force an immediate refresh cycle\r\n        if self.designer_view:\r\n            self.designer_view.refresh_all()\r\n            \r\n        # Schedule refreshes to catch layout settling\r\n        QtCore.QTimer.singleShot(50, self._force_designer_refresh)\r\n        QtCore.QTimer.singleShot(200, self._force_designer_refresh)\r\n\r\n        # 6. Setup Return Signal & Layout\r\n        self.designer_view.exitRequested.connect(self.switch_to_game_mode)\r\n        self.layout.addWidget(self.designer_view)\r\n        \r\n        # [FIX] Force layout update and delayed refresh to ensure canvas draws\r\n        self.central_widget.updateGeometry()\r\n        \r\n        # Force an immediate refresh\r\n        if self.designer_view:\r\n            self.designer_view.refresh_all()\r\n            \r\n        # Schedule a second refresh to catch layout settling\r\n        QtCore.QTimer.singleShot(100, self._force_designer_refresh)\r\n\r\n    def _force_designer_refresh(self):\r\n        \"\"\"Helper to center and refresh designer after layout settles.\"\"\"\r\n        if self.designer_view and hasattr(self.designer_view, 'canvas'):\r\n            # Force scene rect update based on items\r\n            if hasattr(self.designer_view.canvas, 'scene') and self.designer_view.canvas.scene:\r\n                items_rect = self.designer_view.canvas.scene.itemsBoundingRect()\r\n                if not items_rect.isEmpty():\r\n                    self.designer_view.canvas.scene.setSceneRect(\r\n                        items_rect.adjusted(-200, -200, 200, 200)\r\n                    )\r\n            \r\n            # Recalculate view center\r\n            self.designer_view.canvas.center_on_neurons()\r\n            self.designer_view.canvas.viewport().update()\r\n\r\n    def switch_to_game_mode(self):\r\n        \"\"\"\r\n        Restores the game view.\r\n        Includes fixes for blank brain widget issues.\r\n        \"\"\"\r\n        if not self.designer_view:\r\n            return\r\n\r\n        print(\">> Switching back to Game Mode...\")\r\n\r\n        # 1. Get Data from Designer\r\n        new_design_data = self.designer_view.get_current_design_state()\r\n        \r\n        # 2. Cleanup Designer\r\n        self.layout.removeWidget(self.designer_view)\r\n        self.designer_view.deleteLater()\r\n        self.designer_view = None\r\n        \r\n        # 3. Restore Tabs\r\n        self.tabs.show()\r\n        \r\n        # 4. Apply State\r\n        if new_design_data:\r\n            self.apply_designer_state(new_design_data)\r\n        \r\n        # 5. Restore Pause State\r\n        if not self._was_paused_before_designer:\r\n            if self.tamagotchi_logic:\r\n                # Route through UI to remove \"SIMULATION PAUSED\" overlay and update menu\r\n                if hasattr(self.tamagotchi_logic, 'user_interface') and \\\r\n                   hasattr(self.tamagotchi_logic.user_interface, 'set_simulation_speed'):\r\n                    self.tamagotchi_logic.user_interface.set_simulation_speed(1)\r\n                else:\r\n                    self.tamagotchi_logic.set_simulation_speed(1) # Resume logic only\r\n            else:\r\n                self.set_pause_state(False)\r\n            \r\n        # Mark dirty immediately\r\n        self.brain_widget.mark_render_dirty()\r\n        \r\n        # Explicitly invoke the offscreen render request logic\r\n        if hasattr(self.brain_widget, '_request_render'):\r\n            # Reset throttling to ensure request goes through\r\n            self.brain_widget._last_render_request = 0 \r\n            self.brain_widget._request_render()\r\n            \r\n        # Force Qt Repaint\r\n        self.brain_widget.update()\r\n        self.brain_widget.repaint()\r\n        \r\n        print(\">> Game Mode Restored\")\r\n\r\n    def apply_designer_state(self, data):\r\n        \"\"\"\r\n        Applies state from Designer to BrainWidget.\r\n        Robustly handles weights/connections in list or dict format.\r\n        \"\"\"\r\n        # 1. Update Weights\r\n        new_weights = {}\r\n        \r\n        # Check for 'connections' (List of dicts OR Dict of key->val)\r\n        connections = data.get('connections')\r\n        \r\n        # If connections is missing, check 'weights' (Legacy dict)\r\n        if not connections:\r\n            connections = data.get('weights', {})\r\n\r\n        if isinstance(connections, dict):\r\n            for key, weight in connections.items():\r\n                # Handle 'src->dst' (Designer) or 'src|dst' (Legacy)\r\n                if '->' in key:\r\n                    s, t = key.split('->')\r\n                    new_weights[(s, t)] = float(weight)\r\n                elif '|' in key:\r\n                    s, t = key.split('|')\r\n                    new_weights[(s, t)] = float(weight)\r\n                    \r\n        elif isinstance(connections, list):\r\n            # List of dicts: [{'source': 'A', 'target': 'B', 'weight': 0.5}, ...]\r\n            for conn in connections:\r\n                if 'source' in conn and 'target' in conn:\r\n                    new_weights[(conn['source'], conn['target'])] = float(conn.get('weight', 0.5))\r\n\r\n        if new_weights:\r\n            self.brain_widget.weights = new_weights\r\n            print(f\"   [Fix] Applied {len(new_weights)} connections from designer\")\r\n\r\n        # 2. Update Positions\r\n        if 'neurons' in data:\r\n            new_positions = {}\r\n            for name, info in data['neurons'].items():\r\n                if 'position' in info:\r\n                    new_positions[name] = tuple(info['position'])\r\n                \r\n                # Initialize new neurons if missing\r\n                if name not in self.brain_widget.state:\r\n                    self.brain_widget.state[name] = 0.0\r\n                    # Apply color if present\r\n                    if 'color' in info:\r\n                        self.brain_widget.state_colors[name] = tuple(info['color'])\r\n                    elif name not in self.brain_widget.state_colors:\r\n                        self.brain_widget.state_colors[name] = (200, 200, 200)\r\n            \r\n            self.brain_widget.neuron_positions = new_positions\r\n\r\n        # 3. Update Bindings\r\n        if 'output_bindings' in data and hasattr(self.tamagotchi_logic, 'neuron_output_monitor'):\r\n            self.brain_widget.output_bindings = data['output_bindings']\r\n            self.tamagotchi_logic.neuron_output_monitor.load_bindings_from_brain(data)\r\n\r\n        # 4. Trigger Updates\r\n        self.brain_widget.mark_render_dirty()\r\n        self.brain_widget.update()\r\n        \r\n        # Refresh Network Tab Stats\r\n        if hasattr(self, 'network_tab'):\r\n            self.network_tab.update_metrics_display()\r\n\r\n    def init_inspector(self):\r\n        self.inspector_action = QtWidgets.QAction(\"Neuron Inspector\", self)\r\n        self.inspector_action.triggered.connect(self.show_inspector)\r\n        self.debug_menu.addAction(self.inspector_action)\r\n\r\n    def show_inspector(self):\r\n        if not hasattr(self, '_inspector') or not self._inspector or not self._inspector.isVisible(): # Check if visible\r\n            # Pass the SquidBrainWindow instance (self) to NeuronInspector\r\n            self._inspector = NeuronInspector(self, self.brain_widget)\r\n        self._inspector.show()\r\n        self._inspector.raise_()\r\n        self._inspector.activateWindow() # Ensure it gets focus\r\n\r\n    def debug_print(self, message):\r\n        if self.debug_mode:\r\n            print(f\"DEBUG: {message}\")\r\n\r\n    def toggle_debug_mode(self, enabled):\r\n        self.debug_mode = enabled\r\n        self.debug_print(f\"Debug mode {'enabled' if enabled else 'disabled'}\")\r\n        # Update stimulate button state\r\n        if hasattr(self, 'stimulate_button'):\r\n            self.stimulate_button.setEnabled(enabled)\r\n\r\n    def init_tabs(self):\r\n        # Get localisation instance\r\n        loc = Localisation.instance()\r\n\r\n        # Create tab widget\r\n        self.tabs = QtWidgets.QTabWidget()\r\n        self.layout.addWidget(self.tabs)\r\n\r\n        # Set base font for all tab content\r\n        base_font = QtGui.QFont()\r\n        base_font.setPointSize(self.base_font_size)\r\n        self.tabs.setFont(base_font)\r\n\r\n        # Create and add existing tabs with Localized Titles\r\n        self.network_tab = NetworkTab(self, self.tamagotchi_logic, self.brain_widget, self.config_manager, self.debug_mode)\r\n        self.tabs.addTab(self.network_tab, loc.get(\"brain_network\", \"Network\"))\r\n\r\n        # Add our Neural Network Visualizer tab as the Learning tab\r\n        self.nn_viz_tab = NeuralNetworkVisualizerTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.nn_viz_tab, loc.get(\"tab_learning\", \"Learning\"))\r\n\r\n        self.memory_tab = MemoryTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.memory_tab, loc.get(\"memory\", \"Memory\"))\r\n\r\n        self.decisions_tab = DecisionsTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.decisions_tab, loc.get(\"tab_decisions\", \"Decisions\"))\r\n\r\n        self.personality_tab = PersonalityTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.personality_tab, loc.get(\"tab_personality\", \"Personality\"))\r\n\r\n        # ADD THE NEW STATISTICS TAB HERE\r\n        self.statistics_tab = StatisticsTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.statistics_tab, loc.get(\"statistics\", \"Statistics\"))\r\n\r\n        self.about_tab = AboutTab(self, self.tamagotchi_logic, self.brain_widget, self.config, self.debug_mode)\r\n        self.tabs.addTab(self.about_tab, loc.get(\"tab_about\", \"About\"))\r\n\r\n        # Make sure all tabs have correct tamagotchi_logic reference\r\n        for tab_name in ['memory_tab', 'network_tab', 'nn_viz_tab', 'decisions_tab', 'personality_tab', 'statistics_tab', 'about_tab']:\r\n            if hasattr(self, tab_name):\r\n                tab = getattr(self, tab_name)\r\n                if hasattr(tab, 'set_tamagotchi_logic') and self.tamagotchi_logic:\r\n                    tab.set_tamagotchi_logic(self.tamagotchi_logic)\r\n                print(f\"Set tamagotchi_logic for {tab_name}\")\r\n\r\n        # Pre-load the learning tab to make it responsive on first click\r\n        if hasattr(self, 'nn_viz_tab'):\r\n            if hasattr(self.nn_viz_tab, 'pre_load_data'):\r\n                QtCore.QTimer.singleShot(700, self.nn_viz_tab.pre_load_data)\r\n\r\n    def get_brain_state(self):\r\n        \"\"\"\r\n        Save brain state for persistence.\r\n        \r\n        IMPORTANT: Core neuron values (hunger, happiness, etc.) are NOT saved here.\r\n        The squid object is the single source of truth for these values.\r\n        Only non-core state (booleans, direction, position) and neurogenesis data are saved.\r\n        \"\"\"\r\n        # Convert weights to a list for safe JSON serialization\r\n        weights_list = []\r\n        for k, v in self.brain_widget.weights.items():\r\n            if isinstance(k, tuple) and len(k) == 2:\r\n                 weights_list.append([k[0], k[1], v])\r\n            else:\r\n                 print(f\"Warning: Skipping non-standard weight key: {k}\")\r\n\r\n        # Core neurons whose values come from squid (single source of truth)\r\n        core_stat_neurons = {'hunger', 'happiness', 'cleanliness', 'sleepiness', \r\n                            'satisfaction', 'anxiety', 'curiosity'}\r\n        \r\n        # Save only non-core neuron states (booleans, direction, position, new neurons)\r\n        # Core stat values will be synced from squid on load\r\n        non_core_states = {}\r\n        for key, value in self.brain_widget.state.items():\r\n            if key not in core_stat_neurons:\r\n                non_core_states[key] = value\r\n\r\n        # Serialize full EnhancedNeurogenesis state (includes functional_neurons,\r\n        # experience_buffer, counters, awarded_neurons, etc.)\r\n        enhanced_neurogenesis_data = {}\r\n        if hasattr(self.brain_widget, 'enhanced_neurogenesis'):\r\n            try:\r\n                enhanced_neurogenesis_data = self.brain_widget.enhanced_neurogenesis.to_dict()\r\n            except Exception as e:\r\n                print(f\"Warning: Could not serialize EnhancedNeurogenesis: {e}\")\r\n\r\n        return {\r\n            'weights_list': weights_list,\r\n            'neuron_positions': {str(k): v for k, v in self.brain_widget.neuron_positions.items()},\r\n            'neuron_states': non_core_states,  # Only non-core states\r\n            'neurogenesis_data': self.brain_widget.neurogenesis_data,\r\n            'state_colors': getattr(self.brain_widget, 'state_colors', {}),\r\n            'enhanced_neurogenesis': enhanced_neurogenesis_data,  # Full neurogenesis state\r\n            # Legacy key for backward compatibility (subset of enhanced_neurogenesis)\r\n            'functional_neurons': enhanced_neurogenesis_data.get('functional_neurons', {})\r\n        }\r\n\r\n    def set_brain_state(self, state):\r\n        \"\"\"\r\n        Load the brain state from a saved state dictionary.\r\n        Uses robust fallback logic to handle missing keys and incomplete data.\r\n        \r\n        IMPORTANT: Core neuron values (hunger, happiness, etc.) are NOT loaded here.\r\n        Call sync_state_from_squid() after this method to populate them from the squid.\r\n        \"\"\"\r\n        # Load weights (handle new list format and potentially old dict format)\r\n        if 'weights_list' in state:\r\n            weights = {}\r\n            for item in state['weights_list']:\r\n                if isinstance(item, list) and len(item) == 3:\r\n                    weights[(item[0], item[1])] = item[2]\r\n            self.brain_widget.weights = weights\r\n        elif 'weights' in state:  # Fallback for old format (acknowledging its bug)\r\n            print(\"Warning: Loading old 'weights' format. May cause issues with new neurons.\")\r\n            weights = {}\r\n            for k, v in state['weights'].items():\r\n                if '_' in k:\r\n                    key = tuple(k.split('_'))  # This is the buggy part\r\n                else:\r\n                    key = k\r\n                weights[key] = v\r\n            self.brain_widget.weights = weights\r\n        else:\r\n            print(\"⚠️  No weights data in state, preserving current weights\")\r\n\r\n        # Load neuron positions, defaulting to original if not found\r\n        self.brain_widget.neuron_positions = state.get('neuron_positions', self.brain_widget.original_neuron_positions.copy())\r\n\r\n        # Load non-core neuron states (booleans, direction, position, new neuron values)\r\n        # Core stat values will be synced from squid via sync_state_from_squid()\r\n        loaded_states = state.get('neuron_states', {})\r\n        self.brain_widget.state = loaded_states.copy()\r\n\r\n        # Load neurogenesis_data (basic tracking for brain_widget display)\r\n        self.brain_widget.neurogenesis_data = state.get('neurogenesis_data', {\r\n            'new_neurons': [], 'last_neuron_time': time.time(), 'new_neurons_details': {}\r\n        })\r\n        if 'new_neurons' not in self.brain_widget.neurogenesis_data:\r\n            self.brain_widget.neurogenesis_data['new_neurons'] = []\r\n        if 'new_neurons_details' not in self.brain_widget.neurogenesis_data:\r\n            self.brain_widget.neurogenesis_data['new_neurons_details'] = {}\r\n\r\n        # Load state colors (with fallback)\r\n        self.brain_widget.state_colors = state.get('state_colors', {\r\n            'is_sick': (255, 204, 204), 'is_eating': (204, 255, 204),\r\n            'is_sleeping': (204, 229, 255), 'pursuing_food': (255, 229, 204),\r\n            'direction': (229, 204, 255)\r\n        })\r\n\r\n        # =====================================================================\r\n        # LOAD FULL EnhancedNeurogenesis STATE\r\n        # =====================================================================\r\n        if hasattr(self.brain_widget, 'enhanced_neurogenesis'):\r\n            # Prefer new 'enhanced_neurogenesis' key with full state\r\n            if 'enhanced_neurogenesis' in state:\r\n                try:\r\n                    self.brain_widget.enhanced_neurogenesis.from_dict(state['enhanced_neurogenesis'])\r\n                    fn_count = len(self.brain_widget.enhanced_neurogenesis.functional_neurons)\r\n                    print(f\"✅ Loaded full EnhancedNeurogenesis state ({fn_count} functional neurons)\")\r\n                    \r\n                    # If from_dict failed to load neurons, try direct loading\r\n                    if fn_count == 0 and 'functional_neurons' in state['enhanced_neurogenesis']:\r\n                        print(\"⚠️  from_dict returned 0 neurons, attempting direct load...\")\r\n                        self._direct_load_functional_neurons(state['enhanced_neurogenesis']['functional_neurons'])\r\n                except Exception as e:\r\n                    print(f\"⚠️  Error loading EnhancedNeurogenesis: {e}\")\r\n                    import traceback\r\n                    traceback.print_exc()\r\n                    # Fall back to legacy loading\r\n                    self._load_legacy_functional_neurons(state)\r\n            # Fallback: legacy 'functional_neurons' key only\r\n            elif 'functional_neurons' in state:\r\n                self._load_legacy_functional_neurons(state)\r\n                print(\"ℹ️  Loaded legacy functional_neurons format\")\r\n\r\n        # =====================================================================\r\n        # FORCIBLY REBUILD NEUROGENESIS NEURONS FROM functional_neurons OR state\r\n        # =====================================================================\r\n        self._force_rebuild_neurogenesis_neurons(state)\r\n\r\n        # --- Critical Step: Ensure brain_widget consistency after loading ---\r\n        all_neurons = list(self.brain_widget.neuron_positions.keys())\r\n        new_neurons_list = self.brain_widget.neurogenesis_data.get('new_neurons', [])\r\n\r\n        # Get config colors for new neurons\r\n        cfg_appearance = self.config.neurogenesis.get('appearance', {})\r\n        cfg_colors = cfg_appearance.get('colors', {})\r\n        default_colors = {'novelty': (255, 255, 150), 'stress': (255, 150, 150), 'reward': (150, 255, 150)}\r\n\r\n        for neuron in all_neurons:\r\n            # Ensure all neurons exist in the state dictionary (default 50 for new neurons)\r\n            if neuron not in self.brain_widget.state:\r\n                self.brain_widget.state[neuron] = 50\r\n\r\n            # Ensure all neurons exist in communication events\r\n            if hasattr(self.brain_widget, 'communication_events') and neuron not in self.brain_widget.communication_events:\r\n                self.brain_widget.communication_events[neuron] = 0\r\n\r\n            # Ensure new neurons have a color entry\r\n            if neuron in new_neurons_list and neuron not in self.brain_widget.state_colors:\r\n                if neuron.startswith('novel'):\r\n                    color = tuple(cfg_colors.get('novelty', default_colors['novelty']))\r\n                elif neuron.startswith('stress'):\r\n                    color = tuple(cfg_colors.get('stress', default_colors['stress']))\r\n                elif neuron.startswith('reward'):\r\n                    color = tuple(cfg_colors.get('reward', default_colors['reward']))\r\n                else:\r\n                    color = (200, 200, 200)\r\n                self.brain_widget.state_colors[neuron] = color\r\n\r\n        # Ensure all core neurons have their original positions if missing\r\n        for name, pos in self.brain_widget.original_neuron_positions.items():\r\n            if name not in self.brain_widget.neuron_positions:\r\n                self.brain_widget.neuron_positions[name] = pos\r\n\r\n        # Reveal all core neurons for loaded games\r\n        self.brain_widget.reveal_all_core_neurons()\r\n        \r\n        # Trigger staggered link fade animation when loading a save\r\n        if self.brain_widget.show_links:\r\n            self.brain_widget._enable_links_after_reveal()\r\n        self.brain_widget.update()\r\n\r\n    def _load_legacy_functional_neurons(self, state):\r\n        \"\"\"Fallback loader for old save format with only functional_neurons key.\"\"\"\r\n        from .neurogenesis import FunctionalNeuron\r\n        \r\n        functional_neurons_data = state.get('functional_neurons', {})\r\n        restored_neurons = {}\r\n        \r\n        for name, data in functional_neurons_data.items():\r\n            try:\r\n                restored_neurons[name] = FunctionalNeuron.from_dict(data)\r\n            except Exception as e:\r\n                print(f\"Warning: Could not restore neuron {name}: {e}\")\r\n                continue\r\n        \r\n        self.brain_widget.enhanced_neurogenesis.functional_neurons = restored_neurons\r\n        print(f\"Loaded {len(restored_neurons)} functional neurons (legacy format)\")\r\n\r\n    def _direct_load_functional_neurons(self, functional_neurons_data):\r\n        \"\"\"\r\n        Directly load FunctionalNeuron objects from save data, bypassing from_dict.\r\n        This is a fallback when the normal loading path fails.\r\n        \"\"\"\r\n        from .neurogenesis import FunctionalNeuron, ExperienceContext\r\n        \r\n        if not functional_neurons_data:\r\n            print(\"⚠️  No functional_neurons_data to load\")\r\n            return\r\n        \r\n        loaded_count = 0\r\n        for name, data in functional_neurons_data.items():\r\n            try:\r\n                # Manually construct the FunctionalNeuron\r\n                creation_ctx = data.get('creation_context', {})\r\n                active_neurons_data = creation_ctx.get('active_neurons') or creation_ctx.get('brain_state', {})\r\n                \r\n                context = ExperienceContext(\r\n                    trigger_type=creation_ctx.get('trigger_type', 'stress'),\r\n                    active_neurons=active_neurons_data,\r\n                    recent_actions=creation_ctx.get('recent_actions', []),\r\n                    environmental_state=creation_ctx.get('environmental_state', {}),\r\n                    outcome=creation_ctx.get('outcome', 'neutral'),\r\n                    timestamp=creation_ctx.get('timestamp', time.time())\r\n                )\r\n                \r\n                neuron = FunctionalNeuron(\r\n                    name=data.get('name', name),\r\n                    neuron_type=data.get('neuron_type', 'stress'),\r\n                    creation_context=context\r\n                )\r\n                \r\n                # Restore additional attributes\r\n                neuron.specialization = data.get('specialization', neuron.specialization)\r\n                neuron.activation_count = data.get('activation_count', 0)\r\n                neuron.last_activated = data.get('last_activated', 0)\r\n                neuron.utility_score = data.get('utility_score', 0.0)\r\n                neuron.strength_multiplier = data.get('strength_multiplier', 1.0)\r\n                \r\n                self.brain_widget.enhanced_neurogenesis.functional_neurons[name] = neuron\r\n                loaded_count += 1\r\n                print(f\"   ↪ Direct loaded: {name}\")\r\n                \r\n            except Exception as e:\r\n                print(f\"⚠️  Failed to direct load {name}: {e}\")\r\n                import traceback\r\n                traceback.print_exc()\r\n        \r\n        print(f\"✅ Direct loaded {loaded_count} functional neurons\")\r\n\r\n    def _force_rebuild_neurogenesis_neurons(self, state=None):\r\n        \"\"\"\r\n        Forcibly rebuild all neurogenesis neurons from functional_neurons data.\r\n        This ensures neurons are properly drawn after loading a save.\r\n        \r\n        Args:\r\n            state: Optional brain state dict to use as fallback data source\r\n        \"\"\"\r\n        if not hasattr(self.brain_widget, 'enhanced_neurogenesis'):\r\n            return\r\n        \r\n        functional_neurons = self.brain_widget.enhanced_neurogenesis.functional_neurons\r\n        \r\n        # If no functional neurons loaded, try to load directly from state\r\n        if not functional_neurons and state:\r\n            print(\"⚠️  No functional neurons in enhanced_neurogenesis, loading from state...\")\r\n            fn_data = None\r\n            if 'enhanced_neurogenesis' in state and 'functional_neurons' in state['enhanced_neurogenesis']:\r\n                fn_data = state['enhanced_neurogenesis']['functional_neurons']\r\n            elif 'functional_neurons' in state:\r\n                fn_data = state['functional_neurons']\r\n            \r\n            if fn_data:\r\n                self._direct_load_functional_neurons(fn_data)\r\n                functional_neurons = self.brain_widget.enhanced_neurogenesis.functional_neurons\r\n        \r\n        if not functional_neurons:\r\n            print(\"⚠️  No functional neurons to rebuild\")\r\n            return\r\n        \r\n        core_neurons = {'hunger', 'happiness', 'cleanliness', 'sleepiness',\r\n                        'satisfaction', 'anxiety', 'curiosity'}\r\n        excluded = getattr(self.brain_widget, 'excluded_neurons', [])\r\n        \r\n        # Ensure neurogenesis_data structures exist\r\n        if not hasattr(self.brain_widget, 'neurogenesis_data'):\r\n            self.brain_widget.neurogenesis_data = {}\r\n        self.brain_widget.neurogenesis_data.setdefault('new_neurons', [])\r\n        self.brain_widget.neurogenesis_data.setdefault('new_neurons_details', {})\r\n        \r\n        # Ensure neuron_shapes exists\r\n        if not hasattr(self.brain_widget, 'neuron_shapes'):\r\n            self.brain_widget.neuron_shapes = {}\r\n        \r\n        rebuilt_count = 0\r\n        \r\n        for name, fn in functional_neurons.items():\r\n            if name in core_neurons or name in excluded:\r\n                continue\r\n            \r\n            rebuilt_count += 1\r\n            \r\n            # 1. Force add to new_neurons list\r\n            if name not in self.brain_widget.neurogenesis_data['new_neurons']:\r\n                self.brain_widget.neurogenesis_data['new_neurons'].append(name)\r\n            \r\n            # 2. Force add to new_neurons_details\r\n            if name not in self.brain_widget.neurogenesis_data['new_neurons_details']:\r\n                self.brain_widget.neurogenesis_data['new_neurons_details'][name] = {\r\n                    'created_at': fn.creation_context.timestamp,\r\n                    'trigger_type': fn.neuron_type,\r\n                    'trigger_value_at_creation': 0,\r\n                    'specialisation': fn.specialization\r\n                }\r\n            \r\n            # 3. Force add to neuron_positions (calculate if missing)\r\n            if name not in self.brain_widget.neuron_positions:\r\n                # Calculate position based on functional connections\r\n                all_neurons = list(self.brain_widget.neuron_positions.keys())\r\n                connections = fn.get_functional_connections(all_neurons)\r\n                \r\n                if connections:\r\n                    total_weight = 0\r\n                    center_x, center_y = 0, 0\r\n                    for target, weight in connections.items():\r\n                        if target in self.brain_widget.neuron_positions:\r\n                            pos = self.brain_widget.neuron_positions[target]\r\n                            abs_weight = abs(weight)\r\n                            center_x += pos[0] * abs_weight\r\n                            center_y += pos[1] * abs_weight\r\n                            total_weight += abs_weight\r\n                    if total_weight > 0:\r\n                        center_x /= total_weight\r\n                        center_y /= total_weight\r\n                        x = max(50, min(974, center_x + random.randint(-80, 80)))\r\n                        y = max(50, min(668, center_y + random.randint(-80, 80)))\r\n                        self.brain_widget.neuron_positions[name] = (x, y)\r\n                    else:\r\n                        self.brain_widget.neuron_positions[name] = (random.randint(100, 900), random.randint(100, 600))\r\n                else:\r\n                    self.brain_widget.neuron_positions[name] = (random.randint(100, 900), random.randint(100, 600))\r\n                print(f\"   ↪ Rebuilt position for {name}\")\r\n            \r\n            # 4. Force add to state\r\n            if name not in self.brain_widget.state:\r\n                self.brain_widget.state[name] = 50.0\r\n            \r\n            # 5. Force add to visible_neurons\r\n            if hasattr(self.brain_widget, 'visible_neurons'):\r\n                self.brain_widget.visible_neurons.add(name)\r\n            \r\n            # 6. Force set shape based on neuron type\r\n            shape_map = {'novelty': 'diamond', 'stress': 'square', 'reward': 'triangle'}\r\n            self.brain_widget.neuron_shapes[name] = shape_map.get(fn.neuron_type, 'circle')\r\n            \r\n            # 7. Force set color based on type and specialization\r\n            spec = fn.specialization\r\n            if 'stress' in spec or 'anxiety' in spec:\r\n                self.brain_widget.state_colors[name] = (255, 150, 150)\r\n            elif 'reward' in spec or 'satisfaction' in spec:\r\n                self.brain_widget.state_colors[name] = (150, 255, 150)\r\n            elif 'investigation' in spec or 'exploration' in spec:\r\n                self.brain_widget.state_colors[name] = (255, 215, 0)\r\n            else:\r\n                color_map = {'novelty': (255, 255, 150), 'stress': (255, 150, 150), 'reward': (173, 216, 230)}\r\n                self.brain_widget.state_colors[name] = color_map.get(fn.neuron_type, (200, 200, 255))\r\n            \r\n            # 8. Force add to communication_events\r\n            if hasattr(self.brain_widget, 'communication_events'):\r\n                if name not in self.brain_widget.communication_events:\r\n                    self.brain_widget.communication_events[name] = 0\r\n            \r\n            # 9. Ensure connections exist\r\n            all_neurons = list(self.brain_widget.neuron_positions.keys())\r\n            connections = fn.get_functional_connections(all_neurons)\r\n            for target, weight in connections.items():\r\n                if (name, target) not in self.brain_widget.weights:\r\n                    self.brain_widget.weights[(name, target)] = weight\r\n        \r\n        if rebuilt_count > 0:\r\n            print(f\"🔧 Force rebuilt {rebuilt_count} neurogenesis neurons from save data\")\r\n\r\n    def sync_state_from_squid(self, squid):\r\n        \"\"\"\r\n        Sync brain_widget.state with current squid stats.\r\n        \r\n        This makes the squid the single source of truth for core neuron values.\r\n        Call this after set_brain_state() during game load.\r\n        \"\"\"\r\n        if not squid:\r\n            print(\"⚠️  Cannot sync: no squid provided\")\r\n            return\r\n        \r\n        # Core stats that come from squid\r\n        self.brain_widget.state['hunger'] = squid.hunger\r\n        self.brain_widget.state['happiness'] = squid.happiness\r\n        self.brain_widget.state['cleanliness'] = squid.cleanliness\r\n        self.brain_widget.state['sleepiness'] = squid.sleepiness\r\n        self.brain_widget.state['satisfaction'] = squid.satisfaction\r\n        self.brain_widget.state['anxiety'] = squid.anxiety\r\n        self.brain_widget.state['curiosity'] = squid.curiosity\r\n        \r\n        # Boolean states from squid\r\n        self.brain_widget.state['is_sick'] = squid.is_sick\r\n        self.brain_widget.state['is_sleeping'] = getattr(squid, 'is_sleeping', False)\r\n        self.brain_widget.state['is_eating'] = getattr(squid, 'is_eating', False)\r\n        self.brain_widget.state['pursuing_food'] = getattr(squid, 'pursuing_food', False)\r\n        self.brain_widget.state['is_startled'] = getattr(squid, 'is_startled', False)\r\n        self.brain_widget.state['is_fleeing'] = getattr(squid, 'is_fleeing', False)\r\n        self.brain_widget.state['direction'] = getattr(squid, 'squid_direction', 'up')\r\n        self.brain_widget.state['position'] = (squid.squid_x, squid.squid_y)\r\n        \r\n        print(\"✅ Brain state synced from squid\")\r\n\r\n    def init_timers(self):\r\n        # Hebbian learning timer\r\n        self.hebbian_timer = QtCore.QTimer()\r\n        self.hebbian_timer.timeout.connect(self.brain_widget.perform_hebbian_learning)\r\n        self.hebbian_timer.start(self.config.hebbian.get('learning_interval', 30000))\r\n        \r\n        # Hebbian countdown\r\n        self.hebbian_countdown_seconds = int(self.config.hebbian.get('learning_interval', 30000) / 1000)\r\n\r\n        # Add countdown timer\r\n        self.countdown_timer = QtCore.QTimer()\r\n        self.countdown_timer.timeout.connect(self.update_countdown)\r\n        self.countdown_timer.start(1000)  # Update every second\r\n\r\n        # Associations timer\r\n        self.update_timer = QtCore.QTimer()\r\n        self.update_timer.timeout.connect(self.update_associations)\r\n        self.update_timer.start(10000)  # Update every 10 seconds\r\n        self.last_update_time = time.time()\r\n        self.update_threshold = 5  # Minimum seconds between updates\r\n\r\n\r\n    def update_randomness_factors(self, randomness):\r\n        \"\"\"Update the randomness factors table\"\"\"\r\n        self.random_factors_table.setRowCount(len(randomness))\r\n        \r\n        for i, (action, factor) in enumerate(randomness.items()):\r\n            # Action name\r\n            action_item = QtWidgets.QTableWidgetItem(action)\r\n            self.random_factors_table.setItem(i, 0, action_item)\r\n            \r\n            # Random factor\r\n            value_item = QtWidgets.QTableWidgetItem(f\"{factor:.2f}\")\r\n            color = QtGui.QColor(\"darkgreen\") if factor > 1.0 else QtGui.QColor(\"darkred\")\r\n            value_item.setForeground(color)\r\n            self.random_factors_table.setItem(i, 1, value_item)\r\n\r\n\r\n    def create_thought_node(self, text):\r\n        node = QtWidgets.QGraphicsRectItem(0, 0, 250, 150)  # Increase node size\r\n        node.setBrush(QtGui.QBrush(QtGui.QColor(240, 248, 255)))\r\n\r\n        # Use QTextDocument for better text handling\r\n        text_document = QtGui.QTextDocument()\r\n        text_document.setPlainText(text)\r\n        text_document.setTextWidth(230)  # Set text width to fit within the node\r\n\r\n        # Create a QGraphicsTextItem with an empty string\r\n        text_item = QtWidgets.QGraphicsTextItem()\r\n        text_item.setDocument(text_document)\r\n        text_item.setPos(10, 10)\r\n\r\n        group = QtWidgets.QGraphicsItemGroup()\r\n        group.addToGroup(node)\r\n        group.addToGroup(text_item)\r\n        return group\r\n\r\n    def draw_connection(self, start, end, label):\r\n        line = QtWidgets.QGraphicsLineItem(start[0]+200, start[1]+50, end[0], end[1]+50)\r\n        line.setPen(QtGui.QPen(QtCore.Qt.darkGray, 2, QtCore.Qt.DashLine))\r\n        self.decision_scene.addItem(line)\r\n\r\n        arrow = QtWidgets.QGraphicsPolygonItem(\r\n            QtGui.QPolygonF([QtCore.QPointF(0, -5), QtCore.QPointF(10, 0), QtCore.QPointF(0, 5)]))\r\n        arrow.setPos(end[0], end[1]+50)\r\n        arrow.setRotation(180 if start[0] > end[0] else 0)\r\n        self.decision_scene.addItem(arrow)\r\n\r\n        label_item = QtWidgets.QGraphicsTextItem(label)\r\n        label_item.setPos((start[0]+end[0])/2, (start[1]+end[1])/2)\r\n        self.decision_scene.addItem(label_item)\r\n\r\n    \r\n    def _get_memory_colors(self, memory):\r\n        \"\"\"Determine colors based on memory content\"\"\"\r\n        if 'positive' in memory.get('tags', []):\r\n            return \"#E8F5E9\", \"#C8E6C9\"  # Green shades\r\n        elif 'negative' in memory.get('tags', []):\r\n            return \"#FFEBEE\", \"#FFCDD2\"   # Red shades\r\n        elif 'novelty' in memory.get('tags', []):\r\n            return \"#FFFDE7\", \"#FFF9C4\"   # Yellow shades\r\n        return \"#F5F5F5\", \"#EEEEEE\"       # Default gray\r\n\r\n    def update_memory_tab(self):\r\n        \"\"\"Update memory tab if it exists\"\"\"\r\n        if hasattr(self, 'memory_tab'):\r\n            # Forward to the tab's update method\r\n            self.memory_tab.update_memory_display()\r\n\r\n    def _update_overview_stats(self, stm, ltm):\r\n        \"\"\"Update the overview tab with statistics\"\"\"\r\n        stats_html = \"\"\"\r\n        <style>\r\n            .stat-box { \r\n                background: #f8f9fa; \r\n                border-radius: 10px; \r\n                padding: 15px; \r\n                margin: 10px;\r\n            }\r\n            .stat-title { \r\n                font-size: 10pt; \r\n                color: #2c3e50; \r\n                margin-bottom: 10px;\r\n            }\r\n        </style>\r\n        \"\"\"\r\n        \r\n        # Memory counts\r\n        stats_html += \"\"\"\r\n        <div class=\"stat-box\">\r\n            <div class=\"stat-title\">📈 Memory Statistics</div>\r\n            <table>\r\n                <tr><td>Short-Term Memories:</td><td style=\"padding-left: 20px;\">{stm_count}</td></tr>\r\n                <tr><td>Long-Term Memories:</td><td style=\"padding-left: 20px;\">{ltm_count}</td></tr>\r\n            </table>\r\n        </div>\r\n        \"\"\".format(stm_count=len(stm), ltm_count=len(ltm))\r\n        \r\n        # Category breakdown\r\n        categories = {}\r\n        for m in stm + ltm:\r\n            cat = m.get('category', 'unknown')\r\n            categories[cat] = categories.get(cat, 0) + 1\r\n        \r\n        category_html = \"\\n\".join(\r\n            f\"<tr><td>{k}:</td><td style='padding-left: 20px;'>{v}</td></tr>\"\r\n            for k, v in sorted(categories.items())\r\n        )\r\n        \r\n        stats_html += f\"\"\"\r\n        <div class=\"stat-box\">\r\n            <div class=\"stat-title\">🗂️ Categories</div>\r\n            <table>{category_html}</table>\r\n        </div>\r\n        \"\"\"\r\n        \r\n        self.overview_stats.setHtml(stats_html)\r\n\r\n    def _clear_layout(self, layout):\r\n        \"\"\"Clear all widgets from the given layout\"\"\"\r\n        while layout.count():\r\n            item = layout.takeAt(0)\r\n            widget = item.widget()\r\n            if widget is not None:\r\n                widget.deleteLater()\r\n\r\n    def set_pause_state(self, is_paused):\r\n        \"\"\"Set pause state for the brain window and worker thread\"\"\"\r\n        self.is_paused = is_paused\r\n        \r\n        # Set brain widget pause state\r\n        if hasattr(self, 'brain_widget'):\r\n            self.brain_widget.is_paused = is_paused\r\n        \r\n        # Control worker thread - CRITICAL for freezing everything\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            if is_paused:\r\n                self.brain_worker.pause()\r\n                self._pause_start_time = time.time() # Capture time when paused\r\n                print(\"⏸️ BrainWorker paused\")\r\n            else:\r\n                self.brain_worker.resume()\r\n                \r\n                # Adjust timestamps to account for pause duration\r\n                if hasattr(self, '_pause_start_time'):\r\n                    pause_duration = time.time() - self._pause_start_time\r\n                    \r\n                    # 1. Adjust Hebbian timer reference\r\n                    if hasattr(self.brain_widget, 'last_hebbian_time'):\r\n                        self.brain_widget.last_hebbian_time += pause_duration\r\n                        \r\n                    # 2. Adjust Neurogenesis timer reference\r\n                    if hasattr(self.brain_widget, 'neurogenesis_data'):\r\n                        self.brain_widget.neurogenesis_data['last_neuron_time'] += pause_duration\r\n                        \r\n                    # 3. Adjust Animation timer references in brain_widget to prevent jumps\r\n                    if hasattr(self.brain_widget, '_last_animation_time'):\r\n                        self.brain_widget._last_animation_time += pause_duration\r\n                    \r\n                    # 4. Adjust running weight animations\r\n                    if hasattr(self.brain_widget, 'weight_animations'):\r\n                        for anim in self.brain_widget.weight_animations:\r\n                            anim['start_time'] += pause_duration\r\n                            \r\n                    # 5. Adjust reveal animations\r\n                    if hasattr(self.brain_widget, 'neuron_reveal_animations'):\r\n                        for anim in self.brain_widget.neuron_reveal_animations.values():\r\n                            anim['start_time'] += pause_duration\r\n                            \r\n                    # 6. Adjust neurogenesis highlight\r\n                    if hasattr(self.brain_widget, 'neurogenesis_highlight'):\r\n                        self.brain_widget.neurogenesis_highlight['start_time'] += pause_duration\r\n\r\n                print(\"▶️ BrainWorker resumed\")\r\n\r\n        # Manage timers based on pause state\r\n        if is_paused:\r\n            if hasattr(self, 'hebbian_timer'):\r\n                self.hebbian_timer.stop()\r\n        else:\r\n            if hasattr(self, 'hebbian_timer'):\r\n                self.hebbian_timer.start(self.config.hebbian['learning_interval'])\r\n\r\n    def _create_memory_card(self, memory):\r\n        \"\"\"Create a styled HTML memory card with tooltip\"\"\"\r\n        category = memory.get('category', 'unknown')\r\n        value = memory.get('formatted_value', str(memory.get('value', '')))\r\n        timestamp = memory.get('timestamp', '')\r\n        effects = memory.get('effects', {})\r\n        \r\n        # Determine card color based on memory type\r\n        bg_color, border_color = self._get_memory_colors(memory)\r\n        \r\n        # Create concise display text\r\n        card_html = f\"\"\"\r\n        <div style=\"\r\n            background-color: {bg_color};\r\n            border: 2px solid {border_color};\r\n            border-radius: 10px;\r\n            padding: 15px;\r\n            margin: 10px;\r\n            font-size: 10pt;\r\n        \">\r\n            <div style=\"font-weight: bold; color: #333;\">{category.capitalize()}</div>\r\n            <div style=\"font-size: 12pt; margin-top: 8px;\">{value[:60]}</div>\r\n            <div style=\"font-size: 10pt; color: #666; margin-top: 8px;\">\r\n                {timestamp.split(' ')[-1]}\r\n            </div>\r\n        </div>\r\n        \"\"\"\r\n        \r\n        # Create tooltip with extended info\r\n        tooltip = f\"\"\"\r\n        <b>Full Content:</b> {value}<br>\r\n        <b>Effects:</b> {', '.join(f'{k}: {v}' for k,v in effects.items())}<br>\r\n        <b>Last Accessed:</b> {timestamp}\r\n        \"\"\"\r\n        \r\n        # Create widget with HTML and tooltip\r\n        card = QtWidgets.QTextEdit()\r\n        card.setHtml(card_html)\r\n        card.setReadOnly(True)\r\n        card.setToolTip(tooltip)\r\n        card.setFixedHeight(120)\r\n        card.setStyleSheet(\"border: none;\")\r\n        \r\n        return card\r\n    \r\n    def _get_card_style(self, memory):\r\n        \"\"\"Get CSS style for a memory card based on its valence and importance\"\"\"\r\n        # Determine card background color based on memory valence\r\n        if memory.get('category') == 'mental_state' and memory.get('key') == 'startled':\r\n            background_color = \"#FFD1DC\"  # Pastel red for negative\r\n        elif isinstance(memory.get('raw_value'), dict):\r\n            total_effect = sum(float(val) for val in memory['raw_value'].values() \r\n                            if isinstance(val, (int, float)))\r\n            if total_effect > 0:\r\n                background_color = \"#D1FFD1\"  # Pastel green for positive\r\n            elif total_effect < 0:\r\n                background_color = \"#FFD1DC\"  # Pastel red for negative\r\n            else:\r\n                background_color = \"#FFFACD\"  # Pastel yellow for neutral\r\n        else:\r\n            background_color = \"#FFFACD\"  # Pastel yellow for neutral\r\n        \r\n        # Determine border based on importance (only for short-term)\r\n        importance = memory.get('importance', 1)\r\n        if importance >= 7:\r\n            border = \"2px solid #FF5733\"  # Important memory\r\n        elif importance >= 4:\r\n            border = \"1px solid #666\"     # Medium importance\r\n        else:\r\n            border = \"1px solid #ccc\"     # Low importance\r\n        \r\n        return f\"\"\"\r\n            background-color: {background_color};\r\n            border: {border};\r\n            border-radius: 8px;\r\n            padding: 8px;\r\n            margin: 4px;\r\n        \"\"\"\r\n    \r\n    def _format_memory_content(self, memory):\r\n        \"\"\"Format memory content for display in a card\"\"\"\r\n        # Get the display text - prefer formatted_value, fall back to value\r\n        display_text = memory.get('formatted_value', str(memory.get('value', '')))\r\n        \r\n        # Skip if the display text contains just a timestamp\r\n        if 'timestamp' in display_text.lower() and len(display_text.split()) < 3:\r\n            return \"<i>Empty memory</i>\"\r\n        \r\n        # Make text more concise and readable\r\n        # Remove any HTML tags already in the text to avoid nesting issues\r\n        import re\r\n        display_text = re.sub(r'<[^>]*>', '', display_text)\r\n        \r\n        # Format based on category\r\n        category = memory.get('category', '')\r\n        \r\n        if category == 'decorations':\r\n            # Extract the important parts of decoration interactions\r\n            if 'interaction with' in display_text.lower():\r\n                parts = display_text.split(':')\r\n                if len(parts) >= 2:\r\n                    item = parts[0].strip()\r\n                    effects = parts[1].strip()\r\n                    return f\"<b>{item}</b><br>{effects}\"\r\n        \r\n        # Default formatting with bold beginning\r\n        words = display_text.split()\r\n        if len(words) > 3:\r\n            bold_part = ' '.join(words[:3])\r\n            rest = ' '.join(words[3:])\r\n            return f\"<b>{bold_part}</b> {rest}\"\r\n        \r\n        return display_text\r\n    \r\n    def _create_memory_tooltip(self, memory):\r\n        \"\"\"Create detailed tooltip for a memory card\"\"\"\r\n        tooltip = \"<html><body style='white-space:pre'>\"\r\n        tooltip += f\"<b>Category:</b> {memory.get('category', 'unknown')}\\n\"\r\n        tooltip += f\"<b>Key:</b> {memory.get('key', 'unknown')}\\n\"\r\n        \r\n        timestamp = memory.get('timestamp', '')\r\n        if isinstance(timestamp, str):\r\n            try:\r\n                from datetime import datetime\r\n                timestamp = datetime.fromisoformat(timestamp).strftime(\"%H:%M:%S\")\r\n            except:\r\n                timestamp = \"\"\r\n        \r\n        tooltip += f\"<b>Time:</b> {timestamp}\\n\"\r\n        \r\n        if 'importance' in memory:\r\n            tooltip += f\"<b>Importance:</b> {memory.get('importance')}\\n\"\r\n        \r\n        if 'access_count' in memory:\r\n            tooltip += f\"<b>Access count:</b> {memory.get('access_count')}\\n\"\r\n        \r\n        if isinstance(memory.get('raw_value'), dict):\r\n            tooltip += \"\\n<b>Effects:</b>\\n\"\r\n            for key, value in memory['raw_value'].items():\r\n                if isinstance(value, (int, float)):\r\n                    tooltip += f\"  {key}: {value:+.2f}\\n\"\r\n        \r\n        tooltip += \"</body></html>\"\r\n        return tooltip\r\n    \r\n    def _get_stm(self):\r\n        \"\"\"Get short-term memories from squid's memory manager\"\"\"\r\n        if hasattr(self.tamagotchi_logic, 'squid') and hasattr(self.tamagotchi_logic.squid, 'memory_manager'):\r\n            return self.tamagotchi_logic.squid.memory_manager.get_all_short_term_memories()\r\n        return []\r\n        \r\n    def _get_ltm(self):\r\n        \"\"\"Get long-term memories from squid's memory manager\"\"\"\r\n        if hasattr(self.tamagotchi_logic, 'squid') and hasattr(self.tamagotchi_logic.squid, 'memory_manager'):\r\n            return self.tamagotchi_logic.squid.memory_manager.get_all_long_term_memories()\r\n        return []\r\n    \r\n    def _is_displayable(self, memory):\r\n        \"\"\"Check if a memory should be displayed in the UI\"\"\"\r\n        if not isinstance(memory, dict):\r\n            return False\r\n        \r\n        # Skip timestamp-like memories (with numeric keys or timestamp values)\r\n        if isinstance(memory.get('key'), str):\r\n            # Filter out numeric keys (timestamps)\r\n            if memory['key'].replace('.', '', 1).isdigit():\r\n                return False\r\n        \r\n        # Check the value - filter out memories with timestamp values\r\n        value = memory.get('value', '')\r\n        if isinstance(value, str) and 'timestamp' in value.lower():\r\n            return False\r\n            \r\n        # For memories with formatted_value\r\n        formatted_value = memory.get('formatted_value', '')\r\n        if isinstance(formatted_value, str):\r\n            # If it contains timestamp numbers as part of the interaction\r\n            if 'Interaction with' in formatted_value and any(c.isdigit() for c in formatted_value.split('with')[1]):\r\n                return False\r\n        \r\n        # Check for timestamp-like value in the memory\r\n        if 'Interaction with' in str(formatted_value) and '.' in str(formatted_value):\r\n            timestamp_part = str(formatted_value).split('with')[1].strip()\r\n            # If it looks like a float timestamp (e.g., 1744308365.4552662)\r\n            if '.' in timestamp_part and any(part.replace('.', '', 1).isdigit() for part in timestamp_part.split()):\r\n                return False\r\n        \r\n        # Skip memories that don't have a proper category or value\r\n        if not memory.get('category') or not memory.get('value'):\r\n            return False\r\n            \r\n        # Must have either formatted_value or a displayable string value\r\n        if 'formatted_value' not in memory and not isinstance(memory.get('value'), str):\r\n            return False\r\n            \r\n        return True\r\n    \r\n    def _update_memory_stats(self, short_term_memories, long_term_memories):\r\n        \"\"\"Update memory statistics in the overview tab\"\"\"\r\n        stats_html = \"<h2>Memory System Statistics</h2>\"\r\n        \r\n        # Basic stats\r\n        stats_html += f\"<p><b>Short-term memories:</b> {len(short_term_memories)}</p>\"\r\n        stats_html += f\"<p><b>Long-term memories:</b> {len(long_term_memories)}</p>\"\r\n        \r\n        # Category breakdown\r\n        stats_html += \"<h3>Memory Categories</h3>\"\r\n        \r\n        # Count memories by category\r\n        all_memories = short_term_memories + long_term_memories\r\n        categories = {}\r\n        \r\n        for mem in all_memories:\r\n            category = mem.get('category', 'unknown')\r\n            categories[category] = categories.get(category, 0) + 1\r\n        \r\n        # Create category table\r\n        if categories:\r\n            stats_html += \"<table border='1' cellpadding='4' style='border-collapse: collapse;'>\"\r\n            stats_html += \"<tr><th>Category</th><th>Count</th></tr>\"\r\n            \r\n            for category, count in sorted(categories.items(), key=lambda x: x[1], reverse=True):\r\n                stats_html += f\"<tr><td>{category}</td><td>{count}</td></tr>\"\r\n            \r\n            stats_html += \"</table>\"\r\n        \r\n        # Memory importance breakdown (short-term only)\r\n        if short_term_memories:\r\n            stats_html += \"<h3>Memory Importance (Short-term)</h3>\"\r\n            \r\n            importance_levels = {\r\n                \"High (7-10)\": 0,\r\n                \"Medium (4-6)\": 0,\r\n                \"Low (1-3)\": 0\r\n            }\r\n            \r\n            for mem in short_term_memories:\r\n                imp = mem.get('importance', 1)\r\n                if imp >= 7:\r\n                    importance_levels[\"High (7-10)\"] += 1\r\n                elif imp >= 4:\r\n                    importance_levels[\"Medium (4-6)\"] += 1\r\n                else:\r\n                    importance_levels[\"Low (1-3)\"] += 1\r\n            \r\n            stats_html += \"<table border='1' cellpadding='4' style='border-collapse: collapse;'>\"\r\n            stats_html += \"<tr><th>Importance Level</th><th>Count</th></tr>\"\r\n            \r\n            for level, count in importance_levels.items():\r\n                stats_html += f\"<tr><td>{level}</td><td>{count}</td></tr>\"\r\n            \r\n            stats_html += \"</table>\"\r\n        \r\n        # Update stats display\r\n        self.memory_stats_text.setHtml(stats_html)\r\n\r\n\r\n    def add_thought(self, thought):\r\n        \"\"\"Bridge method to forward thoughts to the decisions tab\"\"\"\r\n        if hasattr(self, 'decisions_tab') and self.decisions_tab:\r\n            # If we have a decisions tab, forward to its thought log\r\n            if hasattr(self.decisions_tab, 'thought_log_text'):\r\n                self.decisions_tab.thought_log_text.append(thought)\r\n                # Auto-scroll to bottom\r\n                scrollbar = self.decisions_tab.thought_log_text.verticalScrollBar()\r\n                scrollbar.setValue(scrollbar.maximum())\r\n            elif hasattr(self.decisions_tab, 'add_thought'):\r\n                # Alternative: if the tab has its own add_thought method\r\n                self.decisions_tab.add_thought(thought)\r\n        else:\r\n            # Fallback: print to console if no UI element available\r\n            print(f\"Thought: {thought}\")\r\n\r\n    def clear_thoughts(self):\r\n        self.thoughts_text.clear()\r\n\r\n    def init_decisions_tab(self):\r\n        font = QtGui.QFont()\r\n        font.setPointSize(self.base_font_size)\r\n        # Add a label for decision history\r\n        decision_history_label = QtWidgets.QLabel(\"Decision History:\")\r\n        self.decisions_tab_layout.addWidget(decision_history_label)\r\n\r\n        # Add a text area to display decision history\r\n        self.decision_history_text = QtWidgets.QTextEdit()\r\n        self.decision_history_text.setReadOnly(True)\r\n        self.decisions_tab_layout.addWidget(self.decision_history_text)\r\n\r\n        # Add a label for decision inputs\r\n        decision_inputs_label = QtWidgets.QLabel(\"Decision Inputs:\")\r\n        self.decisions_tab_layout.addWidget(decision_inputs_label)\r\n\r\n        # Add a text area to display decision inputs\r\n        self.decision_inputs_text = QtWidgets.QTextEdit()\r\n        self.decision_inputs_text.setReadOnly(True)\r\n        self.decisions_tab_layout.addWidget(self.decision_inputs_text)\r\n\r\n    def update_decisions_tab(self, decision, decision_inputs):\r\n        # Append the decision to the decision history\r\n        self.decision_history_text.append(f\"Decision: {decision}\")\r\n\r\n        # Display the decision inputs\r\n        self.decision_inputs_text.clear()\r\n        for key, value in decision_inputs.items():\r\n            self.decision_inputs_text.append(f\"{key}: {value}\")\r\n\r\n    def init_associations_tab(self):\r\n        font = QtGui.QFont()\r\n        font.setPointSize(self.base_font_size)\r\n        # Add a checkbox to toggle explanation\r\n        self.show_explanation_checkbox = QtWidgets.QCheckBox(\"Show Explanation\")\r\n        self.show_explanation_checkbox.stateChanged.connect(self.toggle_explanation)\r\n        self.associations_tab_layout.addWidget(self.show_explanation_checkbox)\r\n\r\n        # Add explanation text (hidden by default)\r\n        self.explanation_text = QtWidgets.QTextEdit()\r\n        self.explanation_text.setReadOnly(True)\r\n        self.explanation_text.setHidden(True)\r\n        self.explanation_text.setPlainText(\r\n            \"This tab shows the learned associations between different neural states of the squid. \"\r\n            \"These associations are formed through the Hebbian learning process, where 'neurons that fire together, wire together'. \"\r\n            \"The strength of an association is determined by how often these states occur together or influence each other. \"\r\n            \"Positive associations mean that as one state increases, the other tends to increase as well. \"\r\n            \"Negative associations (indicated by 'reduced') mean that as one state increases, the other tends to decrease. \"\r\n            \"These associations help us understand how the squid's experiences shape its behavior and decision-making processes.\"\r\n        )\r\n        self.associations_tab_layout.addWidget(self.explanation_text)\r\n\r\n        # Add a label for the associations\r\n        label = QtWidgets.QLabel(\"Learned associations:\")\r\n        self.associations_tab_layout.addWidget(label)\r\n\r\n        # Add a text area to display associations\r\n        self.associations_text = QtWidgets.QTextEdit()\r\n        self.associations_text.setReadOnly(True)\r\n        self.associations_tab_layout.addWidget(self.associations_text)\r\n\r\n        # Add export button\r\n        self.export_associations_button = QtWidgets.QPushButton(\"Export Associations\")\r\n        self.export_associations_button.clicked.connect(self.export_associations)\r\n        self.associations_tab_layout.addWidget(self.export_associations_button, alignment=QtCore.Qt.AlignRight)\r\n\r\n    def toggle_explanation(self, state):\r\n        self.explanation_text.setVisible(state == QtCore.Qt.Checked)\r\n\r\n    def update_associations(self):\r\n        \"\"\"Update association data if we have the learning tab\"\"\"\r\n        if hasattr(self, 'learning_tab'):\r\n            # Forward to the tab's update method\r\n            current_time = time.time()\r\n            if current_time - self.last_update_time > self.update_threshold:\r\n                self.last_update_time = current_time\r\n                self.learning_tab.update_from_brain_state(self.brain_widget.state)\r\n\r\n    def generate_association_summary(self, neuron1, neuron2, weight):\r\n        strength = \"strongly\" if abs(weight) > 0.8 else \"moderately\"\r\n        if weight > 0:\r\n            relation = \"associated with\"\r\n        else:\r\n            relation = \"associated with reduced\"\r\n\r\n        # Correct grammar for specific neurons\r\n        neuron1_text = self.get_neuron_display_name(neuron1)\r\n        neuron2_text = self.get_neuron_display_name(neuron2)\r\n\r\n        summaries = {\r\n            \"hunger-satisfaction\": f\"{neuron1_text} is {strength} associated with satisfaction (probably from eating)\",\r\n            \"satisfaction-hunger\": f\"Feeling satisfied is {strength} associated with reduced hunger\",\r\n            \"cleanliness-anxiety\": f\"{neuron1_text} is {strength} {relation} anxiety\",\r\n            \"anxiety-cleanliness\": f\"Feeling anxious is {strength} associated with reduced cleanliness\",\r\n            \"curiosity-happiness\": f\"{neuron1_text} is {strength} associated with happiness\",\r\n            \"happiness-curiosity\": f\"Being happy is {strength} associated with increased curiosity\",\r\n            \"hunger-anxiety\": f\"{neuron1_text} is {strength} associated with increased anxiety\",\r\n            \"sleepiness-satisfaction\": f\"{neuron1_text} is {strength} {relation} satisfaction\",\r\n            \"happiness-cleanliness\": f\"Being happy is {strength} associated with cleanliness\",\r\n        }\r\n\r\n        key = f\"{neuron1}-{neuron2}\"\r\n        if key in summaries:\r\n            return summaries[key]\r\n        else:\r\n            return f\"{neuron1_text} is {strength} {relation} {neuron2_text}\"\r\n\r\n    def get_neuron_display_name(self, neuron):\r\n        display_names = {\r\n            \"cleanliness\": \"Being clean\",\r\n            \"sleepiness\": \"Being sleepy\",\r\n            \"happiness\": \"Being happy\",\r\n            \"hunger\": \"Being hungry\",\r\n            \"satisfaction\": \"Satisfaction\",\r\n            \"anxiety\": \"Being anxious\",\r\n            \"curiosity\": \"Curiosity\",\r\n            \"direction\": \"Direction\"\r\n        }\r\n        return display_names.get(neuron, f\"{neuron}\")\r\n\r\n\r\n\r\n    def update_countdown(self):\r\n        \"\"\"Update the Hebbian learning countdown display\"\"\"\r\n        # Check if simulation is paused\r\n        is_paused = False\r\n        if hasattr(self, 'tamagotchi_logic') and hasattr(self.tamagotchi_logic, 'simulation_speed'):\r\n            is_paused = (self.tamagotchi_logic.simulation_speed == 0)\r\n        \r\n        # Calculate time until next learning cycle\r\n        if not is_paused and hasattr(self.brain_widget, 'last_hebbian_time'):\r\n            elapsed = time.time() - self.brain_widget.last_hebbian_time\r\n            interval_sec = self.config.hebbian.get('learning_interval', 30000) / 1000\r\n            remaining = max(0, interval_sec - elapsed)\r\n            self.brain_widget.hebbian_countdown_seconds = int(remaining)\r\n        elif not hasattr(self.brain_widget, 'last_hebbian_time'):\r\n            self.brain_widget.hebbian_countdown_seconds = 0\r\n\r\n        # Debug print statements\r\n        #print(f\"Elapsed time: {elapsed:.1f}s\")\r\n        #print(f\"Interval: {interval_sec:.1f}s\")\r\n        #print(f\"Remaining: {remaining:.1f}s\")\r\n        #print(f\"Hebbian countdown seconds: {self.brain_widget.hebbian_countdown_seconds}\")\r\n\r\n        # If we have the neural network visualizer tab initialized, update its countdown\r\n        if hasattr(self, 'nn_viz_tab') and hasattr(self.nn_viz_tab, 'countdown_label') and self.nn_viz_tab.countdown_label is not None:\r\n            # Update the formatted display\r\n            if is_paused:\r\n                self.nn_viz_tab.countdown_label.setText(\"PAUSED\")\r\n            else:\r\n                self.nn_viz_tab.countdown_label.setText(f\"{self.brain_widget.hebbian_countdown_seconds} seconds\")\r\n        \r\n        # If countdown reached zero and not paused, trigger learning\r\n        # (moved outside nn_viz_tab check so learning always fires)\r\n        if self.brain_widget.hebbian_countdown_seconds == 0 and not is_paused:\r\n            # print(\"brain_tool.py >> Countdown hit zero, calling perform_hebbian_learning()\")\r\n            self.brain_widget.perform_hebbian_learning()\r\n\r\n    def check_memory_decay(self):\r\n        \"\"\"Check for short-term memory decay and transfer important memories to long-term\"\"\"\r\n        if not hasattr(self.tamagotchi_logic, 'squid') or not self.tamagotchi_logic.squid:\r\n            return\r\n            \r\n        # Run periodic memory management on the squid's memory manager\r\n        if hasattr(self.tamagotchi_logic.squid, 'memory_manager'):\r\n            memory_manager = self.tamagotchi_logic.squid.memory_manager\r\n            \r\n            # Get all short-term memories to check for decay\r\n            short_term_memories = memory_manager.get_all_short_term_memories()\r\n            \r\n            # Process memories that are about to decay (older than 100 seconds)\r\n            current_time = datetime.now()\r\n            for memory in short_term_memories:\r\n                if 'timestamp' in memory:\r\n                    timestamp = memory['timestamp']\r\n                    if isinstance(timestamp, str):\r\n                        timestamp = datetime.fromisoformat(timestamp)\r\n                    \r\n                    time_diff = current_time - timestamp\r\n                    \r\n                    # If memory is influential or important, make sure it's transferred to long-term\r\n                    is_influential = False\r\n                    \r\n                    # Check if it's an important or influential memory\r\n                    if 'importance' in memory and memory['importance'] >= 7:\r\n                        is_influential = True\r\n                        \r\n                    # Check if it's about to decay but is important\r\n                    if time_diff.total_seconds() > 100:  # About to decay (120s is default)\r\n                        category = memory.get('category', '')\r\n                        key = memory.get('key', '')\r\n                        \r\n                        if is_influential:\r\n                            # Log the memory transfer\r\n                            self.activity_log.append(f\"\"\"\r\n                            <div style=\"background-color: #fff3cd; padding: 8px; margin: 5px; border-radius: 5px; border-left: 4px solid #ffc107;\">\r\n                                <span style=\"font-weight: bold;\">Important Memory Transfer</span><br>\r\n                                <span>Category: {category}</span><br>\r\n                                <span>Key: {key}</span><br>\r\n                                <span>Age: {int(time_diff.total_seconds())} seconds</span><br>\r\n                                <span>Importance: {memory.get('importance', 'unknown')}</span>\r\n                            </div>\r\n                            \"\"\")\r\n                            \r\n                            # Transfer to long-term memory\r\n                            memory_manager.transfer_to_long_term_memory(category, key)\r\n\r\n    def clear_learning_data(self):\r\n        self.weight_changes_text.clear()\r\n        self.learning_data_table.setRowCount(0)\r\n        self.learning_data = []\r\n        print(\"Learning data cleared.\")\r\n\r\n    def update_learning_interval(self, seconds):\r\n        \"\"\"Update the learning interval when spinbox value changes\"\"\"\r\n        # Convert seconds to milliseconds (QTimer uses ms)\r\n        interval_ms = seconds * 1000\r\n        \r\n        # Update config\r\n        if hasattr(self.config, 'hebbian'):\r\n            self.config.hebbian['learning_interval'] = interval_ms\r\n        else:\r\n            self.config.hebbian = {'learning_interval': interval_ms}\r\n        \r\n        # Restart timer with new interval\r\n        if hasattr(self, 'hebbian_timer'):\r\n            self.hebbian_timer.setInterval(interval_ms)\r\n            self.last_hebbian_time = time.time()  # Reset countdown\r\n        \r\n        if self.debug_mode:\r\n            print(f\"Learning interval updated to {seconds} seconds ({interval_ms} ms)\")\r\n\r\n    \r\n\r\n    def deduce_weight_change_reason(self, pair, value1, value2, prev_weight, new_weight, weight_change):\r\n        neuron1, neuron2 = pair\r\n        threshold_high = 70\r\n        threshold_low = 30\r\n\r\n        reasons = []\r\n\r\n        # Analyze neuron activity levels\r\n        if value1 > threshold_high and value2 > threshold_high:\r\n            reasons.append(f\"Both {neuron1.upper()} and {neuron2.upper()} were highly active\")\r\n        elif value1 < threshold_low and value2 < threshold_low:\r\n            reasons.append(f\"Both {neuron1.upper()} and {neuron2.upper()} had low activity\")\r\n        elif value1 > threshold_high:\r\n            reasons.append(f\"{neuron1.upper()} was highly active\")\r\n        elif value2 > threshold_high:\r\n            reasons.append(f\"{neuron2.upper()} was highly active\")\r\n\r\n        # Analyze weight change\r\n        if abs(weight_change) > 0.1:\r\n            if weight_change > 0:\r\n                reasons.append(\"Strong positive reinforcement\")\r\n            else:\r\n                reasons.append(\"Strong negative reinforcement\")\r\n        elif abs(weight_change) > 0.01:\r\n            if weight_change > 0:\r\n                reasons.append(\"Moderate positive reinforcement\")\r\n            else:\r\n                reasons.append(\"Moderate negative reinforcement\")\r\n        else:\r\n            reasons.append(\"Weak reinforcement\")\r\n\r\n        # Analyze the relationship between neurons\r\n        if \"hunger\" in pair and \"satisfaction\" in pair:\r\n            reasons.append(\"Potential hunger-satisfaction relationship\")\r\n        elif \"cleanliness\" in pair and \"happiness\" in pair:\r\n            reasons.append(\"Potential cleanliness-happiness relationship\")\r\n\r\n        # Analyze the current weight\r\n        if abs(new_weight) > 0.8:\r\n            reasons.append(\"Strong connection formed\")\r\n        elif abs(new_weight) < 0.2:\r\n            reasons.append(\"Weak connection\")\r\n\r\n        # Analyze learning progress\r\n        if abs(prev_weight) < 0.1 and abs(new_weight) > 0.1:\r\n            reasons.append(\"New significant connection emerging\")\r\n        elif abs(prev_weight) > 0.8 and abs(new_weight) < 0.8:\r\n            reasons.append(\"Previously strong connection weakening\")\r\n\r\n        # Combine reasons\r\n        if len(reasons) > 1:\r\n            return \" | \".join(reasons)\r\n        elif len(reasons) == 1:\r\n            return reasons[0]\r\n        else:\r\n            return \"Complex interaction with no clear single reason\"\r\n\r\n\r\n    def get_neuron_value(self, value):\r\n        if isinstance(value, (int, float)):\r\n            return float(value)\r\n        elif isinstance(value, bool):\r\n            return 100.0 if value else 0.0\r\n        elif isinstance(value, str):\r\n            # For string values (like 'direction'), return a default value\r\n            return 75.0\r\n        else:\r\n            return 0.0\r\n\r\n    def update_learning_data_table(self):\r\n        self.learning_data_table.setRowCount(len(self.learning_data))\r\n        for row, data in enumerate(self.learning_data):\r\n            for col, value in enumerate(data):\r\n                item = QtWidgets.QTableWidgetItem(str(value))\r\n                if col == 3:  # Weight change column\r\n                    item.setData(QtCore.Qt.DisplayRole, f\"{value:.4f}\")\r\n                if col == 4:  # Direction column\r\n                    if value == \"increase ⬆️\":\r\n                        item.setForeground(QtGui.QColor(\"green\"))\r\n                    elif value == \"⬇️ decrease\":\r\n                        item.setForeground(QtGui.QColor(\"red\"))\r\n                self.learning_data_table.setItem(row, col, item)\r\n        self.learning_data_table.scrollToBottom()\r\n\r\n    def export_learning_data(self):\r\n        # Save the weight changes text to a file\r\n        with open(\"learningdata_reasons.txt\", 'w') as file:\r\n            file.write(self.weight_changes_text.toPlainText())\r\n\r\n        # Save the learning data table to a CSV file\r\n        with open(\"learningdata_weights.csv\", 'w', newline='') as file:\r\n            writer = csv.writer(file)\r\n            writer.writerow([\"Timestamp\", \"Neuron 1\", \"Neuron 2\", \"Weight Change\", \"Direction\"])\r\n            for row in range(self.learning_data_table.rowCount()):\r\n                row_data = []\r\n                for col in range(self.learning_data_table.columnCount()):\r\n                    item = self.learning_data_table.item(row, col)\r\n                    row_data.append(item.text() if item else \"\")\r\n                writer.writerow(row_data)\r\n\r\n        QtWidgets.QMessageBox.information(self, \"Export Successful\", \"Learning data exported to 'weight_changes.txt' and 'learning_data.csv'\")\r\n\r\n    def export_learning_tab_contents(self):\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Export Learning Tab Contents\", \"\", \"Text Files (*.txt)\")\r\n        if file_name:\r\n            with open(file_name, 'w') as file:\r\n                file.write(\"Learning Data Table:\\n\")\r\n                for row in range(self.learning_data_table.rowCount()):\r\n                    row_data = []\r\n                    for col in range(self.learning_data_table.columnCount()):\r\n                        item = self.learning_data_table.item(row, col)\r\n                        row_data.append(item.text() if item else \"\")\r\n                    file.write(\"\\t\".join(row_data) + \"\\n\")\r\n\r\n                file.write(\"\\nWeight Changes Text:\\n\")\r\n                file.write(self.weight_changes_text.toPlainText())\r\n\r\n            QtWidgets.QMessageBox.information(self, \"Export Successful\", f\"Learning tab contents exported to {file_name}\")\r\n\r\n    def export_associations(self):\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Export Associations\", \"\", \"Text Files (*.txt)\")\r\n        if file_name:\r\n            with open(file_name, 'w') as file:\r\n                file.write(self.associations_text.toPlainText())\r\n            QtWidgets.QMessageBox.information(self, \"Export Successful\", f\"Associations exported to {file_name}\")\r\n\r\n    def update_personality_effects(self, personality, weights, adjusted_weights):\r\n        \"\"\"Update the personality modifier display in the thinking tab\"\"\"\r\n        # Convert enum to string if needed\r\n        personality_str = getattr(personality, 'value', str(personality))\r\n        \r\n        self.personality_label.setText(f\"Personality: {personality_str.capitalize()}\")\r\n        \r\n        # Generate effect text based on weight differences\r\n        effects_text = []\r\n        for action, weight in weights.items():\r\n            adjusted = adjusted_weights.get(action, weight)\r\n            if abs(adjusted - weight) > 0.01:  # If there's a significant difference\r\n                direction = \"increases\" if adjusted > weight else \"decreases\"\r\n                effect = f\"{action}: {direction} by {abs(adjusted - weight):.2f}\"\r\n                effects_text.append(effect)\r\n        \r\n        if effects_text:\r\n            self.personality_effects.setPlainText(\"\\n\".join(effects_text))\r\n        else:\r\n            self.personality_effects.setPlainText(\"No significant personality effects\")\r\n\r\n    def update_brain(self, state):\r\n        \"\"\"Main update method to distribute state changes to all tabs\"\"\"\r\n        if not self.initialized:\r\n            self.initialized = True\r\n            return  # Skip first update\r\n\r\n        # Update the brain widget first\r\n        self.brain_widget.update_state(state)\r\n\r\n        # Forward updates to each tab that has an update method\r\n        tabs_to_update = ['network_tab', 'nn_viz_tab', 'memory_tab', 'decisions_tab', 'personality_tab', 'about_tab'] # Adjusted list\r\n        for tab_name in tabs_to_update:\r\n            if hasattr(self, tab_name):\r\n                tab = getattr(self, tab_name)\r\n                if hasattr(tab, 'update_from_brain_state'):\r\n                    tab.update_from_brain_state(state)\r\n\r\n\r\n    def train_hebbian(self):\r\n        self.brain_widget.train_hebbian()\r\n        #self.update_data_table(self.brain_widget.state)\r\n        self.update_training_data_table()\r\n\r\n        # Switch to the Console tab\r\n        self.tabs.setCurrentWidget(self.console_tab)\r\n\r\n        # Print training results to the console\r\n        print(\"Hebbian training completed.\")\r\n        print(\"Updated association strengths:\")\r\n        for i, neuron1 in enumerate(self.brain_widget.neuron_positions.keys()):\r\n            for j, neuron2 in enumerate(self.brain_widget.neuron_positions.keys()):\r\n                if i < j:\r\n                    strength = self.brain_widget.get_association_strength(neuron1, neuron2)\r\n                    print(f\"{neuron1} - {neuron2}: {strength:.2f}\")\r\n\r\n    def init_training_data_tab(self):\r\n        self.show_overview_checkbox = QtWidgets.QCheckBox(\"Show Training Process Overview\")\r\n        self.show_overview_checkbox.stateChanged.connect(self.toggle_overview)\r\n        self.training_data_tab_layout.addWidget(self.show_overview_checkbox)\r\n\r\n        self.overview_label = QtWidgets.QLabel(\r\n            \"Training Process Overview:\\n\\n\"\r\n            \"1. Data Capture: When 'Capture training data' is checked, the current state of all neurons is recorded each time the brain is stimulated.\\n\\n\"\r\n            \"2. Hebbian Learning: The 'Train Hebbian' button applies the Hebbian learning rule to the captured data.\\n\\n\"\r\n            \"3. Association Strength: The learning process strengthens connections between neurons that are frequently active together.\\n\\n\"\r\n            \"4. Weight Updates: After training, the weights between neurons are updated based on their co-activation patterns.\\n\\n\"\r\n            \"5. Adaptive Behavior: Over time, this process allows the brain to adapt its behavior based on input patterns.\"\r\n        )\r\n        self.overview_label.setWordWrap(True)\r\n        self.overview_label.hide()  # Hide by default\r\n        self.training_data_tab_layout.addWidget(self.overview_label)\r\n\r\n        self.training_data_table = QtWidgets.QTableWidget()\r\n        self.training_data_tab_layout.addWidget(self.training_data_table)\r\n\r\n        self.training_data_table.setColumnCount(len(self.brain_widget.neuron_positions))\r\n        self.training_data_table.setHorizontalHeaderLabels(list(self.brain_widget.neuron_positions.keys()))\r\n\r\n        self.training_data_timer = QtCore.QTimer()\r\n        self.training_data_timer.timeout.connect(self.update_training_data_table)\r\n        self.training_data_timer.start(1000)  # Update every second\r\n\r\n        self.checkbox_capture_training_data = QtWidgets.QCheckBox(\"Capture training data\")\r\n        self.checkbox_capture_training_data.stateChanged.connect(self.toggle_capture_training_data)\r\n        self.training_data_tab_layout.addWidget(self.checkbox_capture_training_data)\r\n\r\n        self.train_button = self.create_button(\"Train Hebbian\", self.train_hebbian, \"#ADD8E6\")\r\n        self.train_button.setEnabled(False)  # Initially grey out the train button\r\n        self.training_data_tab_layout.addWidget(self.train_button)\r\n\r\n    def toggle_overview(self, state):\r\n        if state == QtCore.Qt.Checked:\r\n            self.overview_label.show()\r\n        else:\r\n            self.overview_label.hide()\r\n\r\n    def toggle_capture_training_data(self, state):\r\n        self.brain_widget.toggle_capture_training_data(state)\r\n        if state == QtCore.Qt.Checked:\r\n            os.makedirs('training_data', exist_ok=True)\r\n\r\n    def update_training_data_table(self):\r\n        self.training_data_table.setRowCount(len(self.brain_widget.training_data))\r\n        for row, sample in enumerate(self.brain_widget.training_data):\r\n            for col, value in enumerate(sample):\r\n                self.training_data_table.setItem(row, col, QtWidgets.QTableWidgetItem(str(value)))\r\n\r\n        if len(self.brain_widget.training_data) > 0:\r\n            self.train_button.setEnabled(True)\r\n\r\n        # Save raw data to file\r\n        if self.checkbox_capture_training_data.isChecked():\r\n            with open(os.path.join('training_data', 'raw_data.json'), 'w') as f:\r\n                json.dump(self.brain_widget.training_data, f)\r\n\r\n    def save_brain_state(self):\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, \"Save Brain State\", \"\", \"JSON Files (*.json)\")\r\n        if file_name:\r\n            with open(file_name, 'w') as f:\r\n                json.dump(self.brain_widget.state, f)\r\n\r\n    def load_brain_state(self):\r\n        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, \"Load Brain State\", \"\", \"JSON Files (*.json)\")\r\n        if file_name:\r\n            with open(file_name, 'r') as f:\r\n                state = json.load(f)\r\n            self.brain_widget.update_state(state)\r\n\r\n    def export_brain_weights_text(self):\r\n        \"\"\"Return a human-readable block with every connection.\"\"\"\r\n        lines = [\"# Squid brain weights – saved {}\\n\".format(\r\n                    QtCore.QDateTime.currentDateTime().toString())]\r\n        for (src, dst), w in sorted(self.brain_widget.weights.items(),\r\n                                    key=lambda kv: abs(kv[1]), reverse=True):\r\n            lines.append(f\"{src:20} → {dst:20}  {w:+.4f}\")\r\n        return \"\\n\".join(lines)\r\n\r\n    def export_hebbian_json(self):\r\n        \"\"\"Return a JSON-serialisable list with the *entire* learning log.\"\"\"\r\n        # learning_data is already a list of lists:\r\n        # [timestamp, n1, n2, Δw, direction]\r\n        return {\r\n            \"saved_at\": time.time(),\r\n            \"learning_interval_ms\": self.config.hebbian.get('learning_interval', 30000),\r\n            \"history\": getattr(self.brain_widget, 'learning_data', [])\r\n        }\r\n    \r\n    def export_decision_engine_json(self):\r\n        \"\"\"Ask the DecisionEngine to serialise itself.\"\"\"\r\n        if not hasattr(self.tamagotchi_logic, 'squid') or \\\r\n           not hasattr(self.tamagotchi_logic.squid, 'decision_engine'):\r\n            return {}\r\n        engine = self.tamagotchi_logic.squid.decision_engine\r\n        # Minimal example – extend as needed\r\n        return {\r\n            \"epsilon\": getattr(engine, 'epsilon', 0.1),\r\n            \"learning_rate\": getattr(engine, 'learning_rate', 0.05),\r\n            \"q_table\": getattr(engine, 'q_table', {}).copy()\r\n        }\r\n\r\n    def init_console(self):\r\n        self.console_output = QtWidgets.QTextEdit()\r\n        self.console_output.setReadOnly(True)\r\n        self.console_tab_layout.addWidget(self.console_output)\r\n        self.console = ConsoleOutput(self.console_output)\r\n\r\n    def create_button(self, text, callback, color):\r\n        button = QtWidgets.QPushButton(text)\r\n        button.clicked.connect(callback)\r\n        button.setStyleSheet(f\"background-color: {color}; border: 1px solid black; padding: 5px;\")\r\n        button.setFixedSize(200, 50)\r\n        return button\r\n\r\n    def stimulate_brain(self):\r\n        dialog = StimulateDialog(self)\r\n        if dialog.exec_() == QtWidgets.QDialog.Accepted:\r\n            stimulation_values = dialog.get_stimulation_values()\r\n            if stimulation_values is not None:\r\n                self.brain_widget.update_state(stimulation_values)\r\n                if self.tamagotchi_logic:\r\n                    self.tamagotchi_logic.update_from_brain(stimulation_values)\r\n                else:\r\n                    print(\"Warning: tamagotchi_logic is not set. Brain stimulation will not affect the squid.\")\r\n\r\n\r\n    def update_neural_visualization(self, inputs):\r\n        \"\"\"Update the neural network visualization with current input values\"\"\"\r\n        if not hasattr(self, 'neuron_items') or not self.neuron_items:\r\n            self.setup_neural_visualization()\r\n            return\r\n        \r\n        # Update neuron colors based on activation values\r\n        for neuron, value in inputs.items():\r\n            if neuron in self.neuron_items:\r\n                # Only update numerical values\r\n                if isinstance(value, (int, float)):\r\n                    # Update stored value\r\n                    self.neuron_items[neuron][\"value\"] = value\r\n                    \r\n                    # Calculate color based on value (0-100)\r\n                    intensity = int(value * 2.55)  # Scale 0-100 to 0-255\r\n                    \r\n                    if neuron in [\"hunger\", \"sleepiness\", \"anxiety\"]:\r\n                        # Red-based for \"negative\" neurons (more red = higher activation)\r\n                        color = QtGui.QColor(255, 255 - intensity, 255 - intensity)\r\n                    else:\r\n                        # Blue/green-based for \"positive\" neurons (more color = higher activation)\r\n                        color = QtGui.QColor(100, intensity, 255)\r\n                    \r\n                    # Update neuron ellipse color\r\n                    self.neuron_items[neuron][\"shape\"].setBrush(QtGui.QBrush(color))\r\n                    \r\n                    # Make neurons pulse slightly based on value\r\n                    scale = 1.0 + (value / 200)  # 1.0 to 1.5\r\n                    rect = self.neuron_items[neuron][\"shape\"].rect()\r\n                    center_x = rect.x() + rect.width()/2\r\n                    center_y = rect.y() + rect.height()/2\r\n                    new_width = 40 * scale\r\n                    new_height = 40 * scale\r\n                    self.neuron_items[neuron][\"shape\"].setRect(\r\n                        center_x - new_width/2,\r\n                        center_y - new_height/2,\r\n                        new_width,\r\n                        new_height\r\n                    )\r\n        \r\n        # Update connection line widths and colors based on neuron activations\r\n        for connection, items in self.connection_items.items():\r\n            source, target = connection\r\n            source_value = self.neuron_items.get(source, {}).get(\"value\", 50)\r\n            target_value = self.neuron_items.get(target, {}).get(\"value\", 50)\r\n            \r\n            # Calculate connection strength based on both neuron activations\r\n            # Higher when both neurons are highly activated\r\n            connection_strength = (source_value * target_value) / 10000  # Scale to 0-1\r\n            \r\n            # Update line width and color\r\n            pen_width = 1 + 3 * connection_strength\r\n            \r\n            # Get current brain connection weight if available\r\n            weight = items.get(\"weight\", 0)\r\n            \r\n            # Color based on weight (green for positive, red for negative)\r\n            if weight > 0:\r\n                pen_color = QtGui.QColor(0, 150, 0, 50 + int(200 * connection_strength))\r\n            else:\r\n                pen_color = QtGui.QColor(150, 0, 0, 50 + int(200 * connection_strength))\r\n            \r\n            items[\"line\"].setPen(QtGui.QPen(pen_color, pen_width))\r\n            \r\n            # Update the weight display\r\n            items[\"text\"].setPlainText(f\"{weight:.1f}\")\r\n\r\n\r\n    def update_brain_weights(self, weights_data):\r\n        \"\"\"Update the brain connection weights based on current neural network weights\"\"\"\r\n        if not hasattr(self, 'connection_items'):\r\n            return\r\n            \r\n        # Update connection weights\r\n        for (src, dst), weight in weights_data.items():\r\n            # Look for the connection in either direction\r\n            connection = (src, dst)\r\n            if connection in self.connection_items:\r\n                self.connection_items[connection][\"weight\"] = weight\r\n            else:\r\n                # Try the reverse connection\r\n                connection = (dst, src)\r\n                if connection in self.connection_items:\r\n                    self.connection_items[connection][\"weight\"] = weight\r\n                    \r\n\r\n    def animate_decision_process(self, decision_data):\r\n        \"\"\"Animate the decision-making process with visual effects\"\"\"\r\n        if not hasattr(self, 'processing_animation'):\r\n            return\r\n            \r\n        # Get the decision information\r\n        decision = decision_data.get('final_decision', 'unknown')\r\n        processing_time = decision_data.get('processing_time', 1000)\r\n        \r\n        # Display processing text\r\n        self.processing_text.setText(f\"Processing decision ({processing_time}ms)...\")\r\n        \r\n        # Start the animation with a brief delay to show processing\r\n        QtCore.QTimer.singleShot(300, lambda: self.highlight_decision_in_ui(decision))\r\n\r\n\r\n    def highlight_decision_in_ui(self, decision):\r\n        \"\"\"Highlight the chosen decision in the UI\"\"\"\r\n        # Update decision output with animation effect\r\n        self.decision_output.setText(decision.capitalize())\r\n        \r\n        # Flash the decision with a highlight animation\r\n        original_style = self.decision_output.styleSheet()\r\n        self.decision_output.setStyleSheet(\"font-size: 18px; font-weight: bold; color: white; background-color: green; padding: 5px; border-radius: 5px;\")\r\n        \r\n        # Reset after brief highlight\r\n        QtCore.QTimer.singleShot(500, lambda: self.decision_output.setStyleSheet(original_style))\r\n        \r\n        # Update processing text\r\n        self.processing_text.setText(f\"Decision made: {decision.capitalize()}\")\r\n\r\n    def update_learning_status(self, is_active):\r\n        \"\"\"Update the learning status indicator\"\"\"\r\n        if is_active:\r\n            self.learning_status.setText(\"Learning Status: Active\")\r\n            self.learning_status.setStyleSheet(\"\"\"\r\n                font-size: 14px;\r\n                padding: 5px;\r\n                background-color: #d4edda;\r\n                border-radius: 4px;\r\n                border: 1px solid #c3e6cb;\r\n                color: #155724;\r\n                font-weight: bold;\r\n            \"\"\")\r\n        else:\r\n            self.learning_status.setText(\"Learning Status: Inactive\")\r\n            self.learning_status.setStyleSheet(\"\"\"\r\n                font-size: 14px;\r\n                padding: 5px;\r\n                background-color: #f8f9fa;\r\n                border-radius: 4px;\r\n                border: 1px solid #e9ecef;\r\n                color: #495057;\r\n            \"\"\")\r\n\r\n    def update_learning_interval(self, seconds):\r\n        \"\"\"Update the learning interval when spinbox value changes\"\"\"\r\n        # Convert seconds to milliseconds (QTimer uses ms)\r\n        interval_ms = seconds * 1000\r\n        \r\n        # Update config\r\n        if hasattr(self.config, 'hebbian'):\r\n            self.config.hebbian['learning_interval'] = interval_ms\r\n        else:\r\n            self.config.hebbian = {'learning_interval': interval_ms}\r\n        \r\n        # Restart timer with new interval\r\n        if hasattr(self, 'hebbian_timer'):\r\n            self.hebbian_timer.setInterval(interval_ms)\r\n            self.brain_widget.last_hebbian_time = time.time()  # Reset countdown\r\n        \r\n        # Log the change\r\n        self.activity_log.append(f\"\"\"\r\n        <div style=\"background-color: #e8f4f8; padding: 8px; margin: 5px; border-radius: 5px;\">\r\n            <span style=\"font-weight: bold;\">Learning interval updated</span><br>\r\n            New interval: {seconds} seconds ({interval_ms} ms)\r\n        </div>\r\n        \"\"\")\r\n        \r\n        # Force update of countdown\r\n        self.update_countdown()\r\n\r\n    def trigger_learning_cycle(self):\r\n        \"\"\"Force an immediate Hebbian learning cycle\"\"\"\r\n        if hasattr(self.brain_widget, 'perform_hebbian_learning'):\r\n            # Record the current state for before/after comparison\r\n            old_weights = {k: v for k, v in self.brain_widget.weights.items()}\r\n            \r\n            # Perform the learning\r\n            self.brain_widget.perform_hebbian_learning()\r\n            \r\n            # Find changed weights\r\n            changes = []\r\n            for k, v in self.brain_widget.weights.items():\r\n                if k in old_weights and abs(v - old_weights[k]) > 0.001:\r\n                    changes.append((k, old_weights[k], v))\r\n            \r\n            # Log the forced learning event\r\n            log_html = f\"\"\"\r\n            <div style=\"background-color: #d4edda; padding: 10px; margin: 8px; border-radius: 5px; border-left: 4px solid #28a745;\">\r\n                <span style=\"font-weight: bold; font-size: 14px;\">Manual learning cycle triggered</span><br>\r\n                <span style=\"color: #555;\">Time: {time.strftime('%H:%M:%S')}</span><br>\r\n                <span>Changes detected: {len(changes)}</span>\r\n            \"\"\"\r\n            \r\n            if changes:\r\n                log_html += \"<ul style='margin-top: 5px;'>\"\r\n                for (source, target), old_val, new_val in changes[:5]:  # Show top 5 changes\r\n                    direction = \"+\" if new_val > old_val else \"\"\r\n                    log_html += f\"\"\"\r\n                    <li>\r\n                        <span style=\"font-weight: bold;\">{source} → {target}</span>: \r\n                        <span style=\"color: #777;\">{old_val:.3f}</span> → \r\n                        <span style=\"color: {'green' if new_val > old_val else 'red'}; font-weight: bold;\">\r\n                            {new_val:.3f} ({direction}{new_val - old_val:.3f})\r\n                        </span>\r\n                    </li>\r\n                    \"\"\"\r\n                if len(changes) > 5:\r\n                    log_html += f\"<li>...and {len(changes) - 5} more changes</li>\"\r\n                log_html += \"</ul>\"\r\n            else:\r\n                log_html += \"<br><span style='font-style: italic;'>No significant weight changes detected</span>\"\r\n                \r\n            log_html += \"</div>\"\r\n            self.activity_log.append(log_html)\r\n            \r\n            # Update the connection table and heatmap\r\n            self.update_connection_table()\r\n            self.update_heatmap()\r\n            self.update_learning_statistics()\r\n\r\n    def update_connection_table(self):\r\n        \"\"\"Update the connection table with current weights\"\"\"\r\n        self.connections_view.setRowCount(0)  # Clear existing rows\r\n        \r\n        # Get all weights\r\n        weights = self.brain_widget.weights\r\n        if not weights:\r\n            return\r\n        \r\n        # Get blacklisted neurons to exclude\r\n        excluded_neurons = getattr(self.brain_widget, 'excluded_neurons', ['is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction'])\r\n        \r\n        # Apply current filter\r\n        filter_text = self.connection_search.text().lower()\r\n        filter_type = self.connection_filter.currentText()\r\n        \r\n        # Add rows to table\r\n        row = 0\r\n        for (source, target), weight in sorted(weights.items(), key=lambda x: abs(x[1]), reverse=True):\r\n            # Skip connections involving blacklisted neurons\r\n            if source in excluded_neurons or target in excluded_neurons:\r\n                continue\r\n                \r\n            # Apply filters\r\n            if filter_type == \"Strong Positive\" and weight <= 0.5:\r\n                continue\r\n            elif filter_type == \"Strong Negative\" and weight >= -0.5:\r\n                continue\r\n            elif filter_type == \"Weak Connections\" and abs(weight) > 0.3:\r\n                continue\r\n            elif filter_type == \"New Connections\":\r\n                # Check if either neuron is new\r\n                if (source not in self.brain_widget.neurogenesis_data.get('new_neurons', []) and\r\n                    target not in self.brain_widget.neurogenesis_data.get('new_neurons', [])):\r\n                    continue\r\n            \r\n            # Apply text search\r\n            if filter_text and not (filter_text in source.lower() or filter_text in target.lower()):\r\n                continue\r\n                \r\n            # Add the row\r\n            self.connections_view.insertRow(row)\r\n            \r\n            # Source neuron\r\n            source_item = QtWidgets.QTableWidgetItem(source)\r\n            if source in self.brain_widget.neurogenesis_data.get('new_neurons', []):\r\n                source_item.setBackground(QtGui.QColor(255, 255, 200))  # Light yellow for new neurons\r\n            source_item.setFont(QtGui.QFont(\"Arial\", 14))  # Bigger font size\r\n            self.connections_view.setItem(row, 0, source_item)\r\n            \r\n            # Target neuron\r\n            target_item = QtWidgets.QTableWidgetItem(target)\r\n            if target in self.brain_widget.neurogenesis_data.get('new_neurons', []):\r\n                target_item.setBackground(QtGui.QColor(255, 255, 200))\r\n            target_item.setFont(QtGui.QFont(\"Arial\", 14))  # Bigger font size\r\n            self.connections_view.setItem(row, 1, target_item)\r\n            \r\n            # Weight value\r\n            weight_item = QtWidgets.QTableWidgetItem(f\"{weight:.3f}\")\r\n            if weight > 0.5:\r\n                weight_item.setForeground(QtGui.QColor(0, 150, 0))  # Green for strong positive\r\n            elif weight > 0:\r\n                weight_item.setForeground(QtGui.QColor(0, 100, 0))  # Dark green for mild positive\r\n            elif weight > -0.5:\r\n                weight_item.setForeground(QtGui.QColor(150, 0, 0))  # Dark red for mild negative\r\n            else:\r\n                weight_item.setForeground(QtGui.QColor(200, 0, 0))  # Bright red for strong negative\r\n            weight_item.setFont(QtGui.QFont(\"Arial\", 14))  # Bigger font size\r\n            self.connections_view.setItem(row, 2, weight_item)\r\n            \r\n            # Trend indicator with emoji arrows\r\n            trend_item = QtWidgets.QTableWidgetItem(\"—\")\r\n            if hasattr(self, '_prev_weights') and (source, target) in self._prev_weights:\r\n                prev = self._prev_weights.get((source, target), 0)\r\n                if weight > prev + 0.01:\r\n                    trend_item = QtWidgets.QTableWidgetItem(\"⬆️\")  # Up emoji arrow\r\n                elif weight < prev - 0.01:\r\n                    trend_item = QtWidgets.QTableWidgetItem(\"⬇️\")  # Down emoji arrow\r\n            trend_item.setFont(QtGui.QFont(\"Arial\", 14))  # Bigger font size\r\n            self.connections_view.setItem(row, 3, trend_item)\r\n            \r\n            row += 1\r\n        \r\n        # Store current weights for future trend comparison\r\n        if not hasattr(self, '_prev_weights'):\r\n            self._prev_weights = {}\r\n        self._prev_weights = weights.copy()\r\n\r\n    def filter_connections(self):\r\n        \"\"\"Apply the current filters to the connection table\"\"\"\r\n        self.update_connection_table()\r\n\r\n    def show_connection_details(self):\r\n        \"\"\"Show details for the selected connection\"\"\"\r\n        selected_items = self.connections_view.selectedItems()\r\n        if not selected_items:\r\n            self.connection_details.clear()\r\n            return\r\n        \r\n        # Get the row (assumes single row selection)\r\n        row = selected_items[0].row()\r\n        \r\n        # Get values from the row\r\n        source = self.connections_view.item(row, 0).text()\r\n        target = self.connections_view.item(row, 1).text()\r\n        weight = float(self.connections_view.item(row, 2).text())\r\n        \r\n        # Generate detailed HTML content\r\n        details_html = f\"\"\"\r\n        <div style=\"font-family: Arial, sans-serif;\">\r\n            <h3 style=\"margin: 5px 0; color: #2c3e50;\">Connection Details</h3>\r\n            \r\n            <table style=\"width: 100%; border-collapse: collapse; margin-top: 10px;\">\r\n                <tr style=\"background-color: #f8f9fa;\">\r\n                    <td style=\"padding: 5px; font-weight: bold;\">Source Neuron:</td>\r\n                    <td style=\"padding: 5px;\">{source}</td>\r\n                </tr>\r\n                <tr>\r\n                    <td style=\"padding: 5px; font-weight: bold;\">Target Neuron:</td>\r\n                    <td style=\"padding: 5px;\">{target}</td>\r\n                </tr>\r\n                <tr style=\"background-color: #f8f9fa;\">\r\n                    <td style=\"padding: 5px; font-weight: bold;\">Connection Weight:</td>\r\n                    <td style=\"padding: 5px; font-weight: bold; color: {'green' if weight > 0 else 'red'};\">\r\n                        {weight:.4f}\r\n                    </td>\r\n                </tr>\r\n                <tr>\r\n                    <td style=\"padding: 5px; font-weight: bold;\">Connection Strength:</td>\r\n                    <td style=\"padding: 5px;\">\r\n        \"\"\"\r\n        \r\n        # Add strength description\r\n        if abs(weight) > 0.8:\r\n            details_html += \"<span style='color: #2980b9; font-weight: bold;'>Very Strong</span>\"\r\n        elif abs(weight) > 0.5:\r\n            details_html += \"<span style='color: #3498db; font-weight: bold;'>Strong</span>\"\r\n        elif abs(weight) > 0.3:\r\n            details_html += \"<span style='color: #7f8c8d;'>Moderate</span>\"\r\n        elif abs(weight) > 0.1:\r\n            details_html += \"<span style='color: #95a5a6;'>Weak</span>\"\r\n        else:\r\n            details_html += \"<span style='color: #bdc3c7;'>Very Weak</span>\"\r\n            \r\n        details_html += \"\"\"\r\n                    </td>\r\n                </tr>\r\n            </table>\r\n            \r\n            <div style=\"margin-top: 15px; font-weight: bold;\">Interpretation:</div>\r\n        \"\"\"\r\n        \r\n        # Add interpretation based on the connection\r\n        if weight > 0:\r\n            details_html += f\"\"\"\r\n            <p style=\"margin: 5px 0;\">This is a <span style=\"color: green;\">positive connection</span>. When <b>{source}</b> is active, it will tend to increase the activity of <b>{target}</b>.</p>\r\n            \"\"\"\r\n        else:\r\n            details_html += f\"\"\"\r\n            <p style=\"margin: 5px 0;\">This is an <span style=\"color: red;\">inhibitory connection</span>. When <b>{source}</b> is active, it will tend to decrease the activity of <b>{target}</b>.</p>\r\n            \"\"\"\r\n        \r\n        # Check if either neuron is from neurogenesis\r\n        if source in self.brain_widget.neurogenesis_data.get('new_neurons', []) or target in self.brain_widget.neurogenesis_data.get('new_neurons', []):\r\n            details_html += \"\"\"\r\n            <div style=\"margin-top: 10px; background-color: #fff9c4; padding: 8px; border-radius: 4px;\">\r\n                <b>Note:</b> This connection involves a neuron created through neurogenesis!\r\n            </div>\r\n            \"\"\"\r\n        \r\n        details_html += \"</div>\"\r\n        \r\n        # Update the details widget\r\n        self.connection_details.setHtml(details_html)\r\n\r\n    def apply_neurogenesis_settings(self):\r\n        \"\"\"Apply changes to neurogenesis settings\"\"\"\r\n        # Validate and update the neurogenesis configuration\r\n        if not hasattr(self.config, 'neurogenesis'):\r\n            self.config.neurogenesis = {}\r\n        \r\n        # Get current values from UI\r\n        self.config.neurogenesis['novelty_threshold'] = self.novelty_threshold.value()\r\n        self.config.neurogenesis['stress_threshold'] = self.stress_threshold.value()\r\n        self.config.neurogenesis['reward_threshold'] = self.reward_threshold.value()\r\n        self.config.neurogenesis['cooldown'] = self.cooldown_spinbox.value()\r\n        \r\n        # Log the changes\r\n        self.activity_log.append(f\"\"\"\r\n        <div style=\"background-color: #d4edda; padding: 10px; margin: 8px; border-radius: 5px; border-left: 4px solid #4285f4;\">\r\n            <span style=\"font-weight: bold; font-size: 14px;\">Neurogenesis Settings Updated</span><br>\r\n            <span style=\"color: #555;\">Time: {time.strftime('%H:%M:%S')}</span><br>\r\n            <ul style=\"margin-top: 5px;\">\r\n                <li>Novelty Threshold: {self.config.neurogenesis['novelty_threshold']}</li>\r\n                <li>Stress Threshold: {self.config.neurogenesis['stress_threshold']}</li>\r\n                <li>Reward Threshold: {self.config.neurogenesis['reward_threshold']}</li>\r\n                <li>Cooldown Period: {self.config.neurogenesis['cooldown']} seconds</li>\r\n            </ul>\r\n        </div>\r\n        \"\"\")\r\n        \r\n        # Update the status display\r\n        self.update_neurogenesis_status()\r\n        \r\n        # Show confirmation message\r\n        QtWidgets.QMessageBox.information(\r\n            self, \"Settings Applied\", \r\n            \"Neurogenesis settings have been updated successfully.\"\r\n        )\r\n\r\n    def trigger_neurogenesis(self):\r\n        \"\"\"Trigger neurogenesis by boosting natural trigger values\"\"\"\r\n        try:\r\n            if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\r\n                self.show_message(\"Brain window not initialized\")\r\n                print(\"Error: Brain window not initialized\")\r\n                return\r\n                \r\n            brain = self.squid_brain_window.brain_widget\r\n            print(\"Brain widget found\")\r\n            \r\n            # Get current neuron count to verify success\r\n            old_neurons = set(brain.neuron_positions.keys())\r\n            print(f\"Current neurons: {len(old_neurons)}\")\r\n            \r\n            # Get thresholds to ensure we exceed them\r\n            novelty_threshold = brain.neurogenesis_config.get('novelty_threshold', 3) \r\n            stress_threshold = brain.neurogenesis_config.get('stress_threshold', 0.7)\r\n            reward_threshold = brain.neurogenesis_config.get('reward_threshold', 0.6)\r\n            \r\n            print(f\"Neurogenesis thresholds - Novelty: {novelty_threshold}, Stress: {stress_threshold}, Reward: {reward_threshold}\")\r\n            \r\n            # Reset cooldown to allow neurogenesis to happen\r\n            if hasattr(brain, 'neurogenesis_data'):\r\n                # Store original for restoration\r\n                original_time = brain.neurogenesis_data.get('last_neuron_time', 0)\r\n                brain.neurogenesis_data['last_neuron_time'] = 0\r\n                print(\"Neurogenesis cooldown temporarily reset\")\r\n            \r\n            # Create state with all triggers boosted significantly above thresholds\r\n            state = {\r\n                # Boost all three pathways to ensure at least one succeeds\r\n                'novelty_exposure': novelty_threshold * 2,  # Double the threshold\r\n                'sustained_stress': stress_threshold * 2,\r\n                'recent_rewards': reward_threshold * 2,\r\n                \r\n                # Add current state values for context\r\n                'hunger': getattr(self.tamagotchi_logic.squid, 'hunger', 50),\r\n                'happiness': getattr(self.tamagotchi_logic.squid, 'happiness', 50),\r\n                'personality': getattr(self.tamagotchi_logic.squid, 'personality', None)\r\n            }\r\n            \r\n            print(f\"Submitting state with trigger values: {state}\")\r\n            \r\n            # Update state which will trigger natural neurogenesis\r\n            brain.update_state(state)\r\n            \r\n            # Verify if neurogenesis occurred\r\n            new_neurons = set(brain.neuron_positions.keys()) - old_neurons\r\n            \r\n            if new_neurons:\r\n                # Get details of the new neuron(s)\r\n                for new_neuron in new_neurons:\r\n                    # Check if connections were created\r\n                    connections = [k for k in brain.weights.keys() if new_neuron in k]\r\n                    \r\n                    # Generate message with details\r\n                    message = f\"Created neuron: {new_neuron} with {len(connections)} connections\"\r\n                    self.show_message(message)\r\n                    print(message)\r\n                    print(f\"Connections: {connections[:5]}...\")\r\n                    \r\n                    # Highlight the new neuron\r\n                    if hasattr(brain, 'neurogenesis_highlight'):\r\n                        brain.neurogenesis_highlight = {\r\n                            'neuron': new_neuron,\r\n                            'start_time': time.time(),\r\n                            'duration': 10.0  # 10 seconds highlight\r\n                        }\r\n                        brain.update()  # Force redraw\r\n                    \r\n                    # Force an immediate hebbian learning cycle to integrate the neuron\r\n                    if hasattr(brain, 'perform_hebbian_learning'):\r\n                        print(\"Triggering hebbian learning cycle to integrate new neuron\")\r\n                        brain.perform_hebbian_learning()\r\n            else:\r\n                self.show_message(\"No new neurons created - check console for details\")\r\n                print(\"WARNING: Neurogenesis was triggered but no new neurons were created\")\r\n                print(f\"State submitted: {state}\")\r\n                print(f\"Neurogenesis config: {brain.neurogenesis_config}\")\r\n            \r\n            # Restore original cooldown time\r\n            if hasattr(brain, 'neurogenesis_data') and 'original_time' in locals():\r\n                brain.neurogenesis_data['last_neuron_time'] = original_time\r\n                print(\"Neurogenesis cooldown restored\")\r\n                \r\n        except Exception as e:\r\n            self.show_message(f\"Neurogenesis Error: {str(e)}\")\r\n            import traceback\r\n            traceback.print_exc()\r\n\r\n    def update_heatmap(self):\r\n        \"\"\"Update the connection weight heatmap visualization\"\"\"\r\n        if not hasattr(self, 'heatmap_scene') or not hasattr(self, 'brain_widget'):\r\n            return\r\n\r\n        self.heatmap_scene.clear()\r\n        \r\n        try:\r\n            # Get neuron data from brain widget\r\n            neurons = list(self.brain_widget.neuron_positions.keys())\r\n            excluded = getattr(self.brain_widget, 'excluded_neurons', [])\r\n            weights = getattr(self.brain_widget, 'weights', {})\r\n            \r\n            # Filter out excluded neurons\r\n            neurons = [n for n in neurons if n not in excluded]\r\n            if not neurons:\r\n                self.heatmap_scene.addText(\"No neurons available\", QtGui.QFont(), QtCore.QPointF(50, 50))\r\n                return\r\n\r\n            # Heatmap parameters\r\n            cell_size = 30\r\n            padding = 50\r\n            max_weight = max(abs(w) for w in weights.values()) if weights else 1.0\r\n            max_weight = max(max_weight, 0.01)  # Prevent division by zero\r\n\r\n            # Create heatmap grid\r\n            for i, src in enumerate(neurons):\r\n                for j, dst in enumerate(neurons):\r\n                    if src == dst:\r\n                        continue\r\n                        \r\n                    # Get weight value (check both direction permutations)\r\n                    weight = weights.get((src, dst), weights.get((dst, src), 0))\r\n                    \r\n                    # Calculate color intensity\r\n                    intensity = min(abs(weight) / max_weight, 1.0)\r\n                    if weight > 0:\r\n                        color = QtGui.QColor(0, 0, int(255 * intensity))  # Blue for positive\r\n                    else:\r\n                        color = QtGui.QColor(int(255 * intensity), 0, 0)  # Red for negative\r\n                        \r\n                    # Draw cell\r\n                    rect = QtCore.QRectF(\r\n                        padding + j * cell_size,\r\n                        padding + i * cell_size,\r\n                        cell_size - 1,  # -1 for grid lines\r\n                        cell_size - 1\r\n                    )\r\n                    self.heatmap_scene.addRect(rect, QtGui.QPen(QtCore.Qt.black, 0.5), \r\n                                            QtGui.QBrush(color))\r\n\r\n            # Add labels\r\n            font = QtGui.QFont()\r\n            font.setPointSize(8)\r\n            for idx, neuron in enumerate(neurons):\r\n                # Column labels (top)\r\n                text = self.heatmap_scene.addText(neuron, font)\r\n                text.setPos(padding + idx * cell_size + cell_size/2 - text.boundingRect().width()/2, \r\n                        padding - 25)\r\n                \r\n                # Row labels (left)\r\n                text = self.heatmap_scene.addText(neuron, font)\r\n                text.setPos(padding - text.boundingRect().width() - 5, \r\n                        padding + idx * cell_size + cell_size/2 - text.boundingRect().height()/2)\r\n\r\n            # Add legend\r\n            self._draw_heatmap_legend(padding, len(neurons) * cell_size + padding + 20)\r\n\r\n        except Exception as e:\r\n            print(f\"Heatmap error: {str(e)}\")\r\n            error_text = self.heatmap_scene.addText(\"Heatmap unavailable\")\r\n            error_text.setPos(50, 50)\r\n\r\n    def _draw_heatmap_legend(self, x, y):\r\n        \"\"\"Add color legend to heatmap\"\"\"\r\n        legend_width = 200\r\n        gradient = QtGui.QLinearGradient(0, 0, legend_width, 0)\r\n        gradient.setColorAt(0, QtGui.QColor(255, 0, 0))  # Red\r\n        gradient.setColorAt(0.5, QtGui.QColor(0, 0, 0))   # Black\r\n        gradient.setColorAt(1, QtGui.QColor(0, 0, 255))  # Blue\r\n        \r\n        legend = QtWidgets.QGraphicsRectItem(x, y, legend_width, 20)\r\n        legend.setBrush(QtGui.QBrush(gradient))\r\n        self.heatmap_scene.addItem(legend)\r\n        \r\n        # Add labels - create text items first, then set their positions\r\n        text_min = self.heatmap_scene.addText(\"-1.0\")\r\n        text_min.setPos(x, y + 20)\r\n        \r\n        text_zero = self.heatmap_scene.addText(\"0\")\r\n        text_zero.setPos(x + legend_width//2 - 10, y + 20)\r\n        \r\n        text_max = self.heatmap_scene.addText(\"+1.0\")\r\n        text_max.setPos(x + legend_width - 30, y + 20)\r\n\r\n    def get_center_position(self):\r\n        \"\"\"Calculate center position for new debug neurons\"\"\"\r\n        x = sum(p[0] for p in self.neuron_positions.values()) // len(self.neuron_positions)\r\n        y = sum(p[1] for p in self.neuron_positions.values()) // len(self.neuron_positions)\r\n        return (x + random.randint(-50, 50), y + random.randint(-50, 50))\r\n    \r\n    def update_paused_overlay(self):\r\n        \"\"\"Update the paused state\"\"\"\r\n        # Maintain pause state but don't show visual overlay\r\n        if hasattr(self, 'paused_overlay_label'):\r\n            self.paused_overlay_label.setVisible(False)  # Always keep it invisible\r\n            self.paused_overlay_label.deleteLater()\r\n            delattr(self, 'paused_overlay_label')\r\n    \r\n    def update_learning_statistics(self):\r\n        \"\"\"Update the statistics tab with comprehensive learning metrics\"\"\"\r\n        # Get blacklisted neurons\r\n        excluded_neurons = getattr(self.brain_widget, 'excluded_neurons', ['is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction'])\r\n        \r\n        # Clear the stats layout\r\n        while self.stats_box_layout.count():\r\n            item = self.stats_box_layout.takeAt(0)\r\n            if item.widget():\r\n                item.widget().deleteLater()\r\n        \r\n        # Add styled stats\r\n        def add_stat_box(title, content, bg_color=\"#f8f9fa\", icon=None):\r\n            box = QtWidgets.QGroupBox()\r\n            box.setStyleSheet(f\"\"\"\r\n                QGroupBox {{\r\n                    background-color: {bg_color};\r\n                    border-radius: 8px;\r\n                    border: 1px solid #dee2e6;\r\n                    margin-top: 15px;\r\n                    padding: 10px;\r\n                }}\r\n                QGroupBox::title {{\r\n                    subcontrol-origin: margin;\r\n                    left: 10px;\r\n                    padding: 0 5px;\r\n                    color: #495057;\r\n                }}\r\n            \"\"\")\r\n            \r\n            box_layout = QtWidgets.QVBoxLayout(box)\r\n            \r\n            # Add title with icon if provided\r\n            title_layout = QtWidgets.QHBoxLayout()\r\n            \r\n            if icon:\r\n                icon_label = QtWidgets.QLabel()\r\n                icon_label.setPixmap(QtGui.QPixmap(icon).scaled(24, 24, QtCore.Qt.KeepAspectRatio))\r\n                title_layout.addWidget(icon_label)\r\n            \r\n            title_label = QtWidgets.QLabel(title)\r\n            title_label.setStyleSheet(\"font-weight: bold; font-size: 14px; color: #212529;\")\r\n            title_layout.addWidget(title_label)\r\n            title_layout.addStretch()\r\n            \r\n            box_layout.addLayout(title_layout)\r\n            \r\n            # Add content\r\n            content_widget = QtWidgets.QLabel(content)\r\n            content_widget.setTextFormat(QtCore.Qt.RichText)\r\n            content_widget.setWordWrap(True)\r\n            content_widget.setStyleSheet(\"font-size: 13px; color: #343a40; margin: 5px;\")\r\n            box_layout.addWidget(content_widget)\r\n            \r\n            self.stats_box_layout.addWidget(box)\r\n        \r\n        # 1. Connection Statistics\r\n        # Filter weights to exclude connections involving blacklisted neurons\r\n        filtered_weights = {(src, dst): weight for (src, dst), weight in self.brain_widget.weights.items() \r\n                        if src not in excluded_neurons and dst not in excluded_neurons}\r\n        \r\n        positive_weights = sum(1 for w in filtered_weights.values() if w > 0)\r\n        negative_weights = sum(1 for w in filtered_weights.values() if w < 0)\r\n        avg_weight = sum(abs(w) for w in filtered_weights.values()) / max(1, len(filtered_weights))\r\n        \r\n        connection_stats = f\"\"\"\r\n        <table style='width:100%; margin-top:5px;'>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Total Connections:</b></td>\r\n                <td style='padding:3px;'>{len(filtered_weights)}</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Positive Connections:</b></td>\r\n                <td style='padding:3px;'>{positive_weights} ({positive_weights/max(1,len(filtered_weights))*100:.1f}%)</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Negative Connections:</b></td>\r\n                <td style='padding:3px;'>{negative_weights} ({negative_weights/max(1,len(filtered_weights))*100:.1f}%)</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Average Weight Strength:</b></td>\r\n                <td style='padding:3px;'>{avg_weight:.3f}</td>\r\n            </tr>\r\n        </table>\r\n        \"\"\"\r\n        add_stat_box(\"Connection Statistics\", connection_stats, \"#e3f2fd\")\r\n        \r\n        # 2. Neuron Statistics\r\n        all_neurons = self.brain_widget.neuron_positions.keys()\r\n        neurons = [n for n in all_neurons if n not in excluded_neurons]\r\n        original_neurons = [n for n in neurons if n in getattr(self.brain_widget, 'original_neuron_positions', {})]\r\n        new_neurons = [n for n in neurons if n in self.brain_widget.neurogenesis_data.get('new_neurons', [])]\r\n        \r\n        neuron_stats = f\"\"\"\r\n        <table style='width:100%; margin-top:5px;'>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Learning-Eligible Neurons:</b></td>\r\n                <td style='padding:3px;'>{len(neurons)}</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Original Core Neurons:</b></td>\r\n                <td style='padding:3px;'>{len(original_neurons)}</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Neurons from Neurogenesis:</b></td>\r\n                <td style='padding:3px;'>{len(new_neurons)}</td>\r\n            </tr>\r\n            <tr>\r\n                <td style='padding:3px;'><b>Excluded System Neurons:</b></td>\r\n                <td style='padding:3px;'>{len(excluded_neurons)}</td>\r\n            </tr>\r\n        </table>\r\n        \"\"\"\r\n        add_stat_box(\"Neuron Statistics\", neuron_stats, \"#e8f5e9\")\r\n        \r\n        # 3. Learning Parameters\r\n        if hasattr(self.config, 'hebbian'):\r\n            learning_rate = self.config.hebbian.get('base_learning_rate', 0.1)\r\n            threshold = self.config.hebbian.get('threshold', 0.7)\r\n            decay = self.config.hebbian.get('weight_decay', 0.01)\r\n            \r\n            learning_params = f\"\"\"\r\n            <table style='width:100%; margin-top:5px;'>\r\n                <tr>\r\n                    <td style='padding:3px;'><b>Learning Rate:</b></td>\r\n                    <td style='padding:3px;'>{learning_rate}</td>\r\n                </tr>\r\n                <tr>\r\n                    <td style='padding:3px;'><b>Activation Threshold:</b></td>\r\n                    <td style='padding:3px;'>{threshold}</td>\r\n                </tr>\r\n                <tr>\r\n                    <td style='padding:3px;'><b>Weight Decay:</b></td>\r\n                    <td style='padding:3px;'>{decay}</td>\r\n                </tr>\r\n                <tr>\r\n                    <td style='padding:3px;'><b>Learning Interval:</b></td>\r\n                    <td style='padding:3px;'>{self.config.hebbian.get('learning_interval', 30000)/1000} seconds</td>\r\n                </tr>\r\n            </table>\r\n            \"\"\"\r\n            add_stat_box(\"Learning Parameters\", learning_params, \"#fff3e0\")\r\n        \r\n        # 4. Strong Influence Neurons\r\n        # Find neurons with strongest outgoing connections\r\n        neuron_influence = {}\r\n        for neuron in neurons:\r\n            outgoing_sum = 0\r\n            outgoing_count = 0\r\n            for (src, dst), weight in filtered_weights.items():\r\n                if src == neuron:\r\n                    outgoing_sum += abs(weight)\r\n                    outgoing_count += 1\r\n            \r\n            if outgoing_count > 0:\r\n                neuron_influence[neuron] = outgoing_sum / outgoing_count\r\n        \r\n        top_influence = sorted(neuron_influence.items(), key=lambda x: x[1], reverse=True)[:5]\r\n        \r\n        influence_stats = \"<table style='width:100%; margin-top:5px;'>\"\r\n        for neuron, influence in top_influence:\r\n            influence_stats += f\"\"\"\r\n            <tr>\r\n                <td style='padding:3px;'><b>{neuron}</b></td>\r\n                <td style='padding:3px;'>{influence:.3f}</td>\r\n            </tr>\r\n            \"\"\"\r\n        influence_stats += \"</table>\"\r\n        \r\n        add_stat_box(\"Top Influential Neurons\", influence_stats, \"#f3e5f5\")\r\n        \r\n        # 5. Recently Created Neurons\r\n        if self.brain_widget.neurogenesis_data.get('new_neurons'):\r\n            new_neurons = [n for n in self.brain_widget.neurogenesis_data.get('new_neurons', []) \r\n                        if n not in excluded_neurons]\r\n            last_time = self.brain_widget.neurogenesis_data.get('last_neuron_time', 0)\r\n            time_ago = time.time() - last_time\r\n            \r\n            neurogenesis_stats = f\"\"\"\r\n            <p>Most recent neuron created <b>{int(time_ago/60)} minutes</b> ago.</p>\r\n            <p>Recent neurons (newest first):</p>\r\n            <ul>\r\n            \"\"\"\r\n            \r\n            for neuron in reversed(new_neurons[-5:]):\r\n                neurogenesis_stats += f\"<li>{neuron}</li>\"\r\n            \r\n            neurogenesis_stats += \"</ul>\"\r\n            \r\n            add_stat_box(\"Neurogenesis\", neurogenesis_stats, \"#ffebee\")\r\n        \r\n        # Add a stretch to push all boxes to the top\r\n        self.stats_box_layout.addStretch()\r\n\r\n    def zoom_heatmap(self, value):\r\n        \"\"\"Zoom the heatmap view based on slider value\"\"\"\r\n        scale = value / 100.0\r\n        \r\n        # Get current transform\r\n        transform = QtGui.QTransform()\r\n        transform.scale(scale, scale)\r\n        \r\n        # Apply new transform\r\n        self.heatmap_view.setTransform(transform)\r\n\r\n    \r\n\r\n    def export_learning_data(self):\r\n        \"\"\"Export learning data with all available information\"\"\"\r\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(\r\n            self, \"Export Learning Data\", \"\", \"HTML Files (*.html);;CSV Files (*.csv);;Text Files (*.txt)\")\r\n        \r\n        if not file_name:\r\n            return\r\n            \r\n        try:\r\n            if file_name.endswith('.html'):\r\n                self.export_learning_data_html(file_name)\r\n            elif file_name.endswith('.csv'):\r\n                self.export_learning_data_csv(file_name)\r\n            else:\r\n                self.export_learning_data_text(file_name)\r\n                \r\n            # Show success message\r\n            QtWidgets.QMessageBox.information(\r\n                self, \"Export Successful\", f\"Learning data exported to {file_name}\")\r\n        except Exception as e:\r\n            QtWidgets.QMessageBox.critical(\r\n                self, \"Export Error\", f\"Error exporting data: {str(e)}\")\r\n\r\n    def export_learning_data_html(self, file_name):\r\n        \"\"\"Export learning data as rich HTML report\"\"\"\r\n        with open(file_name, 'w') as f:\r\n            # Start HTML document\r\n            f.write(\"\"\"<!DOCTYPE html>\r\n            <html>\r\n            <head>\r\n                <title>Squid Brain Learning Data</title>\r\n                <style>\r\n                    body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }\r\n                    h1, h2, h3 { color: #2c3e50; }\r\n                    table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }\r\n                    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }\r\n                    th { background-color: #f2f2f2; }\r\n                    tr:hover { background-color: #f5f5f5; }\r\n                    .positive { color: green; }\r\n                    .negative { color: red; }\r\n                    .stats-box { background-color: #f8f9fa; border-radius: 8px; padding: 15px; margin: 15px 0; }\r\n                    .stats-title { font-weight: bold; font-size: 18px; margin-bottom: 10px; }\r\n                    .heatmap { overflow-x: auto; }\r\n                </style>\r\n            </head>\r\n            <body>\r\n                <h1>Squid Brain Learning Data</h1>\r\n                <p>Export time: \"\"\" + time.strftime(\"%Y-%m-%d %H:%M:%S\") + \"\"\"</p>\r\n            \"\"\")\r\n            \r\n            # Learning parameters\r\n            f.write(\"\"\"\r\n                <div class=\"stats-box\">\r\n                    <div class=\"stats-title\">Learning Parameters</div>\r\n                    <table>\r\n                        <tr>\r\n                            <th>Parameter</th>\r\n                            <th>Value</th>\r\n                        </tr>\r\n            \"\"\")\r\n            \r\n            if hasattr(self.config, 'hebbian'):\r\n                for param, value in self.config.hebbian.items():\r\n                    if param == 'learning_interval':\r\n                        value = f\"{value/1000} seconds\"\r\n                    f.write(f\"<tr><td>{param}</td><td>{value}</td></tr>\")\r\n            \r\n            f.write(\"\"\"\r\n                    </table>\r\n                </div>\r\n            \"\"\")\r\n            \r\n            # Neuron information\r\n            neurons = sorted(self.brain_widget.neuron_positions.keys())\r\n            f.write(\"\"\"\r\n                <div class=\"stats-box\">\r\n                    <div class=\"stats-title\">Neurons</div>\r\n                    <p>Total neurons: \"\"\" + str(len(neurons)) + \"\"\"</p>\r\n                    <table>\r\n                        <tr>\r\n                            <th>Neuron</th>\r\n                            <th>Position</th>\r\n                            <th>Type</th>\r\n                            <th>Current Value</th>\r\n                        </tr>\r\n            \"\"\")\r\n            \r\n            for neuron in neurons:\r\n                neuron_type = \"Original\" if neuron in getattr(self.brain_widget, 'original_neuron_positions', {}) else \"New\"\r\n                value = self.brain_widget.state.get(neuron, 0)\r\n                position = self.brain_widget.neuron_positions.get(neuron, (0, 0))\r\n                \r\n                f.write(f\"\"\"\r\n                    <tr>\r\n                        <td>{neuron}</td>\r\n                        <td>({position[0]:.1f}, {position[1]:.1f})</td>\r\n                        <td>{neuron_type}</td>\r\n                        <td>{value:.1f}</td>\r\n                    </tr>\r\n                \"\"\")\r\n            \r\n            f.write(\"\"\"\r\n                    </table>\r\n                </div>\r\n            \"\"\")\r\n            \r\n            # Connection weights\r\n            f.write(\"\"\"\r\n                <div class=\"stats-box\">\r\n                    <div class=\"stats-title\">Connection Weights</div>\r\n                    <p>Total connections: \"\"\" + str(len(self.brain_widget.weights)) + \"\"\"</p>\r\n                    <table>\r\n                        <tr>\r\n                            <th>Source</th>\r\n                            <th>Target</th>\r\n                            <th>Weight</th>\r\n                        </tr>\r\n            \"\"\")\r\n            \r\n            for (source, target), weight in sorted(self.brain_widget.weights.items(), key=lambda x: abs(x[1]), reverse=True):\r\n                weight_class = \"positive\" if weight > 0 else \"negative\"\r\n                f.write(f\"\"\"\r\n                    <tr>\r\n                        <td>{source}</td>\r\n                        <td>{target}</td>\r\n                        <td class=\"{weight_class}\">{weight:.3f}</td>\r\n                    </tr>\r\n                \"\"\")\r\n            \r\n            f.write(\"\"\"\r\n                    </table>\r\n                </div>\r\n            \"\"\")\r\n            \r\n            # Simple text-based heatmap\r\n            f.write(\"\"\"\r\n                <div class=\"stats-box\">\r\n                    <div class=\"stats-title\">Weight Heatmap (Text Representation)</div>\r\n                    <p>This is a simplified text representation of the weight matrix.</p>\r\n                    <div class=\"heatmap\">\r\n                        <table>\r\n                            <tr>\r\n                                <th>Source / Target</th>\r\n            \"\"\")\r\n            \r\n            # Column headers\r\n            for neuron in neurons:\r\n                f.write(f\"<th>{neuron}</th>\")\r\n            \r\n            f.write(\"</tr>\")\r\n            \r\n            # Rows with data\r\n            for src in neurons:\r\n                f.write(f\"<tr><th>{src}</th>\")\r\n                \r\n                for dst in neurons:\r\n                    if src == dst:\r\n                        f.write(\"<td style='background-color: #f0f0f0;'>—</td>\")\r\n                    else:\r\n                        weight = self.brain_widget.weights.get((src, dst), \r\n                                self.brain_widget.weights.get((dst, src), 0))\r\n                        \r\n                        # Style based on weight\r\n                        if weight > 0:\r\n                            intensity = min(255, int(weight * 255))\r\n                            bg_color = f\"rgba(0, {intensity}, 0, 0.2)\"\r\n                            text_color = \"green\"\r\n                        else:\r\n                            intensity = min(255, int(abs(weight) * 255))\r\n                            bg_color = f\"rgba({intensity}, 0, 0, 0.2)\"\r\n                            text_color = \"red\"\r\n                        \r\n                        f.write(f\"<td style='background-color: {bg_color}; color: {text_color};'>{weight:.2f}</td>\")\r\n                \r\n                f.write(\"</tr>\")\r\n            \r\n            f.write(\"\"\"\r\n                        </table>\r\n                    </div>\r\n                </div>\r\n            \"\"\")\r\n            \r\n            # End HTML document\r\n            f.write(\"\"\"\r\n            </body>\r\n            </html>\r\n            \"\"\")\r\n\r\n    def export_learning_data_csv(self, file_name):\r\n        \"\"\"Export learning data as CSV\"\"\"\r\n        with open(file_name, 'w', newline='') as f:\r\n            writer = csv.writer(f)\r\n            \r\n            # Write neurons section\r\n            writer.writerow([\"NEURONS\"])\r\n            writer.writerow([\"Neuron\", \"Position X\", \"Position Y\", \"Type\", \"Current Value\"])\r\n            \r\n            for neuron in sorted(self.brain_widget.neuron_positions.keys()):\r\n                neuron_type = \"Original\" if neuron in getattr(self.brain_widget, 'original_neuron_positions', {}) else \"New\"\r\n                value = self.brain_widget.state.get(neuron, 0)\r\n                position = self.brain_widget.neuron_positions.get(neuron, (0, 0))\r\n                \r\n                writer.writerow([neuron, position[0], position[1], neuron_type, value])\r\n            \r\n            # Blank row\r\n            writer.writerow([])\r\n            \r\n            # Write connections section\r\n            writer.writerow([\"CONNECTIONS\"])\r\n            writer.writerow([\"Source\", \"Target\", \"Weight\"])\r\n            \r\n            for (source, target), weight in sorted(self.brain_widget.weights.items(), key=lambda x: abs(x[1]), reverse=True):\r\n                writer.writerow([source, target, weight])\r\n            \r\n            # Blank row\r\n            writer.writerow([])\r\n            \r\n            # Write learning parameters\r\n            writer.writerow([\"LEARNING PARAMETERS\"])\r\n            if hasattr(self.config, 'hebbian'):\r\n                for param, value in self.config.hebbian.items():\r\n                    writer.writerow([param, value])\r\n\r\n    def export_learning_data_text(self, file_name):\r\n        \"\"\"Export learning data as plain text\"\"\"\r\n        with open(file_name, 'w') as f:\r\n            f.write(\"SQUID BRAIN LEARNING DATA\\n\")\r\n            f.write(\"=========================\\n\")\r\n            f.write(f\"Export time: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n\\n\")\r\n            \r\n            # Learning parameters\r\n            f.write(\"LEARNING PARAMETERS\\n\")\r\n            f.write(\"-----------------\\n\")\r\n            if hasattr(self.config, 'hebbian'):\r\n                for param, value in self.config.hebbian.items():\r\n                    if param == 'learning_interval':\r\n                        value = f\"{value/1000} seconds\"\r\n                    f.write(f\"{param}: {value}\\n\")\r\n            \r\n            f.write(\"\\n\")\r\n            \r\n            # Neuron information\r\n            neurons = sorted(self.brain_widget.neuron_positions.keys())\r\n            f.write(f\"NEURONS ({len(neurons)} total)\\n\")\r\n            f.write(\"-----------------\\n\")\r\n            \r\n            for neuron in neurons:\r\n                neuron_type = \"Original\" if neuron in getattr(self.brain_widget, 'original_neuron_positions', {}) else \"New\"\r\n                value = self.brain_widget.state.get(neuron, 0)\r\n                position = self.brain_widget.neuron_positions.get(neuron, (0, 0))\r\n                \r\n                f.write(f\"{neuron}: Position ({position[0]:.1f}, {position[1]:.1f}), Type: {neuron_type}, Value: {value:.1f}\\n\")\r\n            \r\n            f.write(\"\\n\")\r\n            \r\n            # Connection weights\r\n            f.write(f\"CONNECTION WEIGHTS ({len(self.brain_widget.weights)} total)\\n\")\r\n            f.write(\"-----------------\\n\")\r\n            \r\n            for (source, target), weight in sorted(self.brain_widget.weights.items(), key=lambda x: abs(x[1]), reverse=True):\r\n                f.write(f\"{source} → {target}: {weight:.3f}\\n\")\r\n\r\n    def clear_learning_log(self):\r\n        \"\"\"Clear the activity log\"\"\"\r\n        reply = QtWidgets.QMessageBox.question(\r\n            self, \"Clear Log\", \r\n            \"Are you sure you want to clear the learning activity log?\",\r\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No\r\n        )\r\n        \r\n        if reply == QtWidgets.QMessageBox.Yes:\r\n            self.activity_log.clear()\r\n\r\n# brain_tool.py\r\n\r\n# ... (other imports at the top of brain_tool.py, ensure these are present)\r\n# from PyQt5 import QtCore, QtGui, QtWidgets\r\n# from PyQt5.QtWidgets import QSplitter, QTabWidget, QTableWidget, QTableWidgetItem, QFormLayout, QGroupBox # Explicitly ensure these\r\n# import time # For time.time() in neurogenesis details\r\n# from datetime import datetime # For formatting timestamps\r\n\r\n# ... (SquidBrainWindow class and other preceding code) ...\r\n\r\nclass NeuronInspector(QtWidgets.QDialog):\r\n    def __init__(self, brain_tool_window, brain_widget_ref, parent=None): # brain_tool_window is the SquidBrainWindow instance\r\n        super().__init__(brain_tool_window) # Set parent to brain_tool_window\r\n        self.brain_tool_window = brain_tool_window \r\n        self.brain_widget = brain_widget_ref \r\n\r\n        self.setWindowTitle(\"Neuron Inspector\")\r\n        # Portrait orientation, larger size\r\n        self.setFixedSize(450, 700) # Width, Height\r\n\r\n        self.main_layout = QtWidgets.QVBoxLayout()\r\n        self.setLayout(self.main_layout)\r\n\r\n        # Neuron selector\r\n        self.neuron_combo = QtWidgets.QComboBox()\r\n        self.neuron_combo.setToolTip(\"Select a neuron to inspect or click one in the visualizer.\")\r\n        self.main_layout.addWidget(self.neuron_combo)\r\n\r\n        # Tab widget\r\n        self.tabs = QtWidgets.QTabWidget() #\r\n        self.main_layout.addWidget(self.tabs)\r\n\r\n        # --- Tab 1: Overview ---\r\n        self.overview_tab = QtWidgets.QWidget()\r\n        self.overview_layout = QtWidgets.QFormLayout(self.overview_tab) \r\n        self.overview_tab.setLayout(self.overview_layout)\r\n        self.tabs.addTab(self.overview_tab, \"Overview\")\r\n\r\n        self.name_label = QtWidgets.QLabel()\r\n        self.value_label = QtWidgets.QLabel()\r\n        self.position_label = QtWidgets.QLabel()\r\n        self.type_label = QtWidgets.QLabel() # Core or Neurogenesis\r\n\r\n        self.overview_layout.addRow(\"<b>Name:</b>\", self.name_label)\r\n        self.overview_layout.addRow(\"<b>Current Value:</b>\", self.value_label)\r\n        self.overview_layout.addRow(\"<b>Position (X,Y):</b>\", self.position_label)\r\n        self.overview_layout.addRow(\"<b>Type:</b>\", self.type_label)\r\n\r\n        # Placeholder for neurogenesis info\r\n        self.neurogenesis_group = QtWidgets.QGroupBox(\"Neurogenesis Details\") \r\n        self.neurogenesis_layout = QtWidgets.QFormLayout() \r\n        self.neurogenesis_group.setLayout(self.neurogenesis_layout)\r\n        self.neurogenesis_group.setVisible(False) # Hidden by default\r\n\r\n        self.created_at_label = QtWidgets.QLabel()\r\n        self.trigger_type_label = QtWidgets.QLabel()\r\n        self.trigger_value_label = QtWidgets.QLabel()\r\n        self.associated_state_label = QtWidgets.QLabel()\r\n        self.associated_state_label.setWordWrap(True)\r\n\r\n        self.neurogenesis_layout.addRow(\"<b>Created At:</b>\", self.created_at_label)\r\n        self.neurogenesis_layout.addRow(\"<b>Trigger Type:</b>\", self.trigger_type_label)\r\n        self.neurogenesis_layout.addRow(\"<b>Trigger Value:</b>\", self.trigger_value_label)\r\n        self.neurogenesis_layout.addRow(\"<b>Associated State:</b>\", self.associated_state_label)\r\n        self.overview_layout.addWidget(self.neurogenesis_group)\r\n\r\n\r\n        # --- Tab 2: Connections ---\r\n        self.connections_tab = QtWidgets.QWidget()\r\n        self.connections_layout = QtWidgets.QVBoxLayout(self.connections_tab)\r\n        self.tabs.addTab(self.connections_tab, \"Connections\")\r\n\r\n        self.connections_table = QtWidgets.QTableWidget() \r\n        self.connections_table.setColumnCount(3)\r\n        self.connections_table.setHorizontalHeaderLabels([\"Connected To\", \"Weight\", \"Direction\"])\r\n        self.connections_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)\r\n        self.connections_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\r\n        self.connections_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\r\n        self.connections_layout.addWidget(self.connections_table)\r\n\r\n        # --- Tab 3: Activity (Placeholder) ---\r\n        self.activity_tab = QtWidgets.QWidget()\r\n        self.activity_layout = QtWidgets.QVBoxLayout(self.activity_tab)\r\n        self.activity_info_label = QtWidgets.QLabel(\"Detailed activity logging and graphing coming soon.\")\r\n        self.activity_info_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.activity_layout.addWidget(self.activity_info_label)\r\n        self.tabs.addTab(self.activity_tab, \"Activity\")\r\n\r\n        # --- Refresh Button (optional, as it updates on click) ---\r\n        self.refresh_btn = QtWidgets.QPushButton(\"Refresh Data\")\r\n        self.refresh_btn.clicked.connect(self.update_info)\r\n        self.main_layout.addWidget(self.refresh_btn)\r\n\r\n        # Connect to brain widget's neuronClicked signal\r\n        if hasattr(self.brain_widget, 'neuronClicked'):\r\n            self.brain_widget.neuronClicked.connect(self.inspect_neuron_by_name) #\r\n\r\n        self.neuron_combo.currentIndexChanged.connect(self.update_info_from_combo)\r\n\r\n        self.update_neuron_list()\r\n        if self.neuron_combo.count() > 0:\r\n            self.update_info() # Initial update\r\n\r\n    def update_neuron_list(self):\r\n        if not self.brain_widget: return\r\n        current_selection = self.neuron_combo.currentText()\r\n        self.neuron_combo.clear()\r\n        # Sort neuron names for consistent order\r\n        neuron_names = sorted(self.brain_widget.neuron_positions.keys())\r\n        self.neuron_combo.addItems(neuron_names)\r\n        if current_selection in neuron_names:\r\n            self.neuron_combo.setCurrentText(current_selection)\r\n        elif neuron_names:\r\n            self.neuron_combo.setCurrentIndex(0)\r\n\r\n\r\n    def inspect_neuron_by_name(self, neuron_name):\r\n        \"\"\"Slot to handle neuronClicked signal from BrainWidget.\"\"\"\r\n        index = self.neuron_combo.findText(neuron_name)\r\n        if index >= 0:\r\n            # Block signals temporarily to prevent double update if setCurrentIndex triggers update_info\r\n            self.neuron_combo.blockSignals(True)\r\n            self.neuron_combo.setCurrentIndex(index)\r\n            self.neuron_combo.blockSignals(False)\r\n            self.update_info() # Explicitly update after setting index\r\n        self.show()\r\n        self.raise_()\r\n        self.activateWindow()\r\n\r\n\r\n    def update_info_from_combo(self):\r\n        # This is called when combobox selection changes\r\n        self.update_info()\r\n\r\n    def update_info(self):\r\n        \"\"\"Update all display elements for the currently selected neuron.\"\"\"\r\n        if not self.brain_widget:\r\n            self.name_label.setText(\"<N/A>\")\r\n            # Clear other fields\r\n            self.value_label.setText(\"\")\r\n            self.position_label.setText(\"\")\r\n            self.type_label.setText(\"\")\r\n            self.neurogenesis_group.setVisible(False)\r\n            if hasattr(self, 'connections_table'): self.connections_table.setRowCount(0)\r\n            return\r\n\r\n        neuron_name = self.neuron_combo.currentText()\r\n        if not neuron_name or neuron_name not in self.brain_widget.neuron_positions:\r\n            # Clear all fields if no valid neuron is selected\r\n            self.name_label.setText(\"<No Neuron Selected>\")\r\n            self.value_label.setText(\"\")\r\n            self.position_label.setText(\"\")\r\n            self.type_label.setText(\"\")\r\n            self.neurogenesis_group.setVisible(False)\r\n            if hasattr(self, 'connections_table'): self.connections_table.setRowCount(0)\r\n            return\r\n\r\n        # --- Overview Tab Data ---\r\n        self.name_label.setText(f\"<b>{neuron_name}</b>\")\r\n\r\n        value = self.brain_widget.state.get(neuron_name, \"N/A\")\r\n        self.value_label.setText(str(round(value, 2) if isinstance(value, (float, int)) else value))\r\n\r\n        pos = self.brain_widget.neuron_positions.get(neuron_name, (\"N/A\", \"N/A\"))\r\n        self.position_label.setText(f\"({pos[0]:.1f}, {pos[1]:.1f})\" if isinstance(pos, tuple) and len(pos) == 2 and all(isinstance(p, (int,float)) for p in pos) else \"N/A\")\r\n\r\n\r\n        # Check if new_neurons_details exists and then if neuron_name is in it\r\n        is_neurogenesis = False\r\n        if hasattr(self.brain_widget, 'neurogenesis_data') and \\\r\n           'new_neurons_details' in self.brain_widget.neurogenesis_data and \\\r\n           neuron_name in self.brain_widget.neurogenesis_data.get('new_neurons_details', {}):\r\n            is_neurogenesis = True\r\n\r\n        neuron_kind = \"Neurogenesis\" if is_neurogenesis else \"Core\"\r\n        if neuron_name in self.brain_widget.excluded_neurons: \r\n            neuron_kind = \"System Status\"\r\n        self.type_label.setText(neuron_kind)\r\n\r\n        if is_neurogenesis:\r\n            details = self.brain_widget.neurogenesis_data['new_neurons_details'].get(neuron_name, {})\r\n            created_timestamp = details.get('created_at')\r\n            if created_timestamp:\r\n                # Ensure datetime is imported if not already: from datetime import datetime\r\n                from datetime import datetime # Local import for safety\r\n                self.created_at_label.setText(datetime.fromtimestamp(created_timestamp).strftime('%Y-%m-%d %H:%M:%S'))\r\n            else:\r\n                self.created_at_label.setText(\"Unknown\")\r\n            self.trigger_type_label.setText(str(details.get('trigger_type', \"N/A\")).capitalize())\r\n            \r\n            trigger_val = details.get('trigger_value_at_creation', \"N/A\")\r\n            self.trigger_value_label.setText(f\"{trigger_val:.2f}\" if isinstance(trigger_val, float) else str(trigger_val))\r\n\r\n\r\n            snapshot = details.get('associated_state_snapshot', {})\r\n            snapshot_text = \", \".join([f\"{k.capitalize()}: {v}\" for k, v in snapshot.items() if v is not None])\r\n            self.associated_state_label.setText(snapshot_text if snapshot_text else \"No specific state captured.\")\r\n            self.neurogenesis_group.setVisible(True)\r\n        else:\r\n            self.neurogenesis_group.setVisible(False)\r\n\r\n        # --- Connections Tab Data ---\r\n        self.connections_table.setRowCount(0) # Clear previous\r\n        connections_data = []\r\n        # Ensure brain_widget.weights exists and is a dictionary\r\n        if hasattr(self.brain_widget, 'weights') and isinstance(self.brain_widget.weights, dict):\r\n            for conn_key, weight_val in self.brain_widget.weights.items():\r\n                # Ensure conn_key is a tuple of two strings (neuron names)\r\n                if isinstance(conn_key, tuple) and len(conn_key) == 2:\r\n                    src, dst = conn_key\r\n                    if src == neuron_name:\r\n                        connections_data.append({'target': dst, 'weight': weight_val, 'direction': \"Outgoing\"})\r\n                    elif dst == neuron_name:\r\n                        connections_data.append({'target': src, 'weight': weight_val, 'direction': \"Incoming\"})\r\n                # else:\r\n                    # print(f\"Skipping malformed weight key: {conn_key}\")\r\n\r\n\r\n        self.connections_table.setRowCount(len(connections_data))\r\n        for row, conn_info in enumerate(connections_data):\r\n            self.connections_table.setItem(row, 0, QtWidgets.QTableWidgetItem(conn_info['target'])) #\r\n            item_weight = QtWidgets.QTableWidgetItem(f\"{conn_info['weight']:.3f}\") #\r\n            item_weight.setForeground(QtGui.QColor(\"green\") if conn_info['weight'] > 0 else QtGui.QColor(\"red\"))\r\n            self.connections_table.setItem(row, 1, item_weight)\r\n            self.connections_table.setItem(row, 2, QtWidgets.QTableWidgetItem(conn_info['direction'])) #\r\n\r\n\r\n        # Refresh neuron list only if necessary (e.g., if current neuron disappeared)\r\n        # This check was simplified; if it causes issues, it might need refinement.\r\n        # The primary update path is now through the combobox or direct neuron click.\r\n        if self.neuron_combo.findText(neuron_name) == -1 and neuron_name in self.brain_widget.neuron_positions :\r\n             self.update_neuron_list()\r\n\r\n\r\n"
  },
  {
    "path": "src/brain_tooltips.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom .display_scaling import DisplayScaling\r\nfrom .localisation import loc \r\nimport time\r\n\r\n\r\nclass EnhancedBrainTooltips:\r\n    \"\"\"Scale-aware tooltips for every neuron.\"\"\"\r\n\r\n    def __init__(self, brain_widget):\r\n        self.brain_widget = brain_widget\r\n        self.current_tooltip_neuron = None\r\n        self.last_tooltip_time = 0\r\n        self.tooltip_delay = 0.3  # seconds\r\n\r\n    # ------------------------------------------------------------------\r\n    # Safe helpers for optional FunctionalNeuronData attributes\r\n    # ------------------------------------------------------------------\r\n    def _get_creation_time(self, func_neuron):\r\n        if hasattr(func_neuron, 'creation_context'):\r\n            return func_neuron.creation_context.timestamp\r\n        return time.time()\r\n\r\n    def _get_last_activated(self, func_neuron):\r\n        return getattr(func_neuron, 'last_activated', 0)\r\n\r\n    def _get_neuron_type(self, func_neuron):\r\n        if hasattr(func_neuron, \"neuron_type\"):\r\n            return func_neuron.neuron_type\r\n        if hasattr(func_neuron, \"specialization\"):\r\n            return func_neuron.specialization\r\n        return loc(\"tooltip_functional\", default=\"functional\")\r\n\r\n    # ------------------------------------------------------------------\r\n    # Public entry point — keeps old signature for compatibility\r\n    # ------------------------------------------------------------------\r\n    def show_tooltip_for_position(self, event):\r\n        \"\"\"Legacy entry — we ignore event.pos() and use neuron centre.\"\"\"\r\n        neuron_name = self.brain_widget.get_neuron_at_pos(event.pos())\r\n        if neuron_name:\r\n            self.show_tooltip_for_neuron(neuron_name, event.pos())\r\n        else:\r\n            QtWidgets.QToolTip.hideText()\r\n            self.current_tooltip_neuron = None\r\n\r\n    # ------------------------------------------------------------------\r\n    # New scale-aware tooltip\r\n    # ------------------------------------------------------------------\r\n    \r\n    def show_tooltip_for_neuron(self, neuron_name, _unused_pos):\r\n        \"\"\"Show tooltip exactly above the neuron, matching paintEvent coordinates.\"\"\"\r\n        if not neuron_name:\r\n            self.hide_tooltip()\r\n            return\r\n\r\n        # Binary neurons will show ON/OFF, continuous neurons show numeric values.\r\n\r\n        # 1. Get neuron's logical position\r\n        x_logic, y_logic = self.brain_widget.neuron_positions[neuron_name]\r\n        \r\n        # 2. Calculate layout scale EXACTLY as paintEvent/render worker does\r\n        indicator_space = 0 \r\n        base_width = 1024\r\n        base_height = 768 - indicator_space\r\n        \r\n        # Use dimensions from the widget\r\n        widget_width = self.brain_widget.width()\r\n        widget_height = self.brain_widget.height()\r\n        \r\n        scale_x = widget_width / base_width\r\n        scale_y = (widget_height - indicator_space) / max(1, base_height)\r\n        scale = max(0.01, min(scale_x, scale_y))\r\n        \r\n        offset_x = 0\r\n        if scale_x > scale_y:\r\n            content_width = base_width * scale\r\n            offset_x = (widget_width - content_width) / 2\r\n        \r\n        # 3. Transform logical coordinates to widget coordinates\r\n        # Formula: Logical * Scale + Offset\r\n        widget_x = int(x_logic * scale + offset_x)\r\n        widget_y = int(y_logic * scale + indicator_space)\r\n        \r\n        # 4. Position tooltip 40px above neuron (visual offset - NOT scaled)\r\n        tooltip_pos_widget = QtCore.QPoint(widget_x, widget_y - 40)\r\n        \r\n        # 5. Convert to global screen coordinates for QToolTip\r\n        tooltip_pos_global = self.brain_widget.mapToGlobal(tooltip_pos_widget)\r\n        \r\n        # 6. Generate and show tooltip (scale only the HTML content)\r\n        html = self._generate_tooltip(neuron_name)\r\n        scaled_html = DisplayScaling.scale_css(html)\r\n        \r\n        QtWidgets.QToolTip.showText(tooltip_pos_global, scaled_html, self.brain_widget)\r\n        self.current_tooltip_neuron = neuron_name\r\n\r\n    def hide_tooltip(self):\r\n        QtWidgets.QToolTip.hideText()\r\n        self.current_tooltip_neuron = None\r\n\r\n    def _generate_tooltip(self, neuron_name):\r\n        bw = self.brain_widget\r\n        is_functional = (\r\n            hasattr(bw, 'enhanced_neurogenesis') and\r\n            neuron_name in bw.enhanced_neurogenesis.functional_neurons\r\n        )\r\n        return (\r\n            self._generate_functional_tooltip(neuron_name)\r\n            if is_functional\r\n            else self._generate_basic_tooltip(neuron_name)\r\n        )\r\n\r\n    def _generate_functional_tooltip(self, neuron_name):\r\n        bw = self.brain_widget\r\n        current_value = bw.state.get(neuron_name, 50)\r\n        \r\n        val_display = f\"{current_value:.1f}\"\r\n        \r\n        return f\"\"\"\r\n        <div style='background-color: black; color: white; padding: 1px; \r\n                    font-family: Arial; font-size: 30px; font-weight: bold; \r\n                    border-radius: 1px; min-width: 60px; text-align: center;'>\r\n            {val_display}\r\n        </div>\r\n        \"\"\"\r\n\r\n    def _is_binary_neuron(self, neuron_name):\r\n        \"\"\"\r\n        Check if a neuron is binary (outputs only 0 or 100).\r\n        \r\n        Priority order (first explicit value wins):\r\n        1. neuron_details from loaded brain file (most authoritative)\r\n        2. neurons dict with is_binary field\r\n        3. BINARY_NEURONS constant\r\n        4. Known binary sensors (hardcoded fallback)\r\n        \r\n        We do NOT use brain_widget.is_binary_neuron() as it may have incorrect logic.\r\n        \"\"\"\r\n        bw = self.brain_widget\r\n        \r\n        # Priority 1: Check neuron_details (from loaded brain JSON) - MOST AUTHORITATIVE\r\n        if hasattr(bw, 'neuron_details') and neuron_name in bw.neuron_details:\r\n            details = bw.neuron_details[neuron_name]\r\n            if isinstance(details, dict) and 'is_binary' in details:\r\n                return details['is_binary']\r\n        \r\n        # Priority 2: Check neurons dict (alternative storage in some brain formats)\r\n        if hasattr(bw, 'neurons') and neuron_name in bw.neurons:\r\n            neuron_data = bw.neurons[neuron_name]\r\n            if isinstance(neuron_data, dict) and 'is_binary' in neuron_data:\r\n                return neuron_data['is_binary']\r\n        \r\n        # Priority 3: Check config's neuron definitions\r\n        if hasattr(bw, 'config') and bw.config:\r\n            try:\r\n                neurons_config = bw.config.get_neurogenesis_config().get('neurons', {})\r\n                if neuron_name in neurons_config:\r\n                    neuron_cfg = neurons_config[neuron_name]\r\n                    if 'is_binary' in neuron_cfg:\r\n                        return neuron_cfg['is_binary']\r\n            except:\r\n                pass\r\n        \r\n        # Priority 4: Check BINARY_NEURONS constant\r\n        try:\r\n            from .brain_constants import BINARY_NEURONS\r\n            if neuron_name in BINARY_NEURONS:\r\n                return True\r\n        except ImportError:\r\n            pass\r\n        \r\n        # Priority 5: Known binary sensors (hardcoded fallback for core sensors)\r\n        # NOTE: plant_proximity is NOT in this list - it's continuous!\r\n        KNOWN_BINARY = {\r\n            'can_see_food', 'is_eating', 'is_sleeping', 'is_sick',\r\n            'pursuing_food', 'is_fleeing', 'is_startled'\r\n        }\r\n        if neuron_name in KNOWN_BINARY:\r\n            return True\r\n        \r\n        # Default: assume continuous (not binary)\r\n        return False\r\n\r\n    def _generate_basic_tooltip(self, neuron_name):\r\n        bw = self.brain_widget\r\n        current_value = bw.state.get(neuron_name, 0)\r\n        \r\n        # Check if this is a binary neuron using comprehensive check\r\n        is_binary = self._is_binary_neuron(neuron_name)\r\n        \r\n        if is_binary:\r\n            # For binary neurons, show localized ON/OFF\r\n            val_display = loc(\"state_on\") if current_value > 50 else loc(\"state_off\")\r\n        else:\r\n            # For continuous neurons (including plant_proximity), show numeric value\r\n            val_display = f\"{current_value:.1f}\"\r\n        \r\n        # Minimal black rectangle with white text (matching weight overlay style)\r\n        return f\"\"\"\r\n        <div style='background-color: black; color: white; padding: 10px; \r\n                    font-family: Arial; font-size: 28px; font-weight: bold; \r\n                    border-radius: 5px; min-width: 60px; text-align: center;'>\r\n            {val_display}\r\n        </div>\r\n        \"\"\"\r\n\r\n    # ------------------------------------------------------------------\r\n    # Helpers \r\n    # ------------------------------------------------------------------\r\n    def _get_connection_summary(self, neuron_name):\r\n        bw = self.brain_widget\r\n        incoming = [(b, w) for (a, b), w in bw.weights.items() if a == neuron_name]\r\n        outgoing = [(b, w) for (a, b), w in bw.weights.items() if b == neuron_name]\r\n        top_in = sorted(incoming, key=lambda x: abs(x[1]), reverse=True)[:3]\r\n        top_out = sorted(outgoing, key=lambda x: abs(x[1]), reverse=True)[:3]\r\n        \r\n        lbl_conn_header = loc(\"tooltip_connections_header\")\r\n        # Ensure we pass named arguments for formatting\r\n        conn_stats = loc(\"tooltip_connections_stats\", incoming=len(incoming), outgoing=len(outgoing))\r\n        \r\n        lbl_top_in = loc(\"tooltip_top_incoming\")\r\n        lbl_top_out = loc(\"tooltip_top_outgoing\")\r\n\r\n        html = f\"<div style='margin: 8px 0;'>\"\r\n        html += f\"<div style='color: #666; font-size: {DisplayScaling.font_size(12)}px; margin-bottom: 4px;'>\"\r\n        html += f\"<b>{lbl_conn_header}:</b> {conn_stats}</div>\"\r\n\r\n        if top_in:\r\n            html += f\"<div style='font-size: {DisplayScaling.font_size(14)}px; margin-left: 8px;'>\"\r\n            html += f\"⬅️ <b>{lbl_top_in}:</b><br>\"\r\n            for src, wt in top_in:\r\n                arrow, color = (\"→\", \"#4CAF50\") if wt > 0 else (\"⊣\", \"#f44336\")\r\n                html += f\"<span style='color: {color};'>{src} {arrow} {wt:.2f}</span><br>\"\r\n            html += \"</div>\"\r\n\r\n        if top_out:\r\n            html += f\"<div style='font-size: {DisplayScaling.font_size(14)}px; margin-left: 8px; margin-top: 4px;'>\"\r\n            html += f\"➡️ <b>{lbl_top_out}:</b><br>\"\r\n            for tgt, wt in top_out:\r\n                arrow, color = (\"→\", \"#4CAF50\") if wt > 0 else (\"⊣\", \"#f44336\")\r\n                html += f\"<span style='color: {color};'>{arrow} {tgt} {wt:.2f}</span><br>\"\r\n            html += \"</div>\"\r\n\r\n        html += \"</div>\"\r\n        return html\r\n\r\n    def _get_utility_color(self, utility):\r\n        if utility > 0.7:\r\n            return \"#4CAF50\"\r\n        elif utility > 0.4:\r\n            return \"#FFC107\"\r\n        elif utility > 0.2:\r\n            return \"#FF9800\"\r\n        return \"#F44336\"\r\n\r\n    def _get_activation_color(self, activation):\r\n        deviation = abs(activation - 50)\r\n        return \"#F44336\" if deviation > 30 else \"#FF9800\" if deviation > 15 else \"#4CAF50\"\r\n\r\n    def _get_utility_indicator(self, utility):\r\n        if utility > 0.7:\r\n            return \"⭐⭐⭐\"\r\n        elif utility > 0.4:\r\n            return \"⭐⭐\"\r\n        elif utility > 0.2:\r\n            return \"⭐\"\r\n        return \"⚠️\"\r\n"
  },
  {
    "path": "src/brain_ui_utils.py",
    "content": "from datetime import datetime\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nclass UiUtils:\r\n    @staticmethod\r\n    def create_styled_button(text, callback, color, size=(200, 50), font_size=10):\r\n        \"\"\"Create a button with consistent styling\"\"\"\r\n        button = QtWidgets.QPushButton(text)\r\n        button.clicked.connect(callback)\r\n        button.setStyleSheet(f\"\"\"\r\n            QPushButton {{\r\n                background-color: {color}; \r\n                border: 1px solid black; \r\n                padding: 5px;\r\n                font-size: {font_size}px;\r\n                border-radius: 5px;\r\n            }}\r\n            QPushButton:hover {{\r\n                background-color: {darken_color(color, 20)};\r\n            }}\r\n        \"\"\")\r\n        button.setFixedSize(size[0], size[1])\r\n        return button\r\n    \r\n    @staticmethod\r\n    def format_memory_display(memory):\r\n        \"\"\"Format a memory dictionary for display with colored boxes based on valence\"\"\"\r\n        if not UiUtils.is_displayable_memory(memory):\r\n            return \"\"\r\n        \r\n        # Get the display text - prefer formatted_value, fall back to value\r\n        display_text = memory.get('formatted_value', str(memory.get('value', '')))\r\n        \r\n        # Skip if the display text contains just a timestamp\r\n        if 'timestamp' in display_text.lower() and len(display_text.split()) < 3:\r\n            return \"\"\r\n        \r\n        timestamp = memory.get('timestamp', '')\r\n        if isinstance(timestamp, str):\r\n            try:\r\n                timestamp = datetime.fromisoformat(timestamp).strftime(\"%H:%M:%S\")\r\n            except:\r\n                timestamp = \"\"\r\n        \r\n        # Determine valence and color\r\n        if memory.get('category') == 'mental_state' and memory.get('key') == 'startled':\r\n            interaction_type = \"Negative\"\r\n            background_color = \"#FFD1DC\"  # Pastel red\r\n        elif isinstance(memory.get('raw_value'), dict):\r\n            total_effect = sum(float(val) for val in memory['raw_value'].values() \r\n                            if isinstance(val, (int, float)))\r\n            if total_effect > 0:\r\n                interaction_type = \"Positive\"\r\n                background_color = \"#D1FFD1\"  # Pastel green\r\n            elif total_effect < 0:\r\n                interaction_type = \"Negative\"\r\n                background_color = \"#FFD1DC\"  # Pastel red\r\n            else:\r\n                interaction_type = \"Neutral\"\r\n                background_color = \"#FFFACD\"  # Pastel yellow\r\n        else:\r\n            interaction_type = \"Neutral\"\r\n            background_color = \"#FFFACD\"  # Pastel yellow\r\n        \r\n        # Create HTML formatted memory box\r\n        formatted_memory = f\"\"\"\r\n        <div style=\"\r\n            background-color: {background_color}; \r\n            padding: 8px; \r\n            margin: 5px; \r\n            border-radius: 5px;\r\n            border: 1px solid #ccc;\r\n        \">\r\n            <div style=\"font-weight: bold; margin-bottom: 5px;\">{interaction_type}</div>\r\n            <div>{display_text}</div>\r\n            <div style=\"font-size: 0.8em; color: #555; margin-top: 5px;\">{timestamp}</div>\r\n        </div>\r\n        \"\"\"\r\n        \r\n        return formatted_memory\r\n    \r\n    @staticmethod\r\n    def _is_displayable_memory(self, memory):\r\n        \"\"\"Check if a memory should be displayed in the UI\"\"\"\r\n        if not isinstance(memory, dict):\r\n            return False\r\n        \r\n        # Skip timestamp-only memories (they have numeric keys)\r\n        if isinstance(memory.get('key'), str) and memory['key'].isdigit():\r\n            return False\r\n            \r\n        # Skip memories that don't have a proper category or value\r\n        if not memory.get('category') or not memory.get('value'):\r\n            return False\r\n            \r\n        # Skip memories where the value is just a timestamp number\r\n        if isinstance(memory.get('value'), (int, float)) and 'timestamp' in str(memory['value']).lower():\r\n            return False\r\n            \r\n        # Must have either formatted_value or a displayable string value\r\n        if 'formatted_value' not in memory and not isinstance(memory.get('value'), str):\r\n            return False\r\n            \r\n        return True\r\n    \r\n    @staticmethod\r\n    def create_memory_card(memory):\r\n        \"\"\"Create a styled HTML memory card\"\"\"\r\n        # Determine card style\r\n        bg_color, border_color = UiUtils.get_memory_colors(memory)\r\n        \r\n        # Format card HTML\r\n        card_html = f\"\"\"\r\n        <div style=\"\r\n            background-color: {bg_color};\r\n            border: 2px solid {border_color};\r\n            border-radius: 10px;\r\n            padding: 15px;\r\n            margin: 10px;\r\n            font-size: 10pt;\r\n        \">\r\n            <div style=\"font-weight: bold; color: #333;\">{memory.get('category', 'unknown').capitalize()}</div>\r\n            <div style=\"font-size: 12pt; margin-top: 8px;\">{memory.get('formatted_value', '')[:60]}</div>\r\n            <div style=\"font-size: 10pt; color: #666; margin-top: 8px;\">\r\n                {memory.get('timestamp', '').split(' ')[-1]}\r\n            </div>\r\n        </div>\r\n        \"\"\"\r\n        \r\n        return card_html\r\n\r\n    @staticmethod\r\n    def get_memory_colors(memory):\r\n        \"\"\"Determine colors based on memory content\"\"\"\r\n        if 'positive' in memory.get('tags', []):\r\n            return \"#E8F5E9\", \"#C8E6C9\"  # Green shades\r\n        elif 'negative' in memory.get('tags', []):\r\n            return \"#FFEBEE\", \"#FFCDD2\"   # Red shades\r\n        elif 'novelty' in memory.get('tags', []):\r\n            return \"#FFFDE7\", \"#FFF9C4\"   # Yellow shades\r\n        return \"#F5F5F5\", \"#EEEEEE\"       # Default gray\r\n\r\n    @staticmethod\r\n    def create_info_box(title, content, icon_path=None, bg_color=\"#f8f9fa\"):\r\n        \"\"\"Create a styled information box with optional icon\"\"\"\r\n        box = QtWidgets.QGroupBox(title)\r\n        box.setStyleSheet(f\"\"\"\r\n            QGroupBox {{\r\n                background-color: {bg_color};\r\n                border-radius: 8px;\r\n                border: 1px solid #dee2e6;\r\n                margin-top: 15px;\r\n                padding: 10px;\r\n                font-weight: bold;\r\n            }}\r\n            QGroupBox::title {{\r\n                subcontrol-origin: margin;\r\n                left: 10px;\r\n                padding: 0 5px;\r\n                color: #495057;\r\n            }}\r\n        \"\"\")\r\n        \r\n        box_layout = QtWidgets.QVBoxLayout(box)\r\n        \r\n        # Add icon if provided\r\n        if icon_path:\r\n            icon_label = QtWidgets.QLabel()\r\n            icon_label.setPixmap(QtGui.QPixmap(icon_path).scaled(24, 24, QtCore.Qt.KeepAspectRatio))\r\n            box_layout.addWidget(icon_label, alignment=QtCore.Qt.AlignRight)\r\n        \r\n        # Add content\r\n        content_label = QtWidgets.QLabel(content)\r\n        content_label.setTextFormat(QtCore.Qt.RichText)\r\n        content_label.setWordWrap(True)\r\n        content_label.setStyleSheet(\"font-weight: normal; color: #343a40;\")\r\n        box_layout.addWidget(content_label)\r\n        \r\n        return box\r\n\r\n# Utility functions\r\ndef darken_color(color, amount=20):\r\n    \"\"\"Darken a hex color by the specified amount\"\"\"\r\n    # Remove # if present\r\n    color = color.lstrip('#')\r\n    \r\n    # Convert to RGB\r\n    r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))\r\n    \r\n    # Darken\r\n    r = max(0, r - amount)\r\n    g = max(0, g - amount)\r\n    b = max(0, b - amount)\r\n    \r\n    # Convert back to hex\r\n    return f\"#{r:02x}{g:02x}{b:02x}\""
  },
  {
    "path": "src/brain_utils.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nclass ConsoleOutput:\r\n    def __init__(self, text_edit):\r\n        self.text_edit = text_edit\r\n\r\n    def write(self, text):\r\n        cursor = self.text_edit.textCursor()\r\n        format = QtGui.QTextCharFormat()\r\n\r\n        if text.startswith(\"Previous value:\"):\r\n            format.setForeground(QtGui.QColor(\"red\"))\r\n        elif text.startswith(\"New value:\"):\r\n            format.setForeground(QtGui.QColor(\"green\"))\r\n        else:\r\n            format.setForeground(QtGui.QColor(\"black\"))\r\n\r\n        cursor.insertText(text, format)\r\n        self.text_edit.setTextCursor(cursor)\r\n        self.text_edit.ensureCursorVisible()\r\n\r\n    def flush(self):\r\n        pass\r\n\r\n"
  },
  {
    "path": "src/brain_widget.py",
    "content": "import sys\r\nimport csv\r\nimport os\r\nimport re\r\nimport time\r\nimport math\r\nimport random\r\nimport numpy as np\r\nimport json\r\n\r\n# This is the biggest, messiest and most shameful of all the files in this project\r\n# I express my condolences to future devs  that curse the mighty brain_widget on late nights as I do\r\n\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom PyQt5.QtWidgets import QSplitter\r\nfrom PyQt5.QtGui import QPixmap, QFont, QImage\r\nfrom datetime import datetime\r\n\r\nfrom .brain_render_worker import BrainRenderWorker, create_render_state_from_widget, RenderState\r\nfrom .brain_worker import BrainWorker\r\nfrom .compute_backend import get_backend\r\nfrom .neurogenesis import EnhancedNeurogenesis, ExperienceBuffer\r\nfrom .brain_tooltips import EnhancedBrainTooltips\r\nfrom .brain_constants import CORE_NEURONS, INPUT_SENSORS, is_core_neuron\r\nfrom .personality import Personality\r\nfrom .learning import LearningConfig\r\nfrom .laboratory import NeuronLaboratory\r\nfrom .localisation import Localisation\r\nfrom typing import Dict, Optional\r\n\r\n# Brain state bridge for designer communication\r\ntry:\r\n    from .brain_state_bridge import (\r\n        export_brain_state,\r\n        set_game_running,\r\n        update_brain_state_from_widget\r\n    )\r\n    _HAS_BRAIN_BRIDGE = True\r\nexcept ImportError:\r\n    try:\r\n        # Try without relative import for standalone testing\r\n        from brain_state_bridge import (\r\n            export_brain_state,\r\n            set_game_running,\r\n            update_brain_state_from_widget\r\n        )\r\n        _HAS_BRAIN_BRIDGE = True\r\n    except ImportError:\r\n        _HAS_BRAIN_BRIDGE = False\r\n        print(\"[BrainWidget] Warning: brain_state_bridge not found, designer sync disabled\")\r\n\r\nfrom .animation_styles import (\r\n    AnimationStyle, VibrantStyle, SubtleStyle,\r\n    get_animation_style, get_available_styles, get_style_info,\r\n    ANIMATION_STYLES\r\n)\r\n\r\n# Performance tracking for Task Manager\r\ntry:\r\n    from .task_manager import perf_tracker\r\n    _PERF_TRACKING_AVAILABLE = True\r\nexcept ImportError:\r\n    _PERF_TRACKING_AVAILABLE = False\r\n\r\nclass BrainWidget(QtWidgets.QWidget):\r\n    \r\n    neuronClicked = QtCore.pyqtSignal(str)\r\n    animationStyleChanged = QtCore.pyqtSignal(str)  # Emitted when style changes\r\n    neuronCreated = QtCore.pyqtSignal(str)  # Emitted when neurogenesis creates a new neuron\r\n\r\n    def __init__(self, config=None, debug_mode=False, tamagotchi_logic=None,\r\n                 animation_style: str = \"vibrant\"):\r\n        self.resolution_scale = 1.0  # Default resolution scale\r\n        self.config = config if config else LearningConfig()\r\n        self._laboratory = None\r\n        self._last_lang = Localisation.instance().current_language\r\n        \r\n        # ===== ANIMATION STYLE INITIALIZATION =====\r\n        self._animation_style_name = animation_style\r\n        self._animation_style: AnimationStyle = get_animation_style(animation_style)\r\n        self.layers = []\r\n\r\n        # State update batching\r\n        self._pending_state_update = False\r\n        self.state_update_queue = []\r\n        \r\n        # Caches for neuron data\r\n        self.neurons = {}\r\n        self.state_colors = {}\r\n        \r\n        # Hover tracking\r\n        self.hovered_neuron = None\r\n        self.hovered_connection = None\r\n        self.hover_value_display_active = False\r\n        self.hover_value_opacity = 0.0\r\n        self.hover_value_animation_time = 0.0\r\n        self.hover_animation_time = 0\r\n        if not hasattr(self.config, 'hebbian'): #\r\n            self.config.hebbian = { #\r\n                'learning_interval': 30000, #\r\n                'weight_decay': 0.01, #\r\n                'active_threshold': 50 #\r\n            }\r\n        super().__init__() #\r\n\r\n        # Get neuron label font size from config (single source of truth)\r\n        display_config = self.config.get_display_config()\r\n        self.neuron_label_font_size = display_config['neuron_label_font_size']\r\n\r\n        # Tutorial glow effect properties\r\n        self.tutorial_glow_active = False\r\n        self.tutorial_glow_timer = None\r\n        self.tutorial_glow_animation = None\r\n        self._tutorial_glow_opacity = 0.0\r\n        self._link_fade_speeds = {}        # (src,dst) → fade speed per link\r\n        self._link_start_times = {}        # (src,dst) → when this link should start\r\n\r\n        from .neurogenesis_show import ShowmanNeurogenesis\r\n        real_engine                 = EnhancedNeurogenesis(self, config)\r\n        self.enhanced_neurogenesis  = ShowmanNeurogenesis(real_engine)\r\n        self.excluded_neurons = ['is_sick', 'is_eating', 'pursuing_food', 'direction', 'is_sleeping']\r\n        \r\n        # Build animation palette from selected style\r\n        self._build_animation_palette()\r\n        \r\n        self.hebbian_countdown_seconds = 30  # Default duration\r\n        self.learning_active = True #\r\n        self.pruning_enabled = True #\r\n        self.debug_mode = debug_mode  # Initialize debug_mode\r\n        self.is_paused = False #\r\n        self.weights = {}           # Init weights early\r\n        self.last_hebbian_time = time.time() #\r\n        self.last_neurogenesis_type = None\r\n        self.tamagotchi_logic = tamagotchi_logic #\r\n        self.recently_updated_neuron_pairs = [] #\r\n        self.neuron_shapes = {} #\r\n        \r\n\r\n        # Neural communication tracking system\r\n        self.communication_events = {} #\r\n        self.communication_highlight_duration = 0.5 #\r\n        self.weight_change_events = {} #\r\n        self.activity_duration = 0.5 #\r\n\r\n        # ADDED IN 2.4.5.0: Replace simple neurogenesis_data with enhanced system\r\n        self.enhanced_neurogenesis = EnhancedNeurogenesis(self, self.config)\r\n        self.experience_buffer = self.enhanced_neurogenesis.experience_buffer\r\n\r\n        # Ensure neurogenesis config exists\r\n        if not hasattr(self.config, 'neurogenesis'): #\r\n            self.config.neurogenesis = {\r\n                'decay_rate': 0.75,  # Default decay rate if not specified\r\n                'novelty_threshold': 3.0, #\r\n                'stress_threshold': 1.2, #\r\n                'reward_threshold': 3.5, #\r\n                'cooldown': 180, #\r\n                'highlight_duration': 5.0, #\r\n                'max_neurons': 50\r\n            }\r\n\r\n        self.neurogenesis_config = self.config.neurogenesis \r\n        self.neurogenesis_data = {\r\n            'new_neurons': [],\r\n            'last_neuron_time': time.time(),\r\n            'new_neurons_details': {}   # used by inspector / logging\r\n        }\r\n\r\n        # Neural state initialization\r\n        self.state = { #\r\n            \"can_see_food\": 0,\r\n            \"hunger\": 50, #\r\n            \"happiness\": 50, #\r\n            \"cleanliness\": 50, #\r\n            \"sleepiness\": 50, #\r\n            \"satisfaction\": 50, #\r\n            \"anxiety\": 50, #\r\n            \"curiosity\": 50, #\r\n            \"is_sick\": False, #\r\n            \"is_eating\": False, #\r\n            \"is_sleeping\": False, #\r\n            \"pursuing_food\": False, #\r\n            \"direction\": \"up\", #\r\n            \"position\": (0, 0), #\r\n            \"is_startled\": False, #\r\n            \"is_fleeing\": False, #\r\n            'neurogenesis_active': True\r\n        }\r\n\r\n        # Neuron position configuration\r\n        self.original_neuron_positions = { #\r\n            \"can_see_food\": (50, 200),\r\n            \"hunger\": (127, 81), #\r\n            \"happiness\": (361, 81), #\r\n            \"cleanliness\": (627, 81), #\r\n            \"sleepiness\": (840, 81), #\r\n            \"satisfaction\": (271, 380), #\r\n            \"anxiety\": (491, 389), #\r\n            \"curiosity\": (701, 386) #\r\n        }\r\n        self.neuron_positions = self.original_neuron_positions.copy() \r\n\r\n        # Randomize positions if configured ---\r\n        neuron_props = self.config.neurogenesis.get('neuron_properties', {})\r\n        if neuron_props.get('randomize_start_positions', False):\r\n            self._randomize_all_positions()\r\n\r\n        # Ensure connection to hunger exists (always)\r\n        self.weights[(\"can_see_food\", \"hunger\")] = 0.2  # Seeing food increases hunger slightly\r\n\r\n        # Random chance (50%) for happiness connection\r\n        if random.random() < 0.5:\r\n            self.weights[(\"can_see_food\", \"happiness\")] = 0.5  # Seeing food can increase happiness\r\n\r\n        # Track which neurons are visible (for animated reveal on new game)\r\n        self.visible_neurons = set()\r\n        # List of core neurons in reveal order\r\n        self.original_neurons = [\"can_see_food\", \"hunger\", \"happiness\", \"cleanliness\", \"sleepiness\", \r\n                                 \"satisfaction\", \"anxiety\", \"curiosity\"]\r\n        # Animation state for neuron reveals\r\n        self.neuron_reveal_animations = {}  # {neuron_name: {'start_time': float, 'progress': float}}\r\n        # --- link fade animation ---\r\n        self._link_opacities = {}          # (src,dst) → current opacity 0.0-1.0\r\n        self._link_targets   = {}          # (src,dst) → target opacity  0 or 1\r\n        self._link_fade_timer = QtCore.QTimer(self)\r\n        self._link_fade_timer.setInterval(16)          # ~60 FPS\r\n        self._link_fade_timer.timeout.connect(self._advance_link_fades)\r\n        self._auto_fade_links_pending = False \r\n        \r\n        # Tutorial mode flag\r\n        self.is_tutorial_mode = False\r\n        \r\n        # Track revealed neurons and connections for synced count display during animation\r\n        # NOTE: These should ONLY be used when is_tutorial_mode is True\r\n        self.revealed_neurons = set()\r\n        self.revealed_connections = set()\r\n\r\n        # Initialize communication events for all neurons\r\n        for neuron in self.neuron_positions.keys(): #\r\n            self.communication_events[neuron] = 0 #\r\n\r\n        # Animation control variables\r\n        self.animation_timer = QtCore.QTimer(self)\r\n        self.animation_timer.timeout.connect(self.update_animations)\r\n\r\n        # ===== PERFORMANCE FIX: Don't create BrainWorker here =====\r\n        # BrainWorker will be provided externally via set_brain_worker()\r\n        # This prevents multiple worker threads competing for CPU\r\n        self.brain_worker = None  # Will be set by SquidBrainWindow\r\n        print(\"🧵 BrainWorker will be set externally\")\r\n        \r\n        # Threading control flags\r\n        self._use_threaded_processing = True\r\n        self._pending_neurogenesis_check = False\r\n        self._pending_hebbian_learning = False\r\n\r\n        # NEUROGENESIS FIX: Start monitoring timer\r\n        self.neurogenesis_timer = QtCore.QTimer(self)\r\n        self.neurogenesis_timer.timeout.connect(self._periodic_neurogenesis_check)\r\n        self.neurogenesis_timer.start(2000)  # Check every 2 seconds\r\n        print(\"🧬 Neurogenesis monitoring timer started\")\r\n        \r\n        # Brain state bridge: export state for designer synchronization\r\n        if _HAS_BRAIN_BRIDGE:\r\n            self._brain_export_timer = QtCore.QTimer(self)\r\n            self._brain_export_timer.timeout.connect(self.export_brain_state_for_designer)\r\n            self._brain_export_timer.start(5000)  # Export every 5 seconds\r\n            set_game_running(True)  # Mark game as running\r\n            print(\"🔗 Brain state export enabled for designer sync\")\r\n        \r\n        self.animation_timer.start(40)  # 2.5.0.0 performance fix\r\n        self._last_animation_update = 0  # For throttling\r\n        \r\n        # ===== PERFORMANCE FIX: Cache for expensive paint objects =====\r\n        self._cached_fonts = {}\r\n        self._cached_pens = {}\r\n        \r\n        self.neuron_sizes = {}  # For smooth size transitions\r\n        self.weight_animations = []  # Track multiple weight changes\r\n        self.neurogenesis_highlight = {\r\n            'neuron': None,\r\n            'start_time': 0,\r\n            'duration': 5.0,\r\n            'pulse_phase': 0\r\n        }\r\n\r\n        # Add neurogenesis visualization tracking\r\n        self.neurogenesis_highlight = { #\r\n            'neuron': None, #\r\n            'start_time': 0, #\r\n            'duration': 5.0  # seconds\r\n        }\r\n\r\n        # Connection and weight initialization\r\n        self.connections = self.initialize_connections() #\r\n        self.initialize_weights()  # Populate weights\r\n        \r\n        self.show_links = True #\r\n        self.frozen_weights = None #\r\n        self.history = [] #\r\n        self.training_data = [] #\r\n        self.learning_rate = 0.1 #\r\n\r\n        # --- Compute backend (NumPy default, optional ONNX for NPU support) ---\r\n        self.compute_backend = get_backend()\r\n        self.associations = self.compute_backend.zeros(\r\n            (len(self.neuron_positions), len(self.neuron_positions))\r\n        ) #\r\n        self.capture_training_data_enabled = False #\r\n        self.dragging = False #\r\n        self.dragged_neuron = None #\r\n        self.drag_start_pos = None #\r\n        self.tooltip_manager = EnhancedBrainTooltips(self)\r\n        self.setMouseTracking(True)\r\n        self.show_weights = False\r\n\r\n        # Ensure connections to hunger and happiness exist with initial weights\r\n        self.weights[(\"can_see_food\", \"hunger\")] = 0.2  # Seeing food increases hunger slightly\r\n        self.weights[(\"can_see_food\", \"happiness\")] = 0.5\r\n        _link_fade_speed = 3.0   # opacity units per second (tweak for faster/slower)\r\n\r\n         # ===== OFFSCREEN RENDERING SETUP =====\r\n        # Initialize render worker for background rendering\r\n        self._render_worker = BrainRenderWorker(self)\r\n        self._render_worker.render_complete.connect(self._on_render_complete)\r\n        self._render_worker.start()\r\n        \r\n        # Cached rendered image\r\n        self._cached_render: Optional[QImage] = None\r\n        self._render_dirty = True\r\n        \r\n        # Render throttling\r\n        self._last_render_request = 0.0\r\n        self._render_interval = 1.0 / 10.0  # 10 FPS target\r\n        \r\n        # State change tracking for smart re-rendering\r\n        self._last_state_hash = None\r\n        \r\n        # Timer for periodic render requests (catches animation updates)\r\n        self._render_timer = QtCore.QTimer(self)\r\n        self._render_timer.timeout.connect(self._request_render_if_dirty)\r\n        self._render_timer.start(100)  # 10 FPS\r\n\r\n    def set_brain_worker(self, worker):\r\n        \"\"\"Accept an external BrainWorker instance.\"\"\"\r\n        \r\n        self.brain_worker = worker  # Always set the new worker\r\n        \r\n        # Reconnect signals (disconnect old ones first to avoid duplicates)\r\n        if hasattr(self, 'brain_worker'):\r\n            try:\r\n                self.brain_worker.neurogenesis_result.disconnect(self._on_neurogenesis_complete)\r\n                self.brain_worker.hebbian_result.disconnect(self._on_hebbian_complete)\r\n                self.brain_worker.state_update_result.disconnect(self._on_state_update_complete)\r\n                self.brain_worker.error_occurred.disconnect(self._on_worker_error)\r\n            except:\r\n                pass  # Ignore if signals weren't connected\r\n        \r\n        # Connect new signals\r\n        self.brain_worker.neurogenesis_result.connect(self._on_neurogenesis_complete)\r\n        self.brain_worker.hebbian_result.connect(self._on_hebbian_complete)\r\n        self.brain_worker.state_update_result.connect(self._on_state_update_complete)\r\n        self.brain_worker.error_occurred.connect(self._on_worker_error)\r\n        \r\n        print(\"🧵 BrainWidget received external BrainWorker\")\r\n\r\n    # =========================================================================\r\n    # BRAIN STATE BRIDGE METHODS (for designer synchronization)\r\n    # =========================================================================\r\n    def showEvent(self, event):\r\n        \"\"\"Ensure render is requested when widget becomes visible.\"\"\"\r\n        super().showEvent(event)\r\n        self.mark_render_dirty()\r\n        # Reset render throttle to force immediate update\r\n        self._last_render_request = 0\r\n        self._request_render()\r\n    \r\n    def export_brain_state_for_designer(self):\r\n        \"\"\"\r\n        Export current brain state to shared file for designer import.\r\n        Public method - can be called by NetworkTab before launching designer.\r\n        \"\"\"\r\n        if not _HAS_BRAIN_BRIDGE:\r\n            return\r\n        \r\n        try:\r\n            # Ensure the game lock exists\r\n            set_game_running(True)\r\n            \r\n            # Export the actual data\r\n            update_brain_state_from_widget(self)\r\n        except Exception as e:\r\n            # Silent failure - don't spam console\r\n            pass\r\n    \r\n    def cleanup_brain_bridge(self):\r\n        \"\"\"\r\n        Clean up brain bridge files when widget is destroyed.\r\n        Should be called when the game exits.\r\n        \"\"\"\r\n        if _HAS_BRAIN_BRIDGE:\r\n            try:\r\n                set_game_running(False)\r\n                print(\"🔗 Brain state bridge cleaned up\")\r\n            except Exception:\r\n                pass\r\n    \r\n    def closeEvent(self, event):\r\n        \"\"\"Handle widget close - clean up brain bridge.\"\"\"\r\n        self.cleanup_brain_bridge()\r\n        self._cleanup_render_worker()\r\n        super().closeEvent(event) if hasattr(super(), 'closeEvent') else None\r\n        event.accept()\r\n\r\n\r\n    def set_debug_mode(self, enabled):\r\n        \"\"\"Set debug mode without causing circular callbacks\"\"\"\r\n        # Only proceed if there's an actual change\r\n        if self.debug_mode == enabled:\r\n            return\r\n\r\n        # Update our own state\r\n        self.debug_mode = enabled\r\n\r\n        # Update brain widget\r\n        if hasattr(self, 'brain_widget'):\r\n            self.brain_widget.debug_mode = enabled\r\n\r\n        # Update tabs\r\n        for tab_name in ['network_tab', 'nn_viz_tab', 'memory_tab', 'decisions_tab', 'about_tab']:\r\n            if hasattr(self, tab_name):\r\n                tab = getattr(self, tab_name)\r\n                if hasattr(tab, 'debug_mode'):\r\n                    tab.debug_mode = enabled\r\n\r\n        # Enable/disable debug-specific UI elements\r\n        if hasattr(self, 'stimulate_button'):\r\n            self.stimulate_button.setEnabled(enabled)\r\n\r\n        print(f\"Brain window debug mode set to: {enabled}\")\r\n\r\n    \r\n    def _request_render_if_dirty(self):\r\n        \"\"\"Called by timer to request render if state has changed\"\"\"\r\n        if self._render_dirty or self._has_active_animations():\r\n            self._request_render()\r\n\r\n    def _has_active_animations(self) -> bool:\r\n        \"\"\"Check if there are active animations requiring re-render\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # Check communication events (connection animations)\r\n        for neuron, event_time in self.communication_events.items():\r\n            if current_time - event_time < 0.5:  # 500ms animation\r\n                return True\r\n        \r\n        # Check link fade animations\r\n        for key, opacity in getattr(self, '_link_opacities', {}).items():\r\n            target = getattr(self, '_link_targets', {}).get(key, opacity)\r\n            if abs(opacity - target) > 0.01:\r\n                return True\r\n        \r\n        return False\r\n    \r\n    def _request_render(self):\r\n        \"\"\"Request a new render from the worker thread\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # Throttle requests\r\n        if current_time - self._last_render_request < self._render_interval:\r\n            return\r\n        \r\n        self._last_render_request = current_time\r\n        \r\n        # Create state snapshot and send to worker\r\n        try:\r\n            state = create_render_state_from_widget(self)\r\n            self._render_worker.request_render(state)\r\n            self._render_dirty = False\r\n        except Exception as e:\r\n            print(f\"Error creating render state: {e}\")\r\n\r\n    def _on_render_complete(self, image: QImage, render_time: float):\r\n        \"\"\"Called when render worker completes a frame\"\"\"\r\n        self._cached_render = image\r\n        # Trigger a lightweight repaint to show the new image\r\n        self.update()\r\n\r\n    def mark_render_dirty(self):\r\n        \"\"\"Call this when state changes that require re-render\"\"\"\r\n        self._render_dirty = True\r\n\r\n    def _cleanup_render_worker(self):\r\n        \"\"\"Clean up the render worker - call on widget destruction\"\"\"\r\n        if hasattr(self, '_render_worker') and self._render_worker:\r\n            self._render_worker.stop()\r\n            self._render_worker.wait(1000)  # Wait up to 1 second\r\n            self._render_worker = None\r\n        \r\n        if hasattr(self, '_render_timer') and self._render_timer:\r\n            self._render_timer.stop()\r\n\r\n    # =========================================================================\r\n    # ANIMATION STYLE METHODS\r\n    # =========================================================================\r\n    \r\n    def _build_animation_palette(self):\r\n        \"\"\"\r\n        Load visual settings from the current animation style.\r\n        Called during init and when style changes.\r\n        \"\"\"\r\n        style = self._animation_style\r\n        \r\n        # Connection line settings\r\n        self.anim_line_base_width = style.line_base_width\r\n        self.anim_line_col_pos = style.line_colour_positive\r\n        self.anim_line_col_neg = style.line_colour_negative\r\n        self.anim_line_alpha = style.line_alpha\r\n        self.anim_use_thick_lines = style.use_thick_lines\r\n        \r\n        # [NEW] Weight-based thickness settings (MISSING IN PREVIOUS VERSION)\r\n        self.weight_thickness_enabled = getattr(style, 'weight_thickness_enabled', False)\r\n        self.weight_thickness_min = getattr(style, 'weight_thickness_min', 1.0)\r\n        self.weight_thickness_max = getattr(style, 'weight_thickness_max', 2.0)\r\n        self.weight_thickness_power = getattr(style, 'weight_thickness_power', 1.0)\r\n        \r\n        # Stress-anxiety connection\r\n        self.anim_stress_width = style.stress_anxiety_width\r\n        self.anim_stress_colour = style.stress_anxiety_colour\r\n        self.anim_stress_dashed = style.stress_anxiety_dashed\r\n        \r\n        # Pulse/travelling dot settings\r\n        self.anim_pulse_enabled = style.pulse_enabled\r\n        self.anim_pulse_colour = style.pulse_colour\r\n        self.anim_pulse_alpha = style.pulse_alpha\r\n        self.anim_pulse_duration = style.pulse_duration\r\n        self.anim_pulse_speed = style.pulse_speed\r\n        self.anim_pulse_diameter = style.pulse_diameter\r\n        \r\n        # Glow effect settings\r\n        self.anim_glow_enabled = style.glow_enabled\r\n        self.anim_glow_colour = style.glow_colour\r\n        self.anim_glow_alpha = style.glow_alpha\r\n        self.anim_glow_fade_threshold = style.glow_fade_threshold\r\n        \r\n        # Hover effect settings\r\n        self.anim_hover_enabled = style.hover_enabled\r\n        self.anim_hover_scale = style.hover_scale\r\n        self.anim_hover_duration = style.hover_animation_duration\r\n        \r\n        # Activity highlight settings\r\n        self.anim_activity_enabled = style.activity_highlight_enabled\r\n        self.anim_activity_colour = style.activity_highlight_colour\r\n        self.anim_activity_alpha = style.activity_highlight_alpha\r\n        self.anim_activity_pulse_speed = style.activity_pulse_speed\r\n        \r\n        # Neurogenesis highlight settings\r\n        self.anim_neurogenesis_colour = style.neurogenesis_highlight_colour\r\n        self.anim_neurogenesis_alpha = style.neurogenesis_highlight_alpha\r\n        self.anim_neurogenesis_duration = style.neurogenesis_highlight_duration\r\n        \r\n        # Background color\r\n        self.anim_background_colour = style.background_colour\r\n        \r\n        # ===== VIBRANT STYLE: AMBIENT PULSING =====\r\n        self.anim_ambient_pulse_enabled = style.ambient_pulse_enabled\r\n        self.anim_ambient_pulse_width_range = style.ambient_pulse_width_range\r\n        self.anim_ambient_pulse_alpha_range = style.ambient_pulse_alpha_range\r\n        self.anim_ambient_pulse_freq_range = style.ambient_pulse_freq_range\r\n        self.anim_ambient_pulse_phase_drift = style.ambient_pulse_phase_drift\r\n        \r\n        # ===== SUBTLE STYLE: COMMUNICATION GLOWS =====\r\n        self.anim_comm_glow_enabled = style.comm_glow_enabled\r\n        self.anim_comm_glow_colour = style.comm_glow_colour\r\n        self.anim_comm_glow_alpha = style.comm_glow_alpha\r\n        self.anim_comm_glow_size = style.comm_glow_size\r\n        self.anim_comm_glow_tail_length = style.comm_glow_tail_length\r\n        self.anim_comm_glow_speed_range = style.comm_glow_speed_range\r\n        self.anim_comm_glow_fade_in = style.comm_glow_fade_in\r\n        self.anim_comm_glow_fade_out = style.comm_glow_fade_out\r\n        self.anim_comm_glow_spawn_on_activity = style.comm_glow_spawn_on_activity\r\n        self.anim_comm_glow_spawn_on_weight_change = style.comm_glow_spawn_on_weight_change\r\n        self.anim_comm_glow_max_per_connection = style.comm_glow_max_per_connection\r\n\r\n        # ===== SUBTLE STYLE: SCROLLING DOTS =====\r\n        # [NEW] Ensure scrolling settings are mapped\r\n        self.anim_scroll_enabled = getattr(style, 'scroll_enabled', False)\r\n        self.anim_scroll_dot_count = getattr(style, 'scroll_dot_count', 3)\r\n        self.anim_scroll_dot_size = getattr(style, 'scroll_dot_size', 6.0)\r\n        self.anim_scroll_dot_colour = getattr(style, 'scroll_dot_colour', (255, 255, 255))\r\n        self.anim_scroll_dot_alpha = getattr(style, 'scroll_dot_alpha', 200)\r\n        self.anim_scroll_speed_range = getattr(style, 'scroll_speed_range', (1.5, 4.0))\r\n        \r\n        # ===== NEURAL STYLE: ACTIVATION PULSES =====\r\n        self.anim_neural_pulse_enabled = style.neural_pulse_enabled\r\n        self.anim_neural_pulse_duration = style.neural_pulse_duration\r\n        self.anim_neural_pulse_width = style.neural_pulse_width\r\n        self.anim_neural_pulse_colour_pos = style.neural_pulse_colour_positive\r\n        self.anim_neural_pulse_colour_neg = style.neural_pulse_colour_negative\r\n        self.anim_neural_weight_thickness = style.neural_weight_thickness\r\n        self.anim_neural_weight_thickness_mult = style.neural_weight_thickness_mult\r\n        self.anim_neural_base_colour_pos = style.neural_base_colour_positive\r\n        self.anim_neural_base_colour_neg = style.neural_base_colour_negative\r\n        self.anim_neural_base_alpha = style.neural_base_alpha\r\n        \r\n        # Initialize style-specific animation state\r\n        self._init_style_animation_state()\r\n        \r\n        print(f\"🎨 Animation palette built for style: {style.display_name}\")\r\n\r\n    def is_binary_neuron(self, neuron_name: str) -> bool:\r\n        \"\"\"Return True if neuron represents a binary (on/off) state.\"\"\"\r\n        binary_neurons = {\r\n            'can_see_food',\r\n            'is_eating',\r\n            'is_sleeping',\r\n            'is_sick',\r\n            'pursuing_food',\r\n            'is_fleeing',\r\n            'is_startled',\r\n            'external_stimulus',   # usually high/low, treat as binary-ish\r\n            'plant_proximity',     # 2.6.0.2 consider this could be analog\r\n        }\r\n        return neuron_name in binary_neurons\r\n    \r\n    def get_animation_style(self) -> str:\r\n        \"\"\"Get the current animation style name.\"\"\"\r\n        return self._animation_style_name\r\n    \r\n    def get_animation_style_info(self) -> tuple:\r\n        \"\"\"Get (name, display_name, description) for current style.\"\"\"\r\n        style = self._animation_style\r\n        return (style.name, style.display_name, style.description)\r\n    \r\n    def set_animation_style(self, style_name: str) -> bool:\r\n        \"\"\"\r\n        Switch to a different animation style at runtime.\r\n        \r\n        Args:\r\n            style_name: One of 'classic', 'vibrant', or 'subtle'\r\n            \r\n        Returns:\r\n            True if style was changed, False if style name is invalid\r\n        \"\"\"\r\n        try:\r\n            new_style = get_animation_style(style_name)\r\n        except KeyError:\r\n            print(f\"⚠️ Unknown animation style: {style_name}. \"\r\n                  f\"Available: {get_available_styles()}\")\r\n            return False\r\n        \r\n        if style_name == self._animation_style_name:\r\n            return True  # Already using this style\r\n        \r\n        old_style = self._animation_style_name\r\n        self._animation_style_name = style_name\r\n        self._animation_style = new_style\r\n        self._build_animation_palette()\r\n        \r\n        # Trigger repaint with new style\r\n        self.update()\r\n        \r\n        # Emit signal for any listeners\r\n        self.animationStyleChanged.emit(style_name)\r\n        \r\n        print(f\"🎨 Animation style changed: {old_style} → {style_name}\")\r\n        return True\r\n    \r\n    @staticmethod\r\n    def get_available_animation_styles() -> list:\r\n        \"\"\"Get list of available animation style names.\"\"\"\r\n        return get_available_styles()\r\n    \r\n    @staticmethod\r\n    def get_animation_styles_info() -> list:\r\n        \"\"\"Get list of (name, display_name, description) for all styles.\"\"\"\r\n        return get_style_info()\r\n\r\n    def _init_style_animation_state(self):\r\n        \"\"\"\r\n        Initialize state tracking for style-specific animations.\r\n        Called when style changes or on startup.\r\n        \"\"\"\r\n        # ===== VIBRANT: Ambient pulse state =====\r\n        # Each connection gets its own phase and frequency for organic pulsing\r\n        if not hasattr(self, '_ambient_pulse_state'):\r\n            self._ambient_pulse_state = {}  # key: (src, dst) -> {phase, freq, phase_drift_dir}\r\n        \r\n        # ===== SUBTLE: Communication glow state =====\r\n        # Each connection has a list of traveling glow packets\r\n        if not hasattr(self, '_comm_glow_packets'):\r\n            self._comm_glow_packets = {}  # key: (src, dst) -> [{progress, speed, direction}]\r\n        \r\n        # Track last spawn times to prevent glow spam\r\n        if not hasattr(self, '_comm_glow_last_spawn'):\r\n            self._comm_glow_last_spawn = {}  # key: (src, dst) -> timestamp\r\n        \r\n        # ===== NEURAL: Activation pulse state =====\r\n        # Tracks traveling activation pulses for the neural style\r\n        if not hasattr(self, '_neural_pulses'):\r\n            self._neural_pulses = {}  # key: (src, dst) -> [{start_time, color}]\r\n    \r\n    def _get_or_create_ambient_pulse(self, conn_key):\r\n        \"\"\"\r\n        Get or create ambient pulse state for a connection.\r\n        Each connection pulses at its own unique rate.\r\n        \"\"\"\r\n        if conn_key not in self._ambient_pulse_state:\r\n            # Assign random frequency and starting phase\r\n            freq_min, freq_max = self.anim_ambient_pulse_freq_range\r\n            self._ambient_pulse_state[conn_key] = {\r\n                'phase': random.uniform(0, 2 * math.pi),  # Random starting phase\r\n                'freq': random.uniform(freq_min, freq_max),  # Random frequency\r\n                'phase_drift_dir': random.choice([-1, 1]),  # Drift direction\r\n                'drift_timer': random.uniform(0, 5)  # When to change drift\r\n            }\r\n        return self._ambient_pulse_state[conn_key]\r\n    \r\n    def _update_ambient_pulses(self, dt):\r\n        \"\"\"\r\n        Update all ambient pulse phases. Called from update_animations().\r\n        Each connection's phase advances at its own rate with subtle drift.\r\n        \"\"\"\r\n        if not self.anim_ambient_pulse_enabled:\r\n            return\r\n        \r\n        for conn_key, state in self._ambient_pulse_state.items():\r\n            # Advance phase based on frequency\r\n            state['phase'] += 2 * math.pi * state['freq'] * dt\r\n            \r\n            # Keep phase in reasonable range\r\n            if state['phase'] > 100 * math.pi:\r\n                state['phase'] -= 100 * math.pi\r\n            \r\n            # Occasionally change drift direction for organic feel\r\n            state['drift_timer'] -= dt\r\n            if state['drift_timer'] <= 0:\r\n                state['phase_drift_dir'] = random.choice([-1, 1])\r\n                state['drift_timer'] = random.uniform(3, 8)\r\n            \r\n            # Apply subtle phase drift\r\n            state['phase'] += state['phase_drift_dir'] * self.anim_ambient_pulse_phase_drift * dt\r\n    \r\n    def _spawn_comm_glow(self, source, target):\r\n        \"\"\"\r\n        Spawn a new communication glow packet traveling from source to target.\r\n        \"\"\"\r\n        if not self.anim_comm_glow_enabled:\r\n            return\r\n        \r\n        conn_key = (source, target)\r\n        current_time = time.time()\r\n        \r\n        # Check spawn cooldown (min 0.1 seconds between spawns on same connection)\r\n        last_spawn = self._comm_glow_last_spawn.get(conn_key, 0)\r\n        if current_time - last_spawn < 0.1:\r\n            return\r\n        \r\n        # Initialize packet list if needed\r\n        if conn_key not in self._comm_glow_packets:\r\n            self._comm_glow_packets[conn_key] = []\r\n        \r\n        # Check max packets per connection\r\n        if len(self._comm_glow_packets[conn_key]) >= self.anim_comm_glow_max_per_connection:\r\n            return\r\n        \r\n        # Create new packet with random speed (for chaotic, organic movement)\r\n        speed_min, speed_max = self.anim_comm_glow_speed_range\r\n        duration = random.uniform(speed_min, speed_max)\r\n        \r\n        self._comm_glow_packets[conn_key].append({\r\n            'progress': 0.0,  # 0 = at source, 1 = at target\r\n            'speed': 1.0 / duration,  # Progress per second\r\n            'start_time': current_time\r\n        })\r\n        \r\n        self._comm_glow_last_spawn[conn_key] = current_time\r\n\r\n    def find_orphan_neurons(self):\r\n        \"\"\"Find neurons with no connections in the weights dictionary\"\"\"\r\n        orphans = []\r\n        \r\n        # Define neurons that should be checked even if they are in excluded_neurons\r\n        explicitly_allowed = {'can_see_food', 'plant_proximity', 'is_fleeing'}\r\n        \r\n        for neuron in self.neuron_positions.keys():\r\n            # Skip if the neuron is excluded, unless it's in our allowed list\r\n            if neuron in self.excluded_neurons and neuron not in explicitly_allowed:\r\n                continue\r\n            \r\n            # Check for any connection (incoming or outgoing)\r\n            has_connection = any(src == neuron or dst == neuron \r\n                                for (src, dst) in self.weights.keys())\r\n            \r\n            if not has_connection:\r\n                orphans.append(neuron)\r\n                \r\n        return orphans\r\n\r\n    \r\n    def _update_comm_glows(self, dt):\r\n        \"\"\"\r\n        Update all communication glow packets. Called from update_animations().\r\n        \"\"\"\r\n        if not self.anim_comm_glow_enabled:\r\n            return\r\n        \r\n        # Update each connection's glow packets\r\n        for conn_key in list(self._comm_glow_packets.keys()):\r\n            packets = self._comm_glow_packets[conn_key]\r\n            \r\n            # Update each packet\r\n            surviving_packets = []\r\n            for packet in packets:\r\n                packet['progress'] += packet['speed'] * dt\r\n                \r\n                # Keep packet if not yet complete\r\n                if packet['progress'] < 1.0:\r\n                    surviving_packets.append(packet)\r\n            \r\n            # Update packet list (remove completed ones)\r\n            if surviving_packets:\r\n                self._comm_glow_packets[conn_key] = surviving_packets\r\n            else:\r\n                del self._comm_glow_packets[conn_key]\r\n    \r\n    def _spawn_activity_glows(self, current_time):\r\n        \"\"\"\r\n        Spawn communication glows based on neuron activity.\r\n        Called from update_animations() when in subtle mode.\r\n        \"\"\"\r\n        if not self.anim_comm_glow_spawn_on_activity:\r\n            return\r\n        \r\n        # Check which neurons are currently active (significant deviation from baseline)\r\n        active_neurons = []\r\n        for neuron, value in self.state.items():\r\n            if isinstance(value, (int, float)) and neuron in self.neuron_positions:\r\n                if neuron not in self.excluded_neurons:\r\n                    # Consider active if significantly away from baseline (50)\r\n                    if abs(value - 50) > 25:\r\n                        active_neurons.append(neuron)\r\n        \r\n        # For active neurons, randomly spawn glows on their connections\r\n        for neuron in active_neurons:\r\n            # Random chance to spawn (don't spam every frame)\r\n            if random.random() > 0.02:  # ~2% chance per frame per active neuron\r\n                continue\r\n            \r\n            # Find connections involving this neuron\r\n            for (src, dst), weight in self.weights.items():\r\n                if src == neuron or dst == neuron:\r\n                    # Determine glow direction based on weight sign and which end is active\r\n                    if src == neuron:\r\n                        self._spawn_comm_glow(src, dst)\r\n                    else:\r\n                        self._spawn_comm_glow(dst, src)\r\n    \r\n    def _draw_comm_glows_for_connection(self, painter, scale, conn_key, start_point, end_point):\r\n        \"\"\"\r\n        Draw all communication glow packets traveling along a connection.\r\n        Each glow is a soft glowing orb that travels from source to target.\r\n        \"\"\"\r\n        # Check both directions for packets\r\n        reverse_key = (conn_key[1], conn_key[0])\r\n        \r\n        for key, direction in [(conn_key, 1), (reverse_key, -1)]:\r\n            if key not in self._comm_glow_packets:\r\n                continue\r\n            \r\n            packets = self._comm_glow_packets[key]\r\n            \r\n            for packet in packets:\r\n                progress = packet['progress']\r\n                \r\n                # Calculate fade based on position (fade in at start, fade out at end)\r\n                if progress < self.anim_comm_glow_fade_in:\r\n                    # Fade in\r\n                    alpha_mult = progress / self.anim_comm_glow_fade_in\r\n                elif progress > (1.0 - self.anim_comm_glow_fade_out):\r\n                    # Fade out\r\n                    alpha_mult = (1.0 - progress) / self.anim_comm_glow_fade_out\r\n                else:\r\n                    alpha_mult = 1.0\r\n                \r\n                # Clamp alpha multiplier\r\n                alpha_mult = max(0.0, min(1.0, alpha_mult))\r\n                \r\n                # Calculate position along the line\r\n                if direction == 1:\r\n                    # Forward direction\r\n                    glow_x = start_point.x() + progress * (end_point.x() - start_point.x())\r\n                    glow_y = start_point.y() + progress * (end_point.y() - start_point.y())\r\n                else:\r\n                    # Reverse direction (packet traveling backward)\r\n                    glow_x = end_point.x() + progress * (start_point.x() - end_point.x())\r\n                    glow_y = end_point.y() + progress * (start_point.y() - end_point.y())\r\n                \r\n                glow_pos = QtCore.QPointF(glow_x, glow_y)\r\n                \r\n                # Draw the glow with multiple layers for soft appearance\r\n                glow_size = self.anim_comm_glow_size * scale\r\n                r, g, b = self.anim_comm_glow_colour\r\n                \r\n                # Outer glow (larger, more transparent)\r\n                outer_alpha = int(self.anim_comm_glow_alpha * 0.3 * alpha_mult)\r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(r, g, b, outer_alpha)))\r\n                painter.setPen(QtCore.Qt.NoPen)\r\n                painter.drawEllipse(glow_pos, glow_size * 1.5, glow_size * 1.5)\r\n                \r\n                # Middle glow\r\n                mid_alpha = int(self.anim_comm_glow_alpha * 0.6 * alpha_mult)\r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(r, g, b, mid_alpha)))\r\n                painter.drawEllipse(glow_pos, glow_size, glow_size)\r\n                \r\n                # Inner core (brightest)\r\n                inner_alpha = int(self.anim_comm_glow_alpha * alpha_mult)\r\n                # Slightly whiter core\r\n                core_r = min(255, r + 50)\r\n                core_g = min(255, g + 50)\r\n                core_b = min(255, b + 50)\r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(core_r, core_g, core_b, inner_alpha)))\r\n                painter.drawEllipse(glow_pos, glow_size * 0.5, glow_size * 0.5)\r\n                \r\n                # Draw tail (trailing glow particles)\r\n                if self.anim_comm_glow_tail_length > 0:\r\n                    tail_steps = 3\r\n                    for i in range(1, tail_steps + 1):\r\n                        tail_progress = progress - (self.anim_comm_glow_tail_length * i / tail_steps)\r\n                        if tail_progress < 0:\r\n                            continue\r\n                        \r\n                        # Calculate tail position\r\n                        if direction == 1:\r\n                            tail_x = start_point.x() + tail_progress * (end_point.x() - start_point.x())\r\n                            tail_y = start_point.y() + tail_progress * (end_point.y() - start_point.y())\r\n                        else:\r\n                            tail_x = end_point.x() + tail_progress * (start_point.x() - end_point.x())\r\n                            tail_y = end_point.y() + tail_progress * (start_point.y() - end_point.y())\r\n                        \r\n                        tail_pos = QtCore.QPointF(tail_x, tail_y)\r\n                        \r\n                        # Tail gets smaller and more transparent\r\n                        tail_size = glow_size * 0.6 * (1.0 - i / (tail_steps + 1))\r\n                        tail_alpha = int(outer_alpha * (1.0 - i / (tail_steps + 1)))\r\n                        \r\n                        painter.setBrush(QtGui.QBrush(QtGui.QColor(r, g, b, tail_alpha)))\r\n                        painter.drawEllipse(tail_pos, tail_size, tail_size)\r\n    \r\n    def _spawn_neural_pulse(self, source, target, color=None):\r\n        \"\"\"\r\n        Spawn a neural activation pulse traveling from source to target.\r\n        Used by the Neural style.\r\n        \"\"\"\r\n        if not self.anim_neural_pulse_enabled:\r\n            return\r\n        \r\n        conn_key = (source, target)\r\n        current_time = time.time()\r\n        \r\n        # Initialize pulse list if needed\r\n        if conn_key not in self._neural_pulses:\r\n            self._neural_pulses[conn_key] = []\r\n        \r\n        # Limit concurrent pulses per connection\r\n        if len(self._neural_pulses[conn_key]) >= 3:\r\n            return\r\n        \r\n        # Determine pulse color based on weight if not specified\r\n        if color is None:\r\n            weight = self.weights.get(conn_key, self.weights.get((target, source), 0))\r\n            if weight >= 0:\r\n                color = self.anim_neural_pulse_colour_pos\r\n            else:\r\n                color = self.anim_neural_pulse_colour_neg\r\n        \r\n        self._neural_pulses[conn_key].append({\r\n            'start_time': current_time,\r\n            'color': color\r\n        })\r\n    \r\n    def _update_neural_pulses(self, current_time):\r\n        \"\"\"\r\n        Update neural activation pulses, removing completed ones.\r\n        \"\"\"\r\n        if not self.anim_neural_pulse_enabled:\r\n            return\r\n        \r\n        duration = self.anim_neural_pulse_duration\r\n        \r\n        for conn_key in list(self._neural_pulses.keys()):\r\n            pulses = self._neural_pulses[conn_key]\r\n            \r\n            # Filter out completed pulses\r\n            surviving = [p for p in pulses if (current_time - p['start_time']) < duration]\r\n            \r\n            if surviving:\r\n                self._neural_pulses[conn_key] = surviving\r\n            else:\r\n                del self._neural_pulses[conn_key]\r\n    \r\n    def _spawn_neural_pulses_from_activity(self, current_time):\r\n        \"\"\"\r\n        Spawn neural pulses based on neuron activity and weight changes.\r\n        \"\"\"\r\n        if not self.anim_neural_pulse_enabled:\r\n            return\r\n        \r\n        # Spawn pulses from weight change events\r\n        if hasattr(self, 'weight_change_events'):\r\n            for neuron, event_time in list(self.weight_change_events.items()):\r\n                # Only process recent events (within last 0.1 seconds)\r\n                if current_time - event_time > 0.1:\r\n                    continue\r\n                \r\n                # Find connections from this neuron\r\n                for (src, dst), weight in self.weights.items():\r\n                    if src == neuron:\r\n                        self._spawn_neural_pulse(src, dst)\r\n    \r\n    def _draw_neural_connections(self, painter, scale):\r\n        \"\"\"\r\n        Draw connections in Neural style with weight-based thickness\r\n        and traveling activation pulses.\r\n        \"\"\"\r\n        if not self.show_links:\r\n            return\r\n        \r\n        current_time = time.time()\r\n        duration = self.anim_neural_pulse_duration\r\n        \r\n        # Pre-calculate scaled positions\r\n        scaled_positions = {}\r\n        for name, (x, y) in self.neuron_positions.items():\r\n            if name not in self.excluded_neurons:\r\n                scaled_positions[name] = (x * scale, y * scale)\r\n        \r\n        # === 1. Draw all static connections (base layer) ===\r\n        for (src, dst), weight in self.weights.items():\r\n            if src not in scaled_positions or dst not in scaled_positions:\r\n                continue\r\n            \r\n            x1, y1 = scaled_positions[src]\r\n            x2, y2 = scaled_positions[dst]\r\n            \r\n            # Base opacity from toggle fade system\r\n            base_opacity = self._link_opacities.get((src, dst), 1.0)\r\n            if base_opacity <= 0:\r\n                continue\r\n            \r\n            # Weight-based thickness\r\n            if self.anim_neural_weight_thickness:\r\n                thickness = max(1.0, abs(weight) * self.anim_neural_weight_thickness_mult)\r\n            else:\r\n                thickness = self.anim_line_base_width\r\n            pen_width = max(1, int(thickness * scale))\r\n            \r\n            # Color based on weight sign\r\n            alpha = int(self.anim_neural_base_alpha * base_opacity)\r\n            if weight >= 0:\r\n                r, g, b = self.anim_neural_base_colour_pos\r\n            else:\r\n                r, g, b = self.anim_neural_base_colour_neg\r\n            \r\n            color = QtGui.QColor(r, g, b, alpha)\r\n            pen = QtGui.QPen(color, pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap)\r\n            painter.setPen(pen)\r\n            painter.drawLine(QtCore.QLineF(x1, y1, x2, y2))\r\n        \r\n        # === 2. Draw activation pulses on top (traveling lights) ===\r\n        for (src, dst), pulses in self._neural_pulses.items():\r\n            if src not in scaled_positions or dst not in scaled_positions:\r\n                continue\r\n            \r\n            x1, y1 = scaled_positions[src]\r\n            x2, y2 = scaled_positions[dst]\r\n            \r\n            for pulse in pulses:\r\n                elapsed = current_time - pulse['start_time']\r\n                progress = elapsed / duration\r\n                \r\n                if progress > 1.0:\r\n                    continue\r\n                \r\n                # Sinusoidal fade (glow in → peak → fade out)\r\n                opacity = math.sin(progress * math.pi)\r\n                \r\n                # Get pulse color\r\n                r, g, b = pulse['color']\r\n                final_alpha = int(255 * opacity * 0.95)\r\n                \r\n                pulse_color = QtGui.QColor(r, g, b, final_alpha)\r\n                \r\n                # Thick glowing pulse line\r\n                pulse_width = max(1, int(self.anim_neural_pulse_width * scale))\r\n                pen = QtGui.QPen(pulse_color, pulse_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap)\r\n                painter.setPen(pen)\r\n                \r\n                # Traveling light effect - pulse moves along connection\r\n                segment_progress = progress * 1.35\r\n                if segment_progress <= 1.0:\r\n                    # First phase: light travels from source toward destination\r\n                    t = min(1.0, segment_progress)\r\n                    px = x1 + t * (x2 - x1)\r\n                    py = y1 + t * (y2 - y1)\r\n                    painter.drawLine(QtCore.QLineF(x1, y1, px, py))\r\n                else:\r\n                    # Second phase: light \"drains\" toward destination\r\n                    t = min(1.0, segment_progress - 0.35)\r\n                    px = x1 + t * (x2 - x1)\r\n                    py = y1 + t * (y2 - y1)\r\n                    painter.drawLine(QtCore.QLineF(px, py, x2, y2))\r\n\r\n\r\n    # =========================================================================\r\n    # Worker thread signal handlers\r\n    # =========================================================================\r\n    \r\n    def _on_neurogenesis_complete(self, result: dict):\r\n        \"\"\"Handle neurogenesis result from worker thread (called on main thread).\r\n        \r\n        The worker thread performs preliminary checks and signals whether creation\r\n        should proceed. Actual neuron creation happens here on the main thread\r\n        via the EnhancedNeurogenesis system.\r\n        \"\"\"\r\n        self._pending_neurogenesis_check = False\r\n        \r\n        # Check if worker signaled that we should create a neuron\r\n        if not result.get('should_create', False):\r\n            return\r\n        \r\n        # Get the context from worker result\r\n        neuron_type = result.get('neuron_type')\r\n        trigger_value = result.get('trigger_value', 0)\r\n        state_context = result.get('state_context', {})\r\n        is_emergency = result.get('is_emergency', False)\r\n        \r\n        if not neuron_type:\r\n            return\r\n        \r\n        # print(f\"🧵 Worker signaled neurogenesis: type={neuron_type}, emergency={is_emergency}\")\r\n        \r\n        # Build environment from state context\r\n        environment = {\r\n            'food_count': state_context.get('food_count', 0),\r\n            'poop_count': state_context.get('poop_count', 0),\r\n            'is_sick': state_context.get('is_sick', False),\r\n            'is_eating': state_context.get('is_eating', False),\r\n            'has_rock': state_context.get('carrying_rock', False),\r\n            'new_object_encountered': state_context.get('new_object_encountered', False),\r\n            'recent_positive_outcome': state_context.get('recent_positive_outcome', False)\r\n        }\r\n        \r\n        # Use the EnhancedNeurogenesis system to create the neuron properly\r\n        try:\r\n            # Capture experience context\r\n            context = self.enhanced_neurogenesis.capture_experience_context(\r\n                trigger_type=neuron_type,\r\n                brain_state=state_context,\r\n                recent_actions=state_context.get('recent_actions', []),\r\n                environment=environment\r\n            )\r\n            \r\n            # For emergency creation, skip the normal checks\r\n            if is_emergency:\r\n                neuron_name = self.enhanced_neurogenesis.create_functional_neuron(context, is_emergency=True)\r\n            else:\r\n                # Normal path: check if we should create based on pattern recurrence\r\n                if self.enhanced_neurogenesis.should_create_neuron(context):\r\n                    neuron_name = self.enhanced_neurogenesis.create_functional_neuron(context)\r\n                else:\r\n                    # Experience captured but not enough pattern recurrence yet\r\n                    return\r\n            \r\n            if neuron_name:\r\n                print(f\"✨ Main thread: Created neuron {neuron_name}\")\r\n                \r\n                # Emit signal to notify listeners (e.g., NetworkTab) of new neuron\r\n                self.neuronCreated.emit(neuron_name)\r\n                \r\n                # Handle pruning if needed\r\n                if self.pruning_enabled:\r\n                    current_count = len(self.neuron_positions) - len(self.excluded_neurons)\r\n                    max_neurons = self.neurogenesis_config.get('max_neurons', 32)\r\n                    if current_count > max_neurons * 0.85:\r\n                        self.enhanced_neurogenesis.intelligent_pruning()\r\n                \r\n                self.update()\r\n                \r\n        except Exception as e:\r\n            print(f\"⚠️ Error during neuron creation on main thread: {e}\")\r\n            import traceback\r\n            traceback.print_exc()\r\n            \r\n    def _on_hebbian_complete(self, result: dict):\r\n        \"\"\"Handle Hebbian learning result from worker thread (called on main thread).\"\"\"\r\n        self._pending_hebbian_learning = False\r\n        \r\n        # Always reset the timer so countdown restarts properly\r\n        self.last_hebbian_time = time.time()\r\n        \r\n        updated_pairs = result.get('updated_pairs', [])\r\n        weight_updates = result.get('weight_updates', {})\r\n        \r\n        if not weight_updates:\r\n            self.update()\r\n            return\r\n            \r\n        # [NEW] Pre-generate distinct colors for this batch to ensure uniqueness\r\n        import random\r\n        from PyQt5.QtGui import QColor\r\n        \r\n        new_connections_created = []  # Track newly formed connections\r\n        \r\n        # Apply the weight updates (main thread only)\r\n        for i, (pair_str, update_data) in enumerate(weight_updates.items()):\r\n            # Convert string key back to tuple if needed\r\n            if isinstance(pair_str, str):\r\n                pair = tuple(pair_str.split(','))\r\n            else:\r\n                pair = pair_str\r\n            \r\n            # Check if this is a new connection being created by Hebbian learning\r\n            is_new_connection = update_data.get('is_new_connection', False)\r\n            \r\n            if is_new_connection and pair not in self.weights:\r\n                # Create the new connection - Hebbian learning forming new pathways!\r\n                self.weights[pair] = 0.0  # Initialize with zero weight\r\n                new_connections_created.append(pair)\r\n                \r\n            if pair in self.weights:\r\n                old_weight = update_data['old_weight']\r\n                new_weight = update_data['new_weight']\r\n                self.weights[pair] = new_weight\r\n                \r\n                # [NEW] Generate unique vibrant color using HSL (Hue, Saturation, Lightness)\r\n                # Offset hue by index to ensure pairs get different colors\r\n                hue = (random.random() + (i * 0.618033988749895)) % 1.0  # Golden ratio spacing\r\n                unique_color = QColor.fromHslF(hue, 0.95, 0.6).toRgb()\r\n                color_tuple = (unique_color.red(), unique_color.green(), unique_color.blue())\r\n                \r\n                # [NEW] Randomize speed (duration) between 0.8s (fast) and 2.0s (slow)\r\n                unique_duration = random.uniform(0.8, 2.0)\r\n                \r\n                # Add animation for visual feedback with custom parameters\r\n                n1, n2 = pair\r\n                self.add_weight_animation(\r\n                    n1, n2, old_weight, new_weight, \r\n                    custom_color=color_tuple, \r\n                    custom_duration=unique_duration\r\n                )\r\n                \r\n        # Update tracking (convert lists to tuples for consistency)\r\n        self.recently_updated_neuron_pairs = [tuple(p) if isinstance(p, list) else p for p in updated_pairs]\r\n        self.last_hebbian_time = time.time()\r\n        \r\n        # Console output\r\n        if updated_pairs:\r\n            COLORS = [\"\\033[96m\", \"\\033[93m\", \"\\033[95m\", \"\\033[92m\", \"\\033[94m\"]\r\n            RESET = \"\\033[0m\"\r\n            colored = []\r\n            for i, pair in enumerate(updated_pairs):\r\n                if isinstance(pair, (list, tuple)) and len(pair) == 2:\r\n                    a, b = pair\r\n                    color = COLORS[i % len(COLORS)]\r\n                    # Mark new connections with a ✨\r\n                    marker = \" ✨\" if pair in new_connections_created else \"\"\r\n                    colored.append(f\"{color}{a} ↔ {b}{marker}{RESET}\")\r\n            if colored:\r\n                print(\"     Hebbian learning chosen pairs: \" + \"  \".join(colored))\r\n        \r\n        # Log new connections formed\r\n        if new_connections_created:\r\n            print(f\"     🔗 New connections formed: {len(new_connections_created)}\")\r\n            # Sync connections list from weights after creating new connections\r\n            self.sync_connections_from_weights()\r\n            \r\n        self.update()\r\n        \r\n\r\n    def _on_state_update_complete(self, result: dict):\r\n        \"\"\"Handle state-update results from the worker thread (main thread).\"\"\"\r\n        self._pending_state_update = False\r\n\r\n        if result.get('health_check'):\r\n            return\r\n\r\n        processed_state = result.get('processed_state', {})\r\n        \r\n        # [CRITICAL FIX] Filter out INPUT SENSORS so the worker cannot overwrite them.\r\n        # The worker operates on slightly old data. For sensors, the Main Thread \r\n        # (Environment) is the single source of truth.\r\n        INPUT_SENSORS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\",\r\n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\",\r\n            \"plant_proximity\"\r\n        }\r\n        \r\n        # Remove sensors from the update packet before applying it\r\n        for sensor in INPUT_SENSORS:\r\n            if sensor in processed_state:\r\n                del processed_state[sensor]\r\n\r\n        # Now it is safe to update. Only hidden/output neurons will be affected.\r\n        self.state.update(processed_state)\r\n\r\n        # Update communication events for glow effects\r\n        comm_events = result.get('communication_events', {})\r\n        self.communication_events.update(comm_events)\r\n\r\n        # Optional decay pre-application (Tamagotchi logic)\r\n        decay_updates = result.get('decay_updates', {})\r\n        if decay_updates and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\r\n            if hasattr(self.tamagotchi_logic, 'brain_hooks'):\r\n                for neuron, decayed_val in decay_updates.items():\r\n                    if neuron in self.state:\r\n                        # [CRITICAL FIX] Don't let decay logic touch sensors\r\n                        if neuron not in INPUT_SENSORS:\r\n                            self.state[neuron] = decayed_val\r\n\r\n        # [NEW] Run Enhanced Neurogenesis logic (Stress reduction, etc.)\r\n        if hasattr(self, 'enhanced_neurogenesis'):\r\n            self.enhanced_neurogenesis.update_neuron_activations(self.state)\r\n\r\n        self.update() # Repaint\r\n\r\n        # Queue next update if any\r\n        if self.state_update_queue:\r\n            next_data = self.state_update_queue.pop(0)\r\n            if hasattr(self, 'brain_worker') and self.brain_worker:\r\n                self.brain_worker.queue_state_update(next_data)\r\n                self._pending_state_update = True\r\n        \r\n    def _on_worker_error(self, error_msg: str):\r\n        \"\"\"Handle errors from the worker thread.\"\"\"\r\n        print(f\"⚠️ BrainWorker error: {error_msg}\")\r\n        \r\n    def _update_worker_cache(self):\r\n        \"\"\"Update the worker's cached data for thread-safe access.\"\"\"\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            # Get connector neurons for Hebbian learning exclusion\r\n            connector_neurons = {name for name, fn in self.enhanced_neurogenesis.functional_neurons.items()\r\n                            if fn.neuron_type == 'connector'}\r\n            \r\n            # Get new neurons for learning rate boost\r\n            new_neurons = set()\r\n            if hasattr(self, 'new_neurons'):\r\n                new_neurons = set(self.new_neurons.keys()) if isinstance(self.new_neurons, dict) else set(self.new_neurons)\r\n            \r\n            self.brain_worker.update_cache(\r\n                self.state,\r\n                self.weights,\r\n                self.neuron_positions,\r\n                self.config,\r\n                excluded_neurons=self.excluded_neurons,\r\n                connector_neurons=connector_neurons,  # Pass connector neurons separately\r\n                learning_rate=getattr(self, 'learning_rate', 0.1),\r\n                new_neurons=new_neurons\r\n            )\r\n            \r\n    def stop_worker(self):\r\n        \"\"\"Stop the worker thread gracefully (call on application exit).\"\"\"\r\n        if hasattr(self, 'brain_worker') and self.brain_worker:\r\n            self.brain_worker.stop()\r\n            self.brain_worker.wait(2000)  # Wait up to 2 seconds\r\n            print(\"🧵 BrainWorker thread stopped\")\r\n    \r\n    # =========================================================================\r\n    # End worker thread signal handlers\r\n    # =========================================================================\r\n\r\n        \r\n    def is_neuron_revealed(self, name):\r\n        \"\"\"Return True if the neuron has finished its reveal animation.\r\n        Only checks revealed tracking during tutorial mode.\"\"\"\r\n        # In normal gameplay (not tutorial), all neurons are considered revealed\r\n        if not self.is_tutorial_mode:\r\n            return True\r\n            \r\n        # During tutorial, check if neuron has completed its animation\r\n        if name not in self.neuron_reveal_animations:\r\n            return name in self.visible_neurons  # never animated = already visible\r\n        anim = self.neuron_reveal_animations[name]\r\n        elapsed = time.time() - anim['start_time']\r\n        return elapsed >= 0.4  # same duration used in draw_neurons\r\n    \r\n\r\n    def _advance_link_fades(self):\r\n        \"\"\"Called every 16 ms while any line is still moving.\"\"\"\r\n        dt = 0.016\r\n        still_animating = False\r\n        now = time.time()\r\n\r\n        for key, target in list(self._link_targets.items()):\r\n            # Check if this link's delay has passed\r\n            start_time = self._link_start_times.get(key, 0)\r\n            if now < start_time:\r\n                still_animating = True\r\n                continue\r\n                \r\n            current = self._link_opacities.get(key, 0.0)\r\n            # Use per-link speed for individual animation timing\r\n            speed = self._link_fade_speeds.get(key, 3.0)\r\n            step = speed * dt\r\n            \r\n            if abs(target - current) <= step:        # arrived\r\n                self._link_opacities[key] = target\r\n                del self._link_targets[key]          # stop tracking this one\r\n                if key in self._link_start_times:\r\n                    del self._link_start_times[key]\r\n                if key in self._link_fade_speeds:\r\n                    del self._link_fade_speeds[key]\r\n            else:                                    # move one step\r\n                self._link_opacities[key] = current + step * (1 if target > current else -1)\r\n                still_animating = True\r\n\r\n        self.update()                                # repaint with new alphas\r\n\r\n        if not still_animating:                      # all lines finished\r\n            self._link_fade_timer.stop()\r\n\r\n    def update_animations(self):\r\n        \"\"\"Update all animation states with smarter dirty-checking.\"\"\"\r\n        \r\n        current_lang = Localisation.instance().current_language\r\n        if getattr(self, '_last_lang', None) != current_lang:\r\n            self._last_lang = current_lang\r\n            self.mark_render_dirty()\r\n            self.update()\r\n\r\n        if self.is_paused:\r\n            return\r\n\r\n        current_time = time.time()\r\n        \r\n        # Performance tracking (keep if using task manager)\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _anim_start = time.perf_counter()\r\n            perf_tracker.increment(\"animation_frames\")\r\n        \r\n        # Frame rate limiting - skip if called too soon\r\n        if hasattr(self, '_last_animation_update'):\r\n            elapsed = current_time - self._last_animation_update\r\n            if elapsed < 0.04:  # 25fps cap (40ms) - reduced from 30fps\r\n                return\r\n        self._last_animation_update = current_time\r\n        \r\n        # Calculate dt\r\n        if not hasattr(self, '_last_animation_time'):\r\n            self._last_animation_time = current_time\r\n        dt = min(current_time - self._last_animation_time, 0.1)\r\n        self._last_animation_time = current_time\r\n\r\n        # SMARTER dirty tracking\r\n        needs_repaint = False\r\n\r\n        # 1. Neurogenesis pulse - only repaint if active\r\n        if self.neurogenesis_highlight['neuron']:\r\n            elapsed = current_time - self.neurogenesis_highlight['start_time']\r\n            if elapsed < self.neurogenesis_highlight['duration']:\r\n                self.neurogenesis_highlight['pulse_phase'] = elapsed * 15\r\n                needs_repaint = True\r\n            else:\r\n                self.neurogenesis_highlight['neuron'] = None\r\n                needs_repaint = True  # One final repaint to clear\r\n\r\n        # 2. Neuron reveal animations - only if we have any\r\n        if self.neuron_reveal_animations:\r\n            completed_reveals = []\r\n            for neuron_name, anim_data in self.neuron_reveal_animations.items():\r\n                elapsed = current_time - anim_data['start_time']\r\n                if elapsed >= 0.4:\r\n                    anim_data['progress'] = 1.0\r\n                    completed_reveals.append(neuron_name)\r\n                else:\r\n                    anim_data['progress'] = 1 - (1 - elapsed / 0.4) ** 3\r\n\r\n            for neuron_name in completed_reveals:\r\n                del self.neuron_reveal_animations[neuron_name]\r\n            \r\n            needs_repaint = True\r\n            \r\n            # Enable links after last reveal\r\n            if (len(self.visible_neurons) == len(self.original_neurons) and\r\n                not self.neuron_reveal_animations and not self.show_links):\r\n                self._enable_links_after_reveal()\r\n\r\n        # 3. Weight animations - check if any are active\r\n        if self.weight_animations:\r\n            old_count = len(self.weight_animations)\r\n            self.weight_animations = [\r\n                anim for anim in self.weight_animations\r\n                if current_time - anim['start_time'] < anim['duration']\r\n            ]\r\n            if self.weight_animations or old_count > 0:\r\n                needs_repaint = True\r\n        \r\n        # 4. VIBRANT: Ambient pulses - only update if enabled AND we have connections\r\n        if self.anim_ambient_pulse_enabled and self._ambient_pulse_state:\r\n            self._update_ambient_pulses(dt)\r\n            needs_repaint = True\r\n        \r\n        # 5. SUBTLE: Communication glows - only if packets exist\r\n        if self.anim_comm_glow_enabled:\r\n            had_packets = bool(self._comm_glow_packets)\r\n            self._update_comm_glows(dt)\r\n            self._spawn_activity_glows(current_time)\r\n            # Only repaint if we had or now have packets\r\n            if had_packets or self._comm_glow_packets:\r\n                needs_repaint = True\r\n        \r\n        # 6. NEURAL: Activation pulses - only if pulses exist  \r\n        if self.anim_neural_pulse_enabled:\r\n            had_pulses = bool(self._neural_pulses)\r\n            self._update_neural_pulses(current_time)\r\n            self._spawn_neural_pulses_from_activity(current_time)\r\n            if had_pulses or self._neural_pulses:\r\n                needs_repaint = True\r\n        \r\n        # Only trigger repaint when needed\r\n        if needs_repaint:\r\n            self.update()\r\n        \r\n        # Performance tracking end\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _anim_elapsed = (time.perf_counter() - _anim_start) * 1000\r\n            perf_tracker.record(\"update_animations\", _anim_elapsed)\r\n\r\n    def _get_cached_font(self, size, bold=False):\r\n        \"\"\"Return cached QFont to avoid recreation every paint.\"\"\"\r\n        key = (size, bold)\r\n        if key not in self._cached_fonts:\r\n            font = QtGui.QFont(\"Arial\", size)\r\n            if bold:\r\n                font.setBold(True)\r\n            self._cached_fonts[key] = font\r\n        return self._cached_fonts[key]\r\n    \r\n    def _get_cached_pen(self, color_tuple, width=1):\r\n        \"\"\"Return cached QPen to avoid recreation every paint.\"\"\"\r\n        key = (*color_tuple, width)\r\n        if key not in self._cached_pens:\r\n            color = QtGui.QColor(*color_tuple[:3])\r\n            if len(color_tuple) > 3:\r\n                color.setAlpha(color_tuple[3])\r\n            self._cached_pens[key] = QtGui.QPen(color, width)\r\n        return self._cached_pens[key]\r\n\r\n    def reveal_neuron(self, neuron_name):\r\n        \"\"\"Reveal a neuron with an expand animation – forces links OFF during reveal.\"\"\"\r\n        import time\r\n        \r\n        if neuron_name not in self.original_neurons:\r\n            return\r\n        if neuron_name in self.visible_neurons:\r\n            return\r\n        if neuron_name not in self.neuron_positions:\r\n            print(f\"❌ ERROR: Neuron {neuron_name} has no position defined!\")\r\n            return\r\n\r\n        # First reveal → force links OFF\r\n        if not self.visible_neurons:\r\n            self.show_links = False\r\n            if hasattr(self, 'parent') and hasattr(self.parent(), 'network_tab'):\r\n                nt = self.parent().network_tab\r\n                nt.checkbox_links.setChecked(False)\r\n                nt.checkbox_links.setEnabled(False)\r\n\r\n        # Staggered start\r\n        stagger = 0.4\r\n        delay = len(self.visible_neurons) * stagger\r\n\r\n        self.visible_neurons.add(neuron_name)\r\n\r\n        # Track this neuron as revealed for count display (tutorial only)\r\n        if self.is_tutorial_mode:\r\n            self.revealed_neurons.add(neuron_name)\r\n            self._reveal_connections_for_neuron(neuron_name)\r\n\r\n        # Trigger immediate metrics update\r\n        self._update_network_metrics()\r\n\r\n        self.neuron_reveal_animations[neuron_name] = {\r\n            'start_time': time.time() + delay,\r\n            'progress': 0.0\r\n        }\r\n\r\n        pos = self.neuron_positions[neuron_name]\r\n        self.mark_render_dirty()\r\n\r\n    def _enable_links_after_reveal(self):\r\n        \"\"\"Re-enable checkbox, tick it, and kick off the existing link-fade engine.\"\"\"\r\n        self.show_links = True\r\n        \r\n        # Populate link targets for all weights\r\n        for key in self.weights.keys():\r\n            self._link_targets[key] = 1.0\r\n            if key not in self._link_opacities:\r\n                self._link_opacities[key] = 0.0\r\n        \r\n        # Set fade speed\r\n        self._link_fade_speed = 3.0\r\n        \r\n        # Re-enable and check the checkbox\r\n        if hasattr(self, 'parent') and hasattr(self.parent(), 'network_tab'):\r\n            nt = self.parent().network_tab\r\n            nt.checkbox_links.setEnabled(True)\r\n            nt.checkbox_links.setChecked(True)\r\n\r\n        # Start the fade timer\r\n        if not self._link_fade_timer.isActive():\r\n            self._link_fade_timer.start()\r\n        \r\n        self.mark_render_dirty()\r\n    \r\n    def reveal_all_core_neurons(self):\r\n        \"\"\"Make all core neurons visible immediately (for loaded games)\"\"\"\r\n        for neuron_name in self.original_neurons:\r\n            self.visible_neurons.add(neuron_name)\r\n        # For loaded games, remove tracking attributes immediately\r\n        if hasattr(self, 'revealed_neurons'):\r\n            delattr(self, 'revealed_neurons')\r\n        if hasattr(self, 'revealed_connections'):\r\n            delattr(self, 'revealed_connections')\r\n        # Ensure tutorial mode is disabled for loaded games\r\n        self.is_tutorial_mode = False\r\n        \r\n        # Mark render dirty and trigger update\r\n        self.mark_render_dirty()\r\n        self.update()\r\n    \r\n    def _reveal_connections_for_neuron(self, neuron_name):\r\n        \"\"\"Reveal connections between this neuron and other revealed neurons\r\n        NOTE: This should only be called during tutorial mode\"\"\"\r\n        if not self.is_tutorial_mode:\r\n            return\r\n        if not hasattr(self, 'weights'):\r\n            return\r\n        if not hasattr(self, 'revealed_neurons') or not hasattr(self, 'revealed_connections'):\r\n            return\r\n            \r\n        # Check all weights to find connections involving this neuron\r\n        for (source, target), weight in self.weights.items():\r\n            # If both ends of the connection are revealed, track it\r\n            if source in self.revealed_neurons and target in self.revealed_neurons:\r\n                edge_key = f\"{source}->{target}\"\r\n                if edge_key not in self.revealed_connections:\r\n                    self.revealed_connections.add(edge_key)\r\n    \r\n    def _update_network_metrics(self):\r\n        \"\"\"Update the network tab metrics display\"\"\"\r\n        try:\r\n            # Try to get the parent window (SquidBrainWindow)\r\n            parent = self.parent()\r\n            if parent and hasattr(parent, 'network_tab'):\r\n                parent.network_tab.update_metrics_display()\r\n        except Exception as e:\r\n            pass  # Silently fail if we can't update - not critical\r\n    \r\n    def finish_reveal_animation(self):\r\n        \"\"\"Called when the reveal animation completes - cleanup tracking attributes\"\"\"\r\n        # Remove reveal tracking so normal counting resumes\r\n        if hasattr(self, 'revealed_neurons'):\r\n            delattr(self, 'revealed_neurons')\r\n        if hasattr(self, 'revealed_connections'):\r\n            delattr(self, 'revealed_connections')\r\n\r\n        # Exit tutorial mode\r\n        self.is_tutorial_mode = False\r\n\r\n        # AUTO-ENABLE links now that all neurons are revealed\r\n        self.show_links = True\r\n        if hasattr(self, 'parent') and hasattr(self.parent(), 'network_tab'):\r\n            nt = self.parent().network_tab\r\n            nt.checkbox_links.setEnabled(True)\r\n            nt.checkbox_links.setChecked(True)      # tick the box\r\n\r\n        # Fade-in the links smoothly\r\n        if self.show_links and self._link_targets:\r\n            self._auto_fade_links_pending = True\r\n            if not self._link_fade_timer.isActive():\r\n                self._advance_link_fades()\r\n\r\n    def add_weight_animation(self, neuron1, neuron2, old_weight, new_weight, \r\n                         custom_color=None, custom_duration=3.0):\r\n        \"\"\"Add animation for a weight change with organic staggering through connectors\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # Determine color based on weight change direction if not custom\r\n        if custom_color:\r\n            color = custom_color\r\n        else:\r\n            color = (0, 255, 0) if new_weight > old_weight else (255, 0, 0)\r\n        \r\n        # Check if there's a connector neuron in the path\r\n        # A connector is in the path if:\r\n        # 1. It has connections to both neuron1 and neuron2\r\n        # 2. It's a connector type neuron\r\n        connector_in_path = None\r\n        possible_connectors = []\r\n        \r\n        # Find all connector neurons that connect to both\r\n        for (src, dst), weight in self.weights.items():\r\n            # Check if this is a connector neuron connection\r\n            is_connector = (src.startswith('connector_') or \r\n                        (src in self.neuron_shapes and self.neuron_shapes[src] == 'hexagon'))\r\n            \r\n            if is_connector:\r\n                # Check if connector connects to both neurons\r\n                conn_to_n1 = (src == neuron1 or dst == neuron1)\r\n                conn_to_n2 = (src == neuron2 or dst == neuron2)\r\n                \r\n                if conn_to_n1 and conn_to_n2:\r\n                    # This connector is between our two neurons\r\n                    connector_in_path = src if src.startswith('connector_') else dst\r\n                    break\r\n        \r\n        # Create staggered animations for organic feel\r\n        if connector_in_path:\r\n            # Two-segment animation: neuron1 → connector → neuron2\r\n            # Add random delay at connector for organic relay effect\r\n            \r\n            # First segment: neuron1 → connector\r\n            self.weight_animations.append({\r\n                'pair': (neuron1, connector_in_path),\r\n                'start_time': current_time,\r\n                'duration': custom_duration * 0.45,  # Slightly shorter for first half\r\n                'start_weight': old_weight,\r\n                'end_weight': new_weight,\r\n                'neuron1': neuron1,\r\n                'neuron2': connector_in_path,\r\n                'color': color,\r\n                'is_segment': True,\r\n                'final_target': neuron2\r\n            })\r\n            \r\n            # Second segment: connector → neuron2 (with stagger delay)\r\n            stagger_delay = random.uniform(0.15, 0.25)  # 150-250ms pause at connector\r\n            self.weight_animations.append({\r\n                'pair': (connector_in_path, neuron2),\r\n                'start_time': current_time + stagger_delay,\r\n                'duration': custom_duration * 0.45,\r\n                'start_weight': old_weight,\r\n                'end_weight': new_weight,\r\n                'neuron1': connector_in_path,\r\n                'neuron2': neuron2,\r\n                'color': color,\r\n                'is_segment': True,\r\n                'final_target': neuron2,\r\n                'stagger_delay': stagger_delay\r\n            })\r\n            \r\n            print(f\"🎨 Staggered animation: {neuron1} → {connector_in_path} → {neuron2} \"\r\n                f\"(delay: {stagger_delay:.2f}s)\")\r\n        else:\r\n            # Direct connection - single animation as before\r\n            self.weight_animations.append({\r\n                'pair': (neuron1, neuron2),\r\n                'start_time': current_time,\r\n                'duration': custom_duration,\r\n                'start_weight': old_weight,\r\n                'end_weight': new_weight,\r\n                'neuron1': neuron1,\r\n                'neuron2': neuron2,\r\n                'color': color,\r\n                'is_segment': False\r\n            })\r\n        \r\n        # Record communication events\r\n        self.weight_change_events[neuron1] = current_time\r\n        self.weight_change_events[neuron2] = current_time\r\n        \r\n        # Add to recently updated pairs (store original pair)\r\n        if (neuron1, neuron2) not in self.recently_updated_neuron_pairs and \\\r\n        (neuron2, neuron1) not in self.recently_updated_neuron_pairs:\r\n            self.recently_updated_neuron_pairs.append((neuron1, neuron2))\r\n        \r\n        # Mark render dirty immediately\r\n        self.mark_render_dirty()\r\n        self.update()\r\n\r\n    def is_neurogenesis_neuron(self, neuron_name: str) -> bool:\r\n        \"\"\"\r\n        Check if a neuron was created through neurogenesis (and can be dragged).\r\n        TO BE REMOVED IN FUTURE VERSION - depreceted\r\n        \"\"\"\r\n        \r\n        if not neuron_name:\r\n            return False\r\n        \r\n        # Core neurons that should NOT be draggable\r\n        if neuron_name in self.original_neurons:\r\n            return True # HACK make it True anyway\r\n        \r\n        # ANY neuron not in original_neurons is draggable (including circular ones)\r\n        return True\r\n\r\n    def _periodic_neurogenesis_check(self):\r\n        \"\"\"Periodic check for neurogenesis triggers and orphans.\"\"\"\r\n        if not hasattr(self, 'check_neurogenesis_triggers'):\r\n            return\r\n            \r\n        # Skip if a check is already pending\r\n        if self._pending_neurogenesis_check:\r\n            return\r\n        \r\n        # Prioritise Orphan Rescue ---\r\n        if hasattr(self, 'enhanced_neurogenesis') and hasattr(self.enhanced_neurogenesis, 'rescue_orphan'):\r\n            orphans = self.find_orphan_neurons()\r\n            if orphans:\r\n                orphan = orphans[0]\r\n                print(f\"🚑 Orphan detected: {orphan}. Creating connector neuron...\")\r\n                \r\n                # ADD ERROR HANDLING\r\n                try:\r\n                    self.enhanced_neurogenesis.rescue_orphan(orphan)\r\n                    self.update()\r\n                    return  # Skip normal neurogenesis this tick\r\n                except Exception as e:\r\n                    print(f\"⚠️ Failed to rescue orphan {orphan}: {e}\")\r\n                    import traceback\r\n                    traceback.print_exc()\r\n        \r\n        # Build state with context and check neurogenesis triggers\r\n        state_with_context = self.state.copy()\r\n        self._pending_neurogenesis_check = True\r\n        \r\n        # Queue to worker thread if available\r\n        if self._use_threaded_processing and hasattr(self, 'brain_worker') and self.brain_worker.isRunning():\r\n            self.brain_worker.queue_neurogenesis_check(state_with_context)\r\n        else:\r\n            # Fallback: synchronous check\r\n            try:\r\n                result = self.check_neurogenesis_triggers(state_with_context)\r\n                if result:\r\n                    self._on_neurogenesis_complete({'should_create': True, **result})\r\n            except Exception as e:\r\n                print(f\"⚠️ Error in neurogenesis check: {e}\")\r\n                import traceback\r\n                traceback.print_exc()\r\n            finally:\r\n                self._pending_neurogenesis_check = False\r\n\r\n    def toggle_pruning(self, enabled):\r\n        \"\"\"Enable or disable the pruning mechanisms for neurogenesis\"\"\"\r\n        previous = self.pruning_enabled\r\n        self.pruning_enabled = enabled\r\n\r\n        if previous != enabled:\r\n            print(f\"\\x1b[{'42' if enabled else '41'}mPruning {'enabled' if enabled else 'disabled'}\\x1b[0m - Neurogenesis {'constrained' if enabled else 'unconstrained'}\")\r\n\r\n            if not enabled:\r\n                print(\"\\x1b[31mWARNING: Disabling pruning may lead to network instability\\x1b[0m\")\r\n\r\n        return self.pruning_enabled\r\n    \r\n    def get_stress_neuron_count(self):\r\n        \"\"\"Counts the number of neurons that start with the name 'stress'.\"\"\"\r\n        return len([name for name in self.neuron_positions if name.startswith('stress')])\r\n\r\n    def stop_hebbian_learning(self):\r\n        \"\"\"Stop Hebbian learning immediately\"\"\"\r\n        self.learning_active = False\r\n        self.hebbian_countdown_seconds = 0\r\n        print(\"++ Hebbian learning stopped\")\r\n\r\n    def start_hebbian_learning(self, duration_seconds=30):\r\n        \"\"\"Start Hebbian learning with a specified duration\"\"\"\r\n        self.hebbian_countdown_seconds = duration_seconds\r\n        self.learning_active = True\r\n        self.is_paused = False\r\n        print(f\"++ Hebbian learning started for {duration_seconds} seconds\")\r\n\r\n    def _update_communication_events(self):\r\n        \"\"\"Clean up old communication events that have expired\"\"\"\r\n        current_time = time.time()\r\n        # Remove events older than the highlight duration\r\n        expired_neurons = [\r\n            neuron for neuron, event_time in self.communication_events.items()\r\n            if current_time - event_time > self.communication_highlight_duration\r\n        ]\r\n        for neuron in expired_neurons:\r\n            del self.communication_events[neuron]\r\n\r\n    def get_neuron_value(self, value):\r\n        \"\"\"\r\n        Convert a neuron value to a numerical format for Hebbian learning.\r\n\r\n        Args:\r\n            value: The value of the neuron, which can be int, float, bool, or str.\r\n\r\n        Returns:\r\n            float: The numerical value of the neuron.\r\n        \"\"\"\r\n        if isinstance(value, (int, float)):\r\n            return float(value)\r\n        elif isinstance(value, bool):\r\n            return 100.0 if value else 0.0\r\n        elif isinstance(value, str):\r\n            # For string values (like 'direction'), return a default value\r\n            return 75.0\r\n        else:\r\n            return 0.0\r\n        \r\n    def is_new_neuron(self, neuron_name, newness_duration_sec=300): # 300s = 5 minutes\r\n        \"\"\"Check if a neuron was created within the newness duration.\"\"\"\r\n        # Check if it's in the primary neurogenesis data\r\n        if neuron_name in self.neurogenesis_data.get('new_neurons', []):\r\n            details = self.neurogenesis_data.get('new_neurons_details', {}).get(neuron_name)\r\n            if details:\r\n                created_at = details.get('created_at')\r\n                if created_at and (time.time() - created_at) < newness_duration_sec:\r\n                    return True # It's new based on details\r\n            else:\r\n                 # If it's in the list and < 5 mins, assume new.\r\n                 pass\r\n        return False\r\n\r\n\r\n    def update_connection(self, neuron1, neuron2, value1, value2):\r\n        \"\"\"Update connection weight and trigger animations, with connector limits.\"\"\"\r\n        import time\r\n        \r\n        current_time = time.time()\r\n        pair = (neuron1, neuron2)\r\n        reverse_pair = (neuron2, neuron1)\r\n\r\n        # Check if connection exists\r\n        exists = pair in self.weights or reverse_pair in self.weights\r\n        use_pair = pair if pair in self.weights else reverse_pair\r\n\r\n        # --- CONNECTOR LIMIT CHECK ---\r\n        # If this is a NEW connection, check if either party is a maxed-out connector\r\n        if not exists:\r\n            # Check Neuron 1\r\n            if self.is_connector_neuron(neuron1):\r\n                if self.get_neuron_degree(neuron1) >= 3:\r\n                    # print(f\"🚫 Blocked new connection to connector {neuron1} (Max 3 reached)\")\r\n                    return\r\n            \r\n            # Check Neuron 2\r\n            if self.is_connector_neuron(neuron2):\r\n                if self.get_neuron_degree(neuron2) >= 3:\r\n                    # print(f\"🚫 Blocked new connection to connector {neuron2} (Max 3 reached)\")\r\n                    return\r\n\r\n        # Initialize if new\r\n        if not exists:\r\n            if neuron1 in self.neuron_positions and neuron2 in self.neuron_positions:\r\n                self.weights[pair] = 0.0\r\n                use_pair = pair\r\n                print(f\"      🆕 Created new connection: {neuron1} ↔ {neuron2}\")\r\n            else:\r\n                return\r\n\r\n        prev_weight = self.weights[use_pair]\r\n\r\n        # Learning Rate Calculation\r\n        base_lr = self.learning_rate\r\n        newness_boost = 2.0\r\n        effective_lr = base_lr\r\n\r\n        is_n1_new = self.is_new_neuron(neuron1)\r\n        is_n2_new = self.is_new_neuron(neuron2)\r\n\r\n        if is_n1_new or is_n2_new:\r\n            effective_lr = base_lr * newness_boost\r\n\r\n        # Calculate weight change (basic Hebbian)\r\n        weight_change = effective_lr * (value1 / 100.0) * (value2 / 100.0)\r\n        \r\n        # Add weight decay\r\n        decay_rate = self.config.hebbian.get('weight_decay', 0.01) * 0.1\r\n        new_weight = prev_weight + weight_change - (prev_weight * decay_rate)\r\n        \r\n        # Clamp weight to [-1, 1] range\r\n        new_weight = min(max(new_weight, -1.0), 1.0)\r\n        self.weights[use_pair] = new_weight\r\n\r\n        # Add animation using helper method\r\n        self.add_weight_animation(neuron1, neuron2, prev_weight, new_weight)\r\n\r\n        # Record weight change time\r\n        if abs(new_weight - prev_weight) > 0.001:\r\n            self.weight_change_events[neuron1] = current_time\r\n            self.weight_change_events[neuron2] = current_time\r\n            \r\n            if (neuron1, neuron2) not in self.recently_updated_neuron_pairs and \\\r\n            (neuron2, neuron1) not in self.recently_updated_neuron_pairs:\r\n                self.recently_updated_neuron_pairs.append((neuron1, neuron2))\r\n            \r\n            self.mark_render_dirty()\r\n            self.update()\r\n\r\n        self.communication_events[neuron1] = current_time\r\n        self.communication_events[neuron2] = current_time\r\n\r\n    def is_connector_neuron(self, neuron_name: str) -> bool:\r\n        \"\"\"\r\n        Check if a neuron is a connector neuron.\r\n        Checks multiple sources to ensure robustness across save/load states.\r\n        \"\"\"\r\n        if not neuron_name:\r\n            return False\r\n            \r\n        # 1. Check visual shape (Primary persistence mechanism)\r\n        if self.neuron_shapes.get(neuron_name) == 'hexagon':\r\n            return True\r\n            \r\n        # 2. Check naming convention\r\n        if neuron_name.startswith('connector_'):\r\n            return True\r\n            \r\n        # 3. Check enhanced neurogenesis registry (Runtime logic)\r\n        if hasattr(self, 'enhanced_neurogenesis') and self.enhanced_neurogenesis:\r\n            fn = self.enhanced_neurogenesis.functional_neurons.get(neuron_name)\r\n            if fn and fn.neuron_type == 'connector':\r\n                return True\r\n                \r\n        return False\r\n\r\n    def get_neuron_degree(self, neuron_name: str) -> int:\r\n        \"\"\"Count the number of active connections for a specific neuron.\"\"\"\r\n        count = 0\r\n        for (u, v) in self.weights.keys():\r\n            if u == neuron_name or v == neuron_name:\r\n                count += 1\r\n        return count\r\n\r\n    def prune_weak_connections(self, threshold=0.05, min_age_sec=600):\r\n        \"\"\"Removes connections with absolute weight below threshold, ignoring connectors.\"\"\"\r\n        if not self.pruning_enabled:\r\n            return 0\r\n\r\n        to_delete = []\r\n\r\n        for pair, weight in list(self.weights.items()):\r\n            if not (isinstance(pair, tuple) and len(pair) == 2):\r\n                continue\r\n\r\n            neuron1, neuron2 = pair\r\n\r\n            # IMMUNITY: Never prune connections attached to connector neurons\r\n            if self.is_connector_neuron(neuron1) or self.is_connector_neuron(neuron2):\r\n                continue\r\n\r\n            # IMMUNITY: New neurons\r\n            if self.is_new_neuron(neuron1, min_age_sec) or self.is_new_neuron(neuron2, min_age_sec):\r\n                continue\r\n\r\n            if abs(weight) < threshold:\r\n                to_delete.append(pair)\r\n\r\n        for pair in to_delete:\r\n            if pair in self.weights:\r\n                del self.weights[pair]\r\n\r\n        if len(to_delete) > 0:\r\n            print(f\"\\x1b[33mPruned {len(to_delete)} weak connections.\\x1b[0m\")\r\n            self.mark_render_dirty()\r\n            self.update()\r\n        \r\n        return len(to_delete)\r\n\r\n\r\n    def perform_hebbian_learning(self):\r\n        \"\"\"\r\n        One full Hebbian update cycle.\r\n        Queues work to background thread for non-blocking operation.\r\n        \"\"\"\r\n        # === Disable Hebbian while squid is sleeping ===\r\n        if (hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic and\r\n            hasattr(self.tamagotchi_logic.squid, 'is_sleeping') and\r\n            self.tamagotchi_logic.squid.is_sleeping):\r\n            # print(\"DEBUG: Hebbian learning skipped - squid is sleeping\")\r\n            return\r\n\r\n        # Skip if already pending\r\n        if self._pending_hebbian_learning:\r\n            return\r\n        \r\n        # CRITICAL: Update cache BEFORE queueing work\r\n        self._update_worker_cache()\r\n        \r\n        if self._use_threaded_processing and hasattr(self, 'brain_worker') and self.brain_worker.isRunning():\r\n            # Queue the work to the background thread\r\n            self._pending_hebbian_learning = True\r\n            self.brain_worker.queue_hebbian_learning()\r\n        else:\r\n            # Fallback: synchronous processing\r\n            self._perform_hebbian_learning_sync()\r\n\r\n\r\n    def _perform_hebbian_learning_sync(self):\r\n        \"\"\"Original synchronous Hebbian learning (fallback if threading disabled).\"\"\"\r\n        from heapq import nlargest\r\n\r\n        print(f\"++ [Sync] Hebbian learning cycle triggered\")\r\n\r\n        COLORS = [\"\\033[96m\", \"\\033[93m\", \"\\033[95m\", \"\\033[92m\", \"\\033[94m\"]\r\n        RESET = \"\\033[0m\"\r\n\r\n        if not hasattr(self, 'weights'):\r\n            self.weights = {}\r\n        \r\n        # Ensure list exists (used for history tracking in sync mode)\r\n        if not hasattr(self, 'recently_updated_neuron_pairs'):\r\n            self.recently_updated_neuron_pairs = []\r\n\r\n        # NEW: Get connector neurons to exclude from learning\r\n        connector_neurons = set()\r\n        if hasattr(self, 'enhanced_neurogenesis') and self.enhanced_neurogenesis:\r\n            connector_neurons = {name for name, fn in self.enhanced_neurogenesis.functional_neurons.items()\r\n                            if fn.neuron_type == 'connector'}\r\n        \r\n        # PURE_INPUTS (Sensors) - Do not include in Hebbian learning\r\n        PURE_INPUTS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \r\n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\", \r\n            \"plant_proximity\"\r\n        }\r\n\r\n        # Combine excluded neurons, connector neurons, and pure inputs\r\n        learning_excluded = set(self.excluded_neurons) | connector_neurons | PURE_INPUTS\r\n        \r\n        neurons = [n for n in self.neuron_positions.keys() if n not in learning_excluded]\r\n        print(f\"   [Sync] Neurons available: {len(neurons)} (excluded {len(connector_neurons)} connectors + inputs), \"\r\n            f\"Weights tracked: {len(self.weights)}\")\r\n\r\n        scored_pairs = []\r\n        \r\n        # Prepare history for quick lookup (ensure sorted tuples)\r\n        recent_history_sorted = set()\r\n        for p in self.recently_updated_neuron_pairs:\r\n            if isinstance(p, (list, tuple)) and len(p) == 2:\r\n                recent_history_sorted.add(tuple(sorted(p)))\r\n\r\n        for i, n1 in enumerate(neurons):\r\n            for n2 in neurons[i + 1:]:\r\n                v1 = self.get_neuron_value(self.state.get(n1, 50))\r\n                v2 = self.get_neuron_value(self.state.get(n2, 50))\r\n                \r\n                # Base Score\r\n                score = v1 + v2\r\n                \r\n                # 1. Add Random Noise to break deterministic loops\r\n                score += random.uniform(0, 40)\r\n                \r\n                # 2. Penalize recently updated pairs to prevent loops\r\n                current_pair = tuple(sorted((n1, n2)))\r\n                \r\n                if current_pair in recent_history_sorted:\r\n                    score -= 500 # Heavy penalty\r\n\r\n                scored_pairs.append((score, n1, n2, v1, v2))\r\n        top_k = self.config.neurogenesis.get('max_hebbian_pairs', 2)\r\n        top_pairs = nlargest(top_k, scored_pairs)\r\n        print(f\"   [Sync] Top {top_k} pairs selected from {len(scored_pairs)} candidates\")\r\n\r\n        updated_pairs = []\r\n\r\n        for _, n1, n2, v1, v2 in top_pairs:\r\n            base_lr = self.learning_rate\r\n            decay_rate = self.config.hebbian.get('weight_decay', 0.01)\r\n\r\n            if self.is_new_neuron(n1) or self.is_new_neuron(n2):\r\n                base_lr *= 2.0\r\n\r\n            delta = base_lr * (v1 / 100.0) * (v2 / 100.0)\r\n\r\n            pair = (n1, n2)\r\n            reverse_pair = (n2, n1)\r\n            use_pair = pair if pair in self.weights else reverse_pair\r\n\r\n            if use_pair not in self.weights:\r\n                print(f\"   [Sync] Skipping pair {n1}-{n2}: not in weights dict\")\r\n                continue\r\n\r\n            old_w = self.weights[use_pair]\r\n            new_w = old_w + delta - (old_w * decay_rate)\r\n            new_w = max(self.config.hebbian.get('min_weight', -1.0),\r\n                        min(self.config.hebbian.get('max_weight', 1.0), new_w))\r\n\r\n            self.weights[use_pair] = new_w\r\n            self.add_weight_animation(n1, n2, old_w, new_w)\r\n            updated_pairs.append((n1, n2))\r\n\r\n        if updated_pairs:\r\n            colored = []\r\n            for i, (a, b) in enumerate(updated_pairs):\r\n                color = COLORS[i % len(COLORS)]\r\n                colored.append(f\"{color}{a} ↔ {b}{RESET}\")\r\n            print(\"     Hebbian learning chosen pairs: \" + \"  \".join(colored))\r\n        else:\r\n            print(\"   [Sync] No pairs were updated this cycle\")\r\n\r\n        self.recently_updated_neuron_pairs = updated_pairs\r\n        self.last_hebbian_time = time.time()\r\n        self.update()\r\n\r\n\r\n    def get_recently_updated_neurons(self):\r\n        \"\"\"Return the list of neuron pairs updated in the last learning cycle\"\"\"\r\n        return self.recently_updated_neuron_pairs\r\n\r\n\r\n    def resizeEvent(self, event):\r\n        \"\"\"Handle window resize events - startles squid and enforces minimum size\"\"\"\r\n        super().resizeEvent(event)\r\n\r\n        # Only trigger startle if we're actually resizing (not just moving)\r\n        old_size = event.oldSize()\r\n        if (old_size.isValid() and\r\n            hasattr(self, 'tamagotchi_logic') and\r\n            self.tamagotchi_logic):\r\n\r\n            new_size = event.size()\r\n            width_change = abs(new_size.width() - old_size.width())\r\n            height_change = abs(new_size.height() - old_size.height())\r\n\r\n            # Only startle if change is significant (>50px)\r\n            if width_change > 50 or height_change > 50:\r\n                # Check if first resize\r\n                if not hasattr(self, '_has_resized_before'):\r\n                    source = \"first_resize\"\r\n                    self._has_resized_before = True\r\n                else:\r\n                    source = \"window_resize\"\r\n\r\n                #self.tamagotchi_logic.startle_squid(source=source) #STARTLE WHEN WINDOW RESIZE\r\n\r\n                if self.debug_mode:\r\n                    print(f\"Squid startled by {source}\")\r\n\r\n        # Enforce minimum window size (1280x900)\r\n        min_width, min_height = 1280, 900\r\n        if event.size().width() < min_width or event.size().height() < min_height:\r\n            self.resize(\r\n                max(event.size().width(), min_width),\r\n                max(event.size().height(), min_height)\r\n            )\r\n\r\n    def closeEvent(self, event):\r\n        \"\"\"Handle window close event - save state and clean up resources\"\"\"\r\n        # Stop the worker thread first\r\n        self.stop_worker()\r\n        \r\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\r\n            # Save current brain state\r\n            try:\r\n                brain_state = self.save_brain_state()\r\n                with open('last_brain_state.json', 'w') as f:\r\n                    json.dump(brain_state, f)\r\n            except Exception as e:\r\n                print(f\"Error saving brain state: {e}\")\r\n\r\n            # Clean up timers\r\n            if hasattr(self, 'hebbian_timer'):\r\n                self.hebbian_timer.stop()\r\n            if hasattr(self, 'countdown_timer'):\r\n                self.countdown_timer.stop()\r\n            if hasattr(self, 'memory_update_timer'):\r\n                self.memory_update_timer.stop()\r\n\r\n        # Close any child windows\r\n        if hasattr(self, '_inspector') and self._inspector: \r\n            self._inspector.close()\r\n            \r\n        if hasattr(self, '_laboratory') and self._laboratory:\r\n            self._laboratory.close()\r\n        \r\n        # Accept the close event \r\n        event.accept()\r\n\r\n    def save_brain_state(self):\r\n        return {\r\n            'weights': self.weights,\r\n            'neuron_positions': self.neuron_positions,\r\n            'neuron_states': self.state,\r\n            'layer_structure': self.layers,\r\n            'neuron_shapes': dict(self.neuron_shapes),\r\n            'state_colors': dict(self.state_colors),\r\n        }\r\n\r\n    def load_brain_state(self, state):\r\n        \"\"\"Load the brain state from a saved state dictionary\"\"\"\r\n        self.weights = state['weights']\r\n        self.neuron_positions = state['neuron_positions']\r\n        self.state = state.get('neuron_states', {})\r\n        self.layers = state.get('layer_structure', [])\r\n        \r\n        # Sync connections list from loaded weights\r\n        self.sync_connections_from_weights()\r\n\r\n        # ADD THESE THREE LINES\r\n        self.neuron_shapes = state.get('neuron_shapes', {})  # Load shapes\r\n        self.state_colors = state.get('state_colors', {})    # Load colors\r\n        print(f\"📦 Loaded {len(self.neuron_shapes)} neuron shapes\")  # Debug print\r\n\r\n        # Ensure all neurons in neuron_positions exist in state\r\n        for neuron in self.neuron_positions:\r\n            if neuron not in self.state:\r\n                self.state[neuron] = 50\r\n\r\n        # Explicitly update excluded neurons if they exist in positions\r\n        for neuron in self.excluded_neurons:\r\n            if neuron in self.neuron_positions and neuron not in self.state:\r\n                self.state[neuron] = False\r\n\r\n        # Ensure visible_neurons is complete\r\n        if not self.is_tutorial_mode:\r\n            self.visible_neurons = set(self.original_neurons)\r\n            for neuron in self.neuron_positions:\r\n                if neuron not in self.excluded_neurons and neuron not in self.original_neurons:\r\n                    self.visible_neurons.add(neuron)\r\n            print(f\"📊 Loaded {len(self.neuron_positions)} neurons, {len(self.visible_neurons)} visible\")\r\n        \r\n        # Mark render dirty and trigger update\r\n        self.mark_render_dirty()\r\n        self.update()\r\n\r\n\r\n\r\n\r\n    def create_initial_state(self):\r\n        \"\"\"\r\n        Create and return the initial brain state for a new game.\r\n        Returns a dictionary with all core neurons set to their default values.\r\n        Also resets animation tracking attributes.\r\n        \"\"\"\r\n        # Reset animation tracking attributes\r\n        self.reset_animation_state()\r\n        \r\n        initial_state = {\r\n            \"can_see_food\": 0,\r\n            \"hunger\": 50,\r\n            \"happiness\": 50,\r\n            \"cleanliness\": 50,\r\n            \"sleepiness\": 50,\r\n            \"satisfaction\": 50,\r\n            \"anxiety\": 50,\r\n            \"curiosity\": 50,\r\n            \"is_sick\": False,\r\n            \"is_eating\": False,\r\n            \"is_sleeping\": False,\r\n            \"pursuing_food\": False,\r\n            \"direction\": \"up\",\r\n            \"position\": (0, 0),\r\n            \"is_startled\": False,\r\n            \"is_fleeing\": False,\r\n            'neurogenesis_active': True\r\n        }\r\n        return initial_state\r\n    \r\n    def reset_animation_state(self):\r\n        \"\"\"\r\n        Reset all animation and reveal tracking attributes for a new game.\r\n        This ensures the neuron reveal animation can run properly.\r\n        \"\"\"\r\n        # Enable tutorial mode for new game reveal animation\r\n        self.is_tutorial_mode = True\r\n        \r\n        # Reset visible neurons\r\n        self.visible_neurons = set()\r\n        \r\n        # Reset reveal tracking (recreate if deleted)\r\n        self.revealed_neurons = set()\r\n        self.revealed_connections = set()\r\n        \r\n        # Clear reveal animations\r\n        self.neuron_reveal_animations = {}\r\n        \r\n        print(\"🔄 Animation state reset for new game (tutorial mode enabled)\")\r\n\r\n    def initialize_connections(self):\r\n        \"\"\"Return list of connection tuples derived from self.weights.\r\n        This ensures self.connections stays synced with self.weights as source of truth.\"\"\"\r\n        return list(self.weights.keys())\r\n    \r\n    def sync_connections_from_weights(self):\r\n        \"\"\"Sync self.connections list from self.weights (single source of truth).\r\n        Call this after any modification to self.weights.\"\"\"\r\n        self.connections = list(self.weights.keys())\r\n\r\n    def initialize_weights(self):\r\n        \"\"\"Initialize weights with sparse random connections (40% density).\"\"\"\r\n        neurons = list(self.neuron_positions.keys())\r\n        # Create sparse random connections (40% density)\r\n        connection_probability = 0.4\r\n        \r\n        for i in range(len(neurons)):\r\n            for j in range(i+1, len(neurons)):\r\n                if random.random() < connection_probability:\r\n                    self.weights[(neurons[i], neurons[j])] = random.uniform(-1, 1)\r\n        \r\n        # Sync connections list from weights\r\n        self.sync_connections_from_weights()\r\n\r\n    def get_neuron_count(self):\r\n        \"\"\"Returns the actual count of neurons in the network positions.\"\"\"\r\n        return len(self.neuron_positions)\r\n\r\n    def get_edge_count(self):\r\n        \"\"\"Returns the actual count of connections (weights) in the network.\"\"\"\r\n        return len(self.weights) # MODIFIED: Use self.weights\r\n\r\n    def get_weakest_connections(self, n=3):\r\n        \"\"\"Return the n weakest connections by absolute weight\"\"\"\r\n        if not hasattr(self, 'weights') or not self.weights:\r\n            return []\r\n\r\n        sorted_weights = sorted(self.weights.items(), key=lambda x: abs(x[1]))\r\n        return sorted_weights[:n]  # Returns list of ((source, target), weight) tuples\r\n\r\n    def get_extreme_neurons(self, n=3):\r\n        \"\"\"Return neurons deviating most from baseline (50)\"\"\"\r\n        neurons = [(k, v) for k, v in self.state.items()\r\n                if isinstance(v, (int, float)) and k in self.neuron_positions]\r\n        most_positive = sorted(neurons, key=lambda x: -x[1])[:n]\r\n        most_negative = sorted(neurons, key=lambda x: x[1])[:n]\r\n        return {'overactive': most_positive, 'underactive': most_negative}\r\n\r\n    def get_unbalanced_connections(self, n=3):\r\n        \"\"\"Return connections with largest weight disparities\"\"\"\r\n        unbalanced = []\r\n        for (a, b), w1 in self.weights.items():\r\n            w2 = self.weights.get((b, a), 0)\r\n            if abs(w1 - w2) > 0.3:  # Only consider significant differences\r\n                unbalanced.append(((a, b), (w1, w2), abs(w1 - w2)))\r\n        return sorted(unbalanced, key=lambda x: -x[2])[:n]\r\n\r\n    def calculate_network_health(self):\r\n        \"\"\"Calculate network health based on connection weights and neuron activity\"\"\"\r\n        total_weight = sum(abs(w) for w in self.weights.values())\r\n        avg_weight = total_weight / len(self.weights) if self.weights else 0\r\n        health = min(100, max(0, avg_weight * 100))\r\n        return health\r\n\r\n    def calculate_network_efficiency(self):\r\n        \"\"\"Calculate network efficiency based on connection distribution\"\"\"\r\n        if not self.connections:\r\n            return 0\r\n        reciprocal_count = 0\r\n        for conn in self.connections:\r\n            reverse_conn = (conn[1], conn[0])\r\n            if reverse_conn in self.connections:\r\n                reciprocal_count += 1\r\n        efficiency = (reciprocal_count / len(self.connections)) * 100\r\n        return efficiency\r\n\r\n    def log_neurogenesis_event(self, neuron_name, event_type, reason=None, details=None):\r\n        \"\"\"Log neurogenesis events in a human-readable paragraph format.\"\"\"\r\n        \r\n        timestamp = datetime.now().strftime(\"%H:%M:%S\")\r\n        log_entry = \"\"\r\n\r\n        if event_type == \"created\" and details:\r\n            neuron_type = details.get(\"trigger_type\")\r\n            trigger_value = details.get(\"trigger_value\", 0.0)\r\n\r\n            if not neuron_type:\r\n                return # Cannot log without a neuron type\r\n\r\n            # General creation message\r\n            log_entry += f\"{timestamp} - a {neuron_type.upper()} neuron ({neuron_name}) was created because {neuron_type} counter was {trigger_value:.2f}\\n\"\r\n\r\n            # Specific details for stress neurons\r\n            if neuron_type == \"stress\":\r\n                log_entry += \"An inhibitory connection was made to ANXIETY\\n\"\r\n                log_entry += \"Maximum anxiety value has been permanently reduced by 10\\n\"\r\n\r\n        elif event_type == \"pruned\":\r\n            # A more consistent format for pruned events\r\n            timestamp_full = datetime.now().strftime(\"%H:%M:%S\")\r\n            log_entry = f\"{timestamp_full} - a neuron ({neuron_name}) was PRUNED due to {reason if reason else 'unknown reason'}\\n\"\r\n\r\n        if log_entry:\r\n            try:\r\n                with open('neurogenesis_log.txt', 'a', encoding='utf-8') as f:\r\n                    f.write(log_entry)\r\n                    # Always add the separator after an entry\r\n                    f.write(\"\\n-------------------------------\\n\\n\")\r\n            except Exception as e:\r\n                print(f\"\\x1b[31mNeurogenesis logging failed: {str(e)}\\x1b[0m\")\r\n\r\n    # In brain_widget.py\r\n\r\n    def update_state(self, new_state=None):\r\n        \"\"\"\r\n        Update neuron activations. \r\n        If new_state is provided, it is the SOURCE OF TRUTH (Sensor data).\r\n        If None, it is a PHYSICS TICK (Internal decay/noise).\r\n        \"\"\"\r\n        import time\r\n        import random\r\n\r\n        # LIST OF PROTECTED INPUTS\r\n        PURE_INPUTS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \r\n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\", \r\n            \"plant_proximity\"\r\n        }\r\n\r\n        # --- CASE A: External Update (Source of Truth) ---\r\n        if new_state is not None:\r\n            for k, v in (new_state.items() if isinstance(new_state, dict) else []):\r\n                val_to_set = v\r\n                # Enforce binary rules for specific neurons\r\n                if k in self.neuron_positions and self.is_binary_neuron(k):\r\n                    if isinstance(v, bool):\r\n                        val_to_set = 100.0 if v else 0.0\r\n                    elif isinstance(v, (int, float)):\r\n                        val_to_set = 100.0 if float(v) > 50.0 else 0.0\r\n                else:\r\n                    val_to_set = float(v) if isinstance(v, (int, float, bool)) else v\r\n\r\n                # 1. Update Display State\r\n                self.state[k] = val_to_set\r\n\r\n                # 2. Update Internal Simulation State\r\n                if k in self.neurons:\r\n                    self.neurons[k]['activation'] = float(val_to_set)\r\n\r\n            # [Update cache so worker knows the new truth immediately]\r\n            if hasattr(self, 'brain_worker') and self.brain_worker:\r\n                self._update_worker_cache()\r\n\r\n            self.mark_render_dirty()\r\n            self.update()\r\n            return\r\n\r\n        # --- CASE B: Internal Physics Tick (Decay/Noise) ---\r\n        # We must NOT apply decay to PURE_INPUTS\r\n        \r\n        updated = {}\r\n\r\n        # 1. Calculate Decay & Noise\r\n        for neuron, props in self.neurons.items():\r\n            # [FIX] SKIP PURE INPUTS. Do not calculate decay for them.\r\n            if neuron in PURE_INPUTS:\r\n                continue\r\n\r\n            value = props.get('activation', 0.0)\r\n            decay = props.get('decay', 1.0)\r\n            noise = props.get('noise', 0.0)\r\n            value *= decay\r\n            value += random.uniform(-noise, noise)\r\n            updated[neuron] = value\r\n\r\n        # 2. Apply Connections - USE self.weights as source of truth\r\n        for (src, dst), weight in self.weights.items():\r\n            # Get source value (Use current state if it's an input)\r\n            if src in PURE_INPUTS and src not in updated:\r\n                src_val = self.neurons.get(src, {}).get('activation', 0.0)\r\n            elif src in updated:\r\n                src_val = updated[src]\r\n            else:\r\n                continue\r\n\r\n            # [FIX] NEVER modify a PURE_INPUT via connections\r\n            if dst in PURE_INPUTS:\r\n                continue\r\n            \r\n            if dst not in updated:\r\n                continue\r\n\r\n            updated[dst] += src_val * weight\r\n\r\n        # 3. Save Back\r\n        for neuron, val in updated.items():\r\n            clamped = max(min(val, 100), -100)\r\n            if neuron in self.neurons:\r\n                self.neurons[neuron]['activation'] = clamped\r\n            else:\r\n                self.state[neuron] = clamped\r\n\r\n        if updated:\r\n            self.mark_render_dirty()\r\n\r\n    def _perform_state_update_sync(self):\r\n        \"\"\"Synchronous fallback when worker is unavailable\"\"\"\r\n        import random\r\n        \r\n        updated = {}\r\n        # [FIX] Define protected neurons here as well\r\n        PURE_INPUTS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \r\n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\", \r\n            \"plant_proximity\"\r\n        }\r\n        \r\n        # First pass: decay and noise\r\n        for neuron, props in self.neurons.items():\r\n            if neuron in self.excluded_neurons:\r\n                continue\r\n            \r\n            # [FIX] Skip decay for inputs\r\n            if neuron in PURE_INPUTS:\r\n                continue\r\n\r\n            value = props.get('activation', 0)\r\n            decay = props.get('decay', 1.0)\r\n            noise = props.get('noise', 0.0)\r\n            value *= decay\r\n            value += random.uniform(-noise, noise)\r\n            updated[neuron] = value\r\n        \r\n        # Second pass: connection effects - USE self.weights as source of truth\r\n        for (src, dst), weight in self.weights.items():\r\n            # Skip if neurons don't exist in updated state\r\n            if src not in self.state and src not in updated:\r\n                continue\r\n            if dst not in updated:\r\n                continue\r\n            \r\n            # [FIX] Prevent inputs from being modified by connections\r\n            if dst in PURE_INPUTS:\r\n                continue\r\n            \r\n            # Get source value\r\n            if src in updated:\r\n                src_val = updated[src]\r\n            elif src in self.state:\r\n                src_val = self.state[src]\r\n                if isinstance(src_val, bool):\r\n                    src_val = 100.0 if src_val else 0.0\r\n            else:\r\n                continue\r\n                \r\n            if isinstance(src_val, (int, float)):\r\n                updated[dst] += src_val * weight\r\n\r\n            # Use current state for inputs if not in 'updated' list yet\r\n            src_val = updated.get(src, self.state.get(src, 0))\r\n            \r\n            if dst in updated:\r\n                updated[dst] += src_val * weight\r\n        \r\n        # Final pass: clamping\r\n        for neuron, val in updated.items():\r\n            updated[neuron] = max(min(val, 100), -100)\r\n        \r\n        self.state.update(updated)\r\n        self.update()\r\n\r\n\r\n    def _fast_apply_external_state(self, new_state):\r\n        \"\"\"Quickly apply externally provided state (plugin updates, etc.)\"\"\"\r\n        for k, v in new_state.items():\r\n            if k in self.neuron_positions:\r\n                if self.is_binary_neuron(k):\r\n                    self.state[k] = 100.0 if (v if isinstance(v, bool) else float(v) > 50) else 0.0\r\n                else:\r\n                    self.state[k] = float(v) if isinstance(v, (int, float)) else v\r\n        \r\n        # >>> ADDED: Mark render dirty <<<\r\n        self.mark_render_dirty()\r\n\r\n    def _perform_state_update_sync(self):\r\n        \"\"\"Synchronous fallback when worker is unavailable\"\"\"\r\n        import random\r\n        \r\n        updated = {}\r\n        BINARY_NEURONS = {\"can_see_food\"}\r\n        \r\n        # First pass: decay and noise\r\n        for neuron, props in self.neurons.items():\r\n            if neuron in self.excluded_neurons:\r\n                continue\r\n            value = props.get('activation', 0)\r\n            decay = props.get('decay', 1.0)\r\n            noise = props.get('noise', 0.0)\r\n            value *= decay\r\n            value += random.uniform(-noise, noise)\r\n            updated[neuron] = value\r\n        \r\n        # Second pass: connection effects - USE self.weights as source of truth\r\n        for (src, dst), weight in self.weights.items():\r\n            if src in updated and dst in updated:\r\n                updated[dst] += updated[src] * weight\r\n        \r\n        # Final pass: clamping\r\n        for neuron, val in updated.items():\r\n            if neuron in BINARY_NEURONS:\r\n                updated[neuron] = 100.0 if val > 50 else 0.0\r\n            else:\r\n                updated[neuron] = max(min(val, 100), -100)\r\n        \r\n        self.state.update(updated)\r\n        self.update()\r\n\r\n\r\n    def provide_outcome_feedback(self, outcome_value: float):\r\n        \"\"\"\r\n        Provide feedback to recently activated neurons.\r\n        outcome_value: 1.0 = very positive, 0.0 = neutral, -1.0 = very negative\r\n        \"\"\"\r\n        if not hasattr(self, 'enhanced_neurogenesis'):\r\n            return\r\n        \r\n        current_time = time.time()\r\n        \r\n        # Update utility scores for recently active neurons\r\n        for name, func_neuron in self.enhanced_neurogenesis.functional_neurons.items():\r\n            # Was this neuron recently active?\r\n            # Check if func_neuron has the last_activated attribute (for FunctionalNeuron objects)\r\n            if hasattr(func_neuron, 'last_activated') and current_time - func_neuron.last_activated < 30:  # 30 seconds\r\n                if hasattr(func_neuron, 'update_utility_score'):\r\n                    func_neuron.update_utility_score(outcome_value)\r\n\r\n\r\n    \r\n    \r\n    def check_neurogenesis_triggers(self, state):\r\n        \"\"\"Enhanced neurogenesis check with proper trigger priority\"\"\"\r\n        if not state.get('neurogenesis_active', True):\r\n            return False\r\n        \r\n        # Build experience context\r\n        recent_actions = state.get('recent_actions', [])\r\n        environment = {\r\n            'food_count': state.get('food_count', 0),\r\n            'poop_count': state.get('poop_count', 0),\r\n            'has_rock': state.get('carrying_rock', False)\r\n        }\r\n        \r\n        # ===== FIX: PROPER TRIGGER PRIORITY WITH EMERGENCY OVERRIDE =====\r\n        trigger_type = None\r\n        \r\n        # 🚨 HIGHEST PRIORITY: Emergency stress (overrides everything)\r\n        if state.get('anxiety', 50) >= 95:\r\n            trigger_type = 'stress'\r\n            print(f\"🚨 EMERGENCY: Critical anxiety level ({state.get('anxiety', 50):.1f})!\")\r\n        \r\n        # HIGH PRIORITY: Stress (anxiety or sustained stress)\r\n        elif state.get('anxiety', 50) > 75 or state.get('sustained_stress', 0) > 1:\r\n            trigger_type = 'stress'\r\n        \r\n        # MEDIUM PRIORITY: Novelty (curiosity or new objects)\r\n        elif state.get('curiosity', 50) > 70 or state.get('novelty_exposure', 0) > 2:\r\n            trigger_type = 'novelty'\r\n        \r\n        # LOW PRIORITY: Reward (satisfaction or positive outcomes)\r\n        elif state.get('satisfaction', 50) > 70 or state.get('recent_rewards', 0) > 2:\r\n            trigger_type = 'reward'\r\n        # ================================================================\r\n        \r\n        if trigger_type:\r\n            # Capture full experience context\r\n            context = self.enhanced_neurogenesis.capture_experience_context(\r\n                trigger_type=trigger_type,\r\n                brain_state=self.state,\r\n                recent_actions=recent_actions,\r\n                environment=environment\r\n            )\r\n            \r\n            # Add to experience buffer\r\n            self.experience_buffer.add_experience(context)\r\n            \r\n            # Check if we should create a neuron\r\n            if self.enhanced_neurogenesis.should_create_neuron(context):\r\n                neuron_name = self.enhanced_neurogenesis.create_functional_neuron(context)\r\n                \r\n                if neuron_name and self.pruning_enabled:\r\n                    # Check if we need to prune\r\n                    current_count = len(self.neuron_positions) - len(self.excluded_neurons)\r\n                    max_neurons = self.neurogenesis_config.get('max_neurons', 32)\r\n                    \r\n                    if current_count > max_neurons * 0.85:\r\n                        self.enhanced_neurogenesis.intelligent_pruning()\r\n                \r\n                return neuron_name is not None\r\n        \r\n        return False\r\n\r\n    def get_neurogenesis_threshold(self, trigger_type):\r\n        \"\"\"Safely get threshold for a trigger type with fallback defaults\"\"\"\r\n        try:\r\n            return self.neurogenesis_config['triggers'][trigger_type]['threshold']\r\n        except KeyError:\r\n            defaults = {'novelty': 0.7, 'stress': 0.8, 'reward': 0.6}\r\n            return defaults.get(trigger_type, 1.0)\r\n\r\n    def stimulate_brain(self, stimulation_values):\r\n        \"\"\"Handle brain stimulation with validation\"\"\"\r\n        if not isinstance(stimulation_values, dict):\r\n            return\r\n        filtered_update = {}\r\n        for key in self.state.keys():\r\n            if key in stimulation_values:\r\n                filtered_update[key] = stimulation_values[key]\r\n        self.update_state(filtered_update)\r\n\r\n    def get_adjusted_threshold(self, base_threshold, trigger_type):\r\n        \"\"\"Scale threshold based on network size to prevent runaway neurogenesis\"\"\"\r\n        original_count = len(self.original_neuron_positions)\r\n        current_count = len(self.neuron_positions) - len(self.excluded_neurons)\r\n        new_neuron_count = current_count - original_count\r\n        baseline = original_count + 3\r\n        if new_neuron_count <= 0 or current_count <= baseline:\r\n            return base_threshold\r\n        scaling_factors = {'novelty': 0.25, 'stress': 0.1, 'reward': 0.08}\r\n        scaling_factor = scaling_factors.get(trigger_type, 0.15)\r\n        multiplier = 1.0 + (scaling_factor * (new_neuron_count - baseline + 1))\r\n        adjusted = base_threshold * multiplier\r\n        return adjusted\r\n\r\n    def prune_weak_neurons(self):\r\n        \"\"\"Remove weakly connected or inactive neurons, skipping connectors.\"\"\"\r\n        min_neurons = len(self.original_neuron_positions)\r\n        current_count = len(self.neuron_positions) - len(self.excluded_neurons)\r\n        if current_count <= min_neurons:\r\n            return False\r\n\r\n        candidates = []\r\n        for neuron in list(self.neuron_positions.keys()):\r\n            # Basic exclusions\r\n            if neuron in self.original_neuron_positions or neuron in self.excluded_neurons:\r\n                continue\r\n            \r\n            # IMMUNITY: Connector neurons are structural and cannot be pruned\r\n            if self.is_connector_neuron(neuron):\r\n                continue\r\n\r\n            connections = [abs(w) for (a, b), w in self.weights.items() if (a == neuron or b == neuron)]\r\n            activity = self.state.get(neuron, 0)\r\n            activity_score = 0 if isinstance(activity, bool) else abs(activity - 50)\r\n            \r\n            if not connections or sum(connections) / len(connections) < 0.2:\r\n                candidates.append((neuron, 1))\r\n            elif activity_score < 10:\r\n                candidates.append((neuron, 2))\r\n        \r\n        candidates.sort(key=lambda x: x[1])\r\n\r\n        if candidates:\r\n            neuron_to_remove = candidates[0][0]\r\n            if neuron_to_remove in self.neuron_positions:\r\n                del self.neuron_positions[neuron_to_remove]\r\n            if neuron_to_remove in self.state:\r\n                del self.state[neuron_to_remove]\r\n            if neuron_to_remove in self.neuron_shapes:\r\n                del self.neuron_shapes[neuron_to_remove]\r\n            if neuron_to_remove in self.state_colors:\r\n                del self.state_colors[neuron_to_remove]\r\n                \r\n            for conn in list(self.weights.keys()):\r\n                if isinstance(conn, tuple) and (conn[0] == neuron_to_remove or conn[1] == neuron_to_remove):\r\n                    del self.weights[conn]\r\n                    \r\n            if neuron_to_remove in self.neurogenesis_data.get('new_neurons', []):\r\n                self.neurogenesis_data['new_neurons'].remove(neuron_to_remove)\r\n                \r\n            reason = \"weak connections/activity\"\r\n            self.log_neurogenesis_event(neuron_to_remove, \"pruned\", reason)\r\n            \r\n            self.mark_render_dirty()\r\n            self.update()\r\n            return True\r\n        return False\r\n\r\n    def apply_repulsion_force(self, iterations=15, strength=0.6, threshold=120.0):\r\n        \"\"\"Applies repulsion force and enforces boundary constraints from config.\"\"\"\r\n        \r\n        neuron_props = self.config.neurogenesis.get('neuron_properties', {})\r\n        force_bounds = neuron_props.get('force_bounds', True)\r\n        centering_force = neuron_props.get('centering_force', 0.02)\r\n        padding = neuron_props.get('canvas_padding', 60)\r\n        \r\n        # Logical canvas center\r\n        center_x, center_y = 512, 384\r\n        min_x, max_x = padding, 1024 - padding\r\n        min_y, max_y = padding, 768 - padding\r\n\r\n        neuron_list = [name for name in self.neuron_positions.keys() if name not in self.excluded_neurons]\r\n\r\n        for _ in range(iterations):\r\n            displacements = {name: [0.0, 0.0] for name in neuron_list}\r\n\r\n            # 1. Repulsion between neurons\r\n            for i in range(len(neuron_list)):\r\n                for j in range(i + 1, len(neuron_list)):\r\n                    neuron1 = neuron_list[i]\r\n                    neuron2 = neuron_list[j]\r\n\r\n                    pos1 = self.neuron_positions[neuron1]\r\n                    pos2 = self.neuron_positions[neuron2]\r\n\r\n                    dx = pos1[0] - pos2[0]\r\n                    dy = pos1[1] - pos2[1]\r\n\r\n                    distance_sq = dx*dx + dy*dy\r\n\r\n                    if 0 < distance_sq < threshold*threshold:\r\n                        distance = math.sqrt(distance_sq)\r\n                        force = strength * (threshold - distance) / distance\r\n                        move_x = (dx / distance) * force\r\n                        move_y = (dy / distance) * force\r\n                        displacements[neuron1][0] += move_x\r\n                        displacements[neuron1][1] += move_y\r\n                        displacements[neuron2][0] -= move_x\r\n                        displacements[neuron2][1] -= move_y\r\n\r\n            # 2. Apply movements + Centering Force + Boundary Constraints\r\n            damping = 0.5\r\n            moved = False\r\n            \r\n            for name in neuron_list:\r\n                # Skip core neurons IF you want them fixed, but requirement says randomise start\r\n                # so we allow them to move, but maybe less? For now, move all.\r\n                \r\n                pos = self.neuron_positions[name]\r\n                disp = displacements[name]\r\n                \r\n                # Apply Centering Force (pull towards middle)\r\n                if centering_force > 0:\r\n                    dir_to_center_x = center_x - pos[0]\r\n                    dir_to_center_y = center_y - pos[1]\r\n                    disp[0] += dir_to_center_x * centering_force\r\n                    disp[1] += dir_to_center_y * centering_force\r\n\r\n                if abs(disp[0]) > 0.1 or abs(disp[1]) > 0.1:\r\n                    new_x = pos[0] + disp[0] * damping\r\n                    new_y = pos[1] + disp[1] * damping\r\n                    \r\n                    # Force Bounds\r\n                    if force_bounds:\r\n                        new_x = max(min_x, min(max_x, new_x))\r\n                        new_y = max(min_y, min(max_y, new_y))\r\n                        \r\n                    self.neuron_positions[name] = (new_x, new_y)\r\n                    moved = True\r\n\r\n            if not moved:\r\n                break\r\n        if moved:\r\n           self.update()\r\n\r\n\r\n    def update_weights(self):\r\n\r\n        if self.frozen_weights is not None:\r\n            return\r\n\r\n        # Iterate over a copy of the actual weight keys ---\r\n        for conn in list(self.weights.keys()):\r\n            # Check if the connection still exists (it might be pruned concurrently)\r\n            if conn in self.weights:\r\n                # Clamp the weight to stay within [-1, 1]\r\n                self.weights[conn] = max(-1, min(1, self.weights[conn]))\r\n\r\n    def freeze_weights(self):\r\n        self.frozen_weights = self.weights.copy()\r\n\r\n    def unfreeze_weights(self):\r\n        self.frozen_weights = None\r\n\r\n    def strengthen_connection(self, neuron1, neuron2, amount):\r\n        \"\"\"Strengthen a connection, respecting connector limits for new links.\"\"\"\r\n        pair = (neuron1, neuron2)\r\n        reverse_pair = (neuron2, neuron1)\r\n        \r\n        exists = pair in self.weights or reverse_pair in self.weights\r\n        use_pair = pair if pair in self.weights else reverse_pair\r\n\r\n        # --- CONNECTOR LIMIT CHECK ---\r\n        if not exists:\r\n            if self.is_connector_neuron(neuron1) and self.get_neuron_degree(neuron1) >= 3:\r\n                return\r\n            if self.is_connector_neuron(neuron2) and self.get_neuron_degree(neuron2) >= 3:\r\n                return\r\n            # If we pass checks, create the connection\r\n            self.weights[pair] = 0.0\r\n            use_pair = pair\r\n\r\n        self.weights[use_pair] += amount\r\n        self.weights[use_pair] = max(-1, min(1, self.weights[use_pair]))\r\n        \r\n        self.mark_render_dirty()\r\n        self.update()\r\n\r\n    def capture_training_data(self, state):\r\n        training_sample = [state[neuron] for neuron in self.neuron_positions.keys()]\r\n        self.training_data.append(training_sample)\r\n        print(\"Captured training data:\", training_sample)\r\n\r\n    def train_hebbian(self):\r\n        print(\"Starting Hebbian training...\")\r\n        for sample in self.training_data:\r\n            self.associations = self.compute_backend.hebbian(\r\n                self.associations, sample, self.learning_rate\r\n            )\r\n        self.training_data = []\r\n\r\n    def get_association_strength(self, neuron1, neuron2):\r\n        idx1 = list(self.neuron_positions.keys()).index(neuron1)\r\n        idx2 = list(self.neuron_positions.keys()).index(neuron2)\r\n        return self.compute_backend.get_value(self.associations, idx1, idx2)\r\n    \r\n    def draw_layers(self, painter, scale):\r\n        \"\"\"Draw background rectangles for custom layers (matches BrainDesigner).\"\"\"\r\n        if not self.layers:                       # ← already stored by load_brain_state\r\n            return\r\n\r\n        for layer in self.layers:\r\n            y_pos   = layer.get('y_position', 0)\r\n            name    = layer.get('name', 'Layer')\r\n            l_type  = layer.get('layer_type', 'hidden')\r\n\r\n            # ---- same metrics as BrainDesigner ----\r\n            rect_height = 120\r\n            rect_top    = y_pos - rect_height / 2\r\n            rect_left   = -200\r\n            rect_width  = 2000\r\n\r\n            # ---- same colours / alpha ----\r\n            if l_type == 'input':\r\n                color = QtGui.QColor(200, 255, 200, 80)\r\n                border_color = QtGui.QColor(150, 220, 150, 120)\r\n            elif l_type == 'output':\r\n                color = QtGui.QColor(255, 200, 200, 80)\r\n                border_color = QtGui.QColor(220, 150, 150, 120)\r\n            else:   # hidden\r\n                color = QtGui.QColor(230, 230, 255, 80)\r\n                border_color = QtGui.QColor(200, 200, 240, 120)\r\n\r\n            # ---- filled background ----\r\n            painter.setBrush(QtGui.QBrush(color))\r\n            painter.setPen(QtCore.Qt.NoPen)\r\n            painter.drawRect(QtCore.QRectF(rect_left, rect_top,\r\n                                         rect_width, rect_height))\r\n\r\n            # ---- top & bottom border lines ----\r\n            painter.setPen(QtGui.QPen(border_color, 2))\r\n            painter.drawLine(QtCore.QLineF(rect_left, rect_top,\r\n                                         rect_left + rect_width, rect_top))\r\n            painter.drawLine(QtCore.QLineF(rect_left, rect_top + rect_height,\r\n                                         rect_left + rect_width, rect_top + rect_height))\r\n\r\n            # ---- layer label ----\r\n            font = painter.font()\r\n            font.setPointSize(int(10 * scale))\r\n            font.setBold(True)\r\n            painter.setFont(font)\r\n            painter.setPen(QtGui.QColor(100, 100, 120))\r\n            painter.drawText(QtCore.QPointF(10, rect_top + 20),\r\n                           f\"{name} ({l_type})\")\r\n\r\n    def draw_connections(self, painter, scale):\r\n        \"\"\"\r\n        Draw connections with extended 2-second weight-change animations.\r\n        Links are forced INVISIBLE while core neurons are still being revealed.\r\n        Includes special visualization for Stress->Anxiety inhibitory links.\r\n        \"\"\"\r\n        if not self.show_links:\r\n            return\r\n\r\n        # ===== PERFORMANCE FIX: Skip if widget is hidden =====\r\n        if not self.isVisible():\r\n            return\r\n\r\n        # ===== PERFORMANCE TRACKING =====\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _conn_start = time.perf_counter()\r\n\r\n        # absolutely no connections until every core neuron is completely revealed (tutorial mode guard). \r\n        if self.is_tutorial_mode and len(self.visible_neurons) < len(self.original_neurons):\r\n            return\r\n        \r\n        # ===== NEURAL STYLE: Use dedicated neural renderer =====\r\n        if self.anim_neural_pulse_enabled:\r\n            self._draw_neural_connections(painter, scale)\r\n            return\r\n\r\n        current_time = time.time()\r\n        connections_drawn = 0\r\n        connections_skipped = 0\r\n\r\n        for key, weight in self.weights.items():\r\n            if not isinstance(key, tuple) or len(key) != 2:\r\n                continue\r\n            source, target = key\r\n\r\n            # skip tutorial-only connections when not in tutorial\r\n            tutorial_mode = getattr(self.parent(), 'tutorial_active', False)\r\n            is_tutorial_conn = (source.startswith('tutorial_neuron_') or\r\n                                target.startswith('tutorial_neuron_'))\r\n            if is_tutorial_conn and not tutorial_mode:\r\n                continue\r\n\r\n            # skip if either neuron is still revealing (tutorial safety)\r\n            if self.is_tutorial_mode and (\r\n                    not self.is_neuron_revealed(source) or\r\n                    not self.is_neuron_revealed(target)):\r\n                connections_skipped += 1\r\n                continue\r\n\r\n            if (source not in self.neuron_positions or\r\n                target not in self.neuron_positions or\r\n                source in self.excluded_neurons or\r\n                target in self.excluded_neurons):\r\n                continue\r\n\r\n            # skip connections involving non-visible core neurons (tutorial)\r\n            if self.is_tutorial_mode:\r\n                if source in self.original_neurons and source not in self.visible_neurons:\r\n                    connections_skipped += 1\r\n                    continue\r\n                if target in self.original_neurons and target not in self.visible_neurons:\r\n                    connections_skipped += 1\r\n                    continue\r\n\r\n            start = self.neuron_positions[source]\r\n            end   = self.neuron_positions[target]\r\n            start_point = QtCore.QPointF(float(start[0]), float(start[1]))\r\n            end_point   = QtCore.QPointF(float(end[0]),   float(end[1]))\r\n\r\n            # === SPECIAL STYLING: Stress -> Anxiety (Dotted Red with Moving Arrows) ===\r\n            # Detect connection between any stress neuron and anxiety\r\n            is_stress_to_anxiety = (\r\n                (source.lower().startswith('stress') and target.lower() == 'anxiety') or\r\n                (target.lower().startswith('stress') and source.lower() == 'anxiety'))\r\n            \r\n            if is_stress_to_anxiety:\r\n                # 1. Draw Dotted Red Line\r\n                # Use a bright red color to indicate inhibitory/stress signal\r\n                pen = QtGui.QPen(QtGui.QColor(255, 60, 60)) \r\n                pen.setWidth(max(2, int(3 * scale))) # Thicker than normal for visibility\r\n                pen.setStyle(QtCore.Qt.DotLine)\r\n                painter.setPen(pen)\r\n                painter.drawLine(start_point, end_point)\r\n                \r\n                # 2. Draw Moving Arrows (Flowing towards Anxiety)\r\n                # Determine which end is Anxiety\r\n                if target.lower() == 'anxiety':\r\n                    actual_start = start_point\r\n                    actual_end = end_point\r\n                else:\r\n                    actual_start = end_point\r\n                    actual_end = start_point\r\n                \r\n                # Vector math for the line\r\n                dx = actual_end.x() - actual_start.x()\r\n                dy = actual_end.y() - actual_start.y()\r\n                length = math.sqrt(dx*dx + dy*dy)\r\n                \r\n                if length > 0:\r\n                    # Unit vector\r\n                    ux = dx / length\r\n                    uy = dy / length\r\n                    \r\n                    # Arrow animation parameters\r\n                    arrow_spacing = 40 * scale   # Distance between arrows\r\n                    speed = 60 * scale           # Pixels per second\r\n                    \r\n                    # Offset based on time to create movement\r\n                    offset = (current_time * speed) % arrow_spacing\r\n                    \r\n                    painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 0, 0)))\r\n                    painter.setPen(QtCore.Qt.NoPen)\r\n                    \r\n                    current_dist = offset\r\n                    while current_dist < length:\r\n                        # Calculate position of arrow center on the line\r\n                        ax = actual_start.x() + ux * current_dist\r\n                        ay = actual_start.y() + uy * current_dist\r\n                        \r\n                        # Draw triangle (Arrowhead)\r\n                        arrow_size = 6 * scale\r\n                        \r\n                        # Perpendicular vector for arrowhead width\r\n                        px = -uy * arrow_size\r\n                        py = ux * arrow_size\r\n                        \r\n                        # Tip of arrow (pointing forward)\r\n                        tip_x = ax + ux * arrow_size\r\n                        tip_y = ay + uy * arrow_size\r\n                        \r\n                        # Base corners of arrow\r\n                        base_x = ax - ux * arrow_size\r\n                        base_y = ay - uy * arrow_size\r\n                        \r\n                        points = [\r\n                            QtCore.QPointF(tip_x, tip_y),\r\n                            QtCore.QPointF(base_x + px, base_y + py),\r\n                            QtCore.QPointF(base_x - px, base_y - py)\r\n                        ]\r\n                        painter.drawPolygon(QtGui.QPolygonF(points))\r\n                        \r\n                        current_dist += arrow_spacing\r\n\r\n                # Skip standard drawing logic for this connection\r\n                connections_drawn += 1\r\n                continue\r\n\r\n            # === STANDARD CONNECTION DRAWING ===\r\n            anim_weight = weight\r\n            base_width  = self.anim_line_base_width * scale\r\n            line_width  = base_width\r\n            pen_style   = QtCore.Qt.SolidLine\r\n            animating   = False\r\n            pulse_progress = 0.0\r\n\r\n            # check for active weight-change animations (2-second window)\r\n            for anim in self.weight_animations:\r\n                if anim['pair'] == key:\r\n                    elapsed = current_time - anim['start_time']\r\n                    if elapsed < anim['duration']:\r\n                        progress = elapsed / anim['duration']\r\n                        anim_weight = anim['start_weight'] + progress * (\r\n                            anim['end_weight'] - anim['start_weight'])\r\n\r\n                        # growing / shrinking line width\r\n                        if progress < 0.5:\r\n                            line_width = base_width + (6.0 * scale * progress * 2)\r\n                        else:\r\n                            line_width = base_width + (6.0 * scale * (1 - progress) * 2)\r\n\r\n                        pulse_progress = progress * anim['pulse_speed']\r\n                        animating = True\r\n                        break\r\n\r\n            # colour & alpha\r\n            if animating:\r\n                r, g, b = anim['color']\r\n                alpha = int(255 * (1 - pulse_progress ** 2))\r\n                color = QtGui.QColor(r, g, b, alpha)\r\n            else:\r\n                base_alpha = int(self._link_opacities.get(key, 0.0) * 255)\r\n                \r\n                # ===== VIBRANT STYLE: Apply ambient pulse =====\r\n                if self.anim_ambient_pulse_enabled and base_alpha > 0:\r\n                    pulse_state = self._get_or_create_ambient_pulse(key)\r\n                    phase = pulse_state['phase']\r\n                    \r\n                    # Sinusoidal oscillation (0 to 1 range)\r\n                    pulse_factor = (math.sin(phase) + 1.0) / 2.0\r\n                    \r\n                    # Interpolate width\r\n                    w_min, w_max = self.anim_ambient_pulse_width_range\r\n                    width_mult = w_min + pulse_factor * (w_max - w_min)\r\n                    line_width = base_width * width_mult\r\n                    \r\n                    # Interpolate alpha\r\n                    a_min, a_max = self.anim_ambient_pulse_alpha_range\r\n                    pulse_alpha = int(a_min + pulse_factor * (a_max - a_min))\r\n                    # Combine with base opacity\r\n                    final_alpha = int((base_alpha / 255.0) * pulse_alpha)\r\n                    \r\n                    if weight > 0:\r\n                        color = QtGui.QColor(self.anim_line_col_pos[0], \r\n                                           self.anim_line_col_pos[1], \r\n                                           self.anim_line_col_pos[2], final_alpha)\r\n                    else:\r\n                        color = QtGui.QColor(self.anim_line_col_neg[0], \r\n                                           self.anim_line_col_neg[1], \r\n                                           self.anim_line_col_neg[2], final_alpha)\r\n                else:\r\n                    # Standard coloring (classic style or opacity 0)\r\n                    color = (QtGui.QColor(0, int(255 * abs(weight)), 0, base_alpha) if weight > 0 else\r\n                            QtGui.QColor(int(255 * abs(weight)), 0, 0, base_alpha))\r\n\r\n            # line style\r\n            if is_tutorial_conn and tutorial_mode:\r\n                color = QtGui.QColor(255, 255, 0, 180)\r\n                line_width = 3\r\n                pen_style = QtCore.Qt.DashLine\r\n            else:\r\n                pen_style = (QtCore.Qt.DashLine if weight < 0 else\r\n                            QtCore.Qt.SolidLine)\r\n                if abs(anim_weight) < 0.1:\r\n                    pen_style = QtCore.Qt.DotLine\r\n\r\n            painter.setPen(QtGui.QPen(color, line_width, pen_style))\r\n            painter.drawLine(start_point, end_point)\r\n\r\n            # pulse circle travelling along the wire (uses animation style params)\r\n            if self.anim_pulse_enabled and animating and pulse_progress < 1.0:\r\n                pulse_pos = pulse_progress\r\n                pulse_x = start_point.x() + pulse_pos * (end_point.x() - start_point.x())\r\n                pulse_y = start_point.y() + pulse_pos * (end_point.y() - start_point.y())\r\n                pulse_size = self.anim_pulse_diameter * scale * (1 - pulse_progress ** 2)\r\n\r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(*self.anim_pulse_colour, self.anim_pulse_alpha)))\r\n                painter.setPen(QtCore.Qt.NoPen)\r\n                painter.drawEllipse(QtCore.QPointF(pulse_x, pulse_y),\r\n                                    pulse_size, pulse_size)\r\n\r\n            # glow around the line (uses animation style params)\r\n            if self.anim_glow_enabled and animating and progress < self.anim_glow_fade_threshold:\r\n                glow_progress = progress / self.anim_glow_fade_threshold\r\n                glow_width = line_width + 4 * scale * (1 - glow_progress)\r\n                glow_color = QtGui.QColor(*self.anim_glow_colour, self.anim_glow_alpha)\r\n                painter.setPen(QtGui.QPen(glow_color, glow_width, pen_style))\r\n                painter.drawLine(start_point, end_point)\r\n            \r\n            # ===== SUBTLE STYLE: Draw communication glow packets =====\r\n            if self.anim_comm_glow_enabled:\r\n                self._draw_comm_glows_for_connection(\r\n                    painter, scale, key, start_point, end_point\r\n                )\r\n\r\n            # weight text (optional)\r\n            if self.show_weights and abs(weight) > 0.1:\r\n                # Calculate angle for rotation\r\n                dx = end_point.x() - start_point.x()\r\n                dy = end_point.y() - start_point.y()\r\n                angle_deg = math.degrees(math.atan2(dy, dx))\r\n                \r\n                # Ensure text is readable (not upside down)\r\n                if angle_deg > 90:\r\n                    angle_deg -= 180\r\n                elif angle_deg < -90:\r\n                    angle_deg += 180\r\n\r\n                midpoint = QtCore.QPointF((start_point.x() + end_point.x()) / 2,\r\n                                        (start_point.y() + end_point.y()) / 2)\r\n                \r\n                text_str = f\"{weight:.2f}\"\r\n                \r\n                # Font config\r\n                font_size = max(7, int(8 * scale))\r\n                padding = 4 * scale\r\n                \r\n                font = painter.font()\r\n                font.setPointSize(font_size)\r\n                font.setBold(True)\r\n                painter.setFont(font)\r\n                \r\n                fm = painter.fontMetrics()\r\n                text_w = fm.horizontalAdvance(text_str)\r\n                text_h = fm.height()\r\n\r\n                painter.save()\r\n                painter.translate(midpoint)\r\n                painter.rotate(angle_deg)\r\n                \r\n                # Draw background pill\r\n                rect = QtCore.QRectF(-text_w/2 - padding, -text_h/2, \r\n                                     text_w + padding*2, text_h)\r\n                \r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 255, 255, 220)))\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, 150), 1))\r\n                painter.drawRoundedRect(rect, 4, 4)\r\n                \r\n                # Draw text\r\n                text_color = QtGui.QColor(0, 100, 0) if weight >= 0 else QtGui.QColor(150, 0, 0)\r\n                painter.setPen(text_color)\r\n                painter.drawText(rect, QtCore.Qt.AlignCenter, text_str)\r\n                \r\n                painter.restore()\r\n\r\n            connections_drawn += 1\r\n\r\n        # debug guard: warn only when something looks wrong\r\n        if connections_drawn == 0 and len(self.weights) > 0:\r\n            print(f\"🔴 NO CONNECTIONS DRAWN! tutorial_mode={self.is_tutorial_mode}, \"\r\n                f\"visible_neurons={len(self.visible_neurons)}, original_neurons={len(self.original_neurons)}, \"\r\n                f\"total_weights={len(self.weights)}, skipped={connections_skipped}\")\r\n        \r\n        # ===== END PERFORMANCE TRACKING =====\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _conn_elapsed = (time.perf_counter() - _conn_start) * 1000\r\n            perf_tracker.record(\"draw_connections\", _conn_elapsed)\r\n\r\n    def _get_logical_coords(self, widget_pos):\r\n        \"\"\"\r\n        Maps widget pixels back to logical neuron coordinates.\r\n        MUST MATCH brain_render_worker.py scaling logic exactly.\r\n        \"\"\"\r\n        # Constants from render worker\r\n        indicator_space = 0 \r\n        base_width = 1024\r\n        base_height = 768 - indicator_space\r\n        \r\n        # Calculate Scale\r\n        scale_x = self.width() / base_width\r\n        scale_y = (self.height() - indicator_space) / max(1, base_height)\r\n        scale = max(0.01, min(scale_x, scale_y))\r\n        \r\n        # Calculate Offset (centering)\r\n        offset_x = 0\r\n        if scale_x > scale_y:\r\n            content_width = base_width * scale\r\n            offset_x = (self.width() - content_width) / 2\r\n            \r\n        # Inverse Transform: (Screen - Offset) / Scale = Logical\r\n        lx = (widget_pos.x() - offset_x) / scale\r\n        ly = (widget_pos.y() - indicator_space) / scale\r\n        \r\n        return QtCore.QPointF(lx, ly)\r\n\r\n    def get_connection_at_pos(self, widget_pos):\r\n        logical_pos = self._get_logical_coords(widget_pos)\r\n        threshold = 15.0  # logical pixels tolerance\r\n        closest_dist = float('inf')\r\n        closest_conn = None\r\n\r\n        # Helper to convert tuple coords to QPointF\r\n        to_pt = lambda c: QtCore.QPointF(c[0], c[1])\r\n\r\n        for (u, v) in self.weights.keys():\r\n            # Skip invisible/excluded\r\n            if u not in self.neuron_positions or v not in self.neuron_positions: continue\r\n            if u in self.excluded_neurons or v in self.excluded_neurons: continue\r\n            if not self.show_links: continue\r\n\r\n            # If strictly 2-element tuple\r\n            p1 = to_pt(self.neuron_positions[u])\r\n            p2 = to_pt(self.neuron_positions[v])\r\n            \r\n            d2 = self._dist_to_segment_squared(logical_pos, p1, p2)\r\n            \r\n            if d2 < threshold**2 and d2 < closest_dist:\r\n                closest_dist = d2\r\n                closest_conn = (u, v)\r\n\r\n        return closest_conn\r\n\r\n    def _draw_connection_highlight(self, painter):\r\n        if not self.hovered_connection:\r\n            return\r\n\r\n        u, v = self.hovered_connection\r\n        if u not in self.neuron_positions or v not in self.neuron_positions:\r\n            return\r\n\r\n        indicator_space = 0\r\n        base_width = 1024\r\n        base_height = 768 - indicator_space\r\n        \r\n        scale_x = self.width() / base_width\r\n        scale_y = (self.height() - indicator_space) / max(1, base_height)\r\n        scale = max(0.01, min(scale_x, scale_y))\r\n        \r\n        offset_x = 0\r\n        if scale_x > scale_y:\r\n            content_width = base_width * scale\r\n            offset_x = (self.width() - content_width) / 2\r\n\r\n        pos1 = self.neuron_positions[u]\r\n        pos2 = self.neuron_positions[v]\r\n\r\n        # Apply Transform: Logical * Scale + Offset = Screen\r\n        x1 = pos1[0] * scale + offset_x\r\n        y1 = pos1[1] * scale + indicator_space\r\n        x2 = pos2[0] * scale + offset_x\r\n        y2 = pos2[1] * scale + indicator_space\r\n\r\n        p1 = QtCore.QPointF(x1, y1)\r\n        p2 = QtCore.QPointF(x2, y2)\r\n\r\n        # Draw Glow/Highlight\r\n        painter.save()\r\n        # Yellow, thick, slightly transparent\r\n        pen = QtGui.QPen(QtGui.QColor(255, 255, 0, 200), 6 * scale)\r\n        pen.setCapStyle(QtCore.Qt.RoundCap)\r\n        painter.setPen(pen)\r\n        painter.drawLine(p1, p2)\r\n        \r\n        # Label (Weight Value)\r\n        weight = self.weights.get(self.hovered_connection, 0.0)\r\n        mid = (p1 + p2) / 2\r\n        \r\n        font = painter.font()\r\n        font.setPointSize(max(10, int(12 * scale)))\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        \r\n        label = f\"{weight:.2f}\"\r\n        fm = painter.fontMetrics()\r\n        w = fm.horizontalAdvance(label) + 10\r\n        h = fm.height() + 4\r\n        r = QtCore.QRectF(mid.x() - w/2, mid.y() - h/2, w, h)\r\n        \r\n        painter.setPen(QtCore.Qt.NoPen)\r\n        painter.setBrush(QtGui.QColor(0, 0, 0, 180))\r\n        painter.drawRoundedRect(r, 4, 4)\r\n        \r\n        painter.setPen(QtGui.QColor(255, 255, 0))\r\n        painter.drawText(r, QtCore.Qt.AlignCenter, label)\r\n        \r\n        painter.restore()\r\n\r\n    def _dist_to_segment_squared(self, p, v, w):\r\n        l2 = (v.x() - w.x())**2 + (v.y() - w.y())**2\r\n        if l2 == 0: return (p.x() - v.x())**2 + (p.y() - v.y())**2\r\n        t = ((p.x() - v.x()) * (w.x() - v.x()) + (p.y() - v.y()) * (w.y() - v.y())) / l2\r\n        t = max(0, min(1, t))\r\n        dist_x = p.x() - (v.x() + t * (w.x() - v.x()))\r\n        dist_y = p.y() - (v.y() + t * (w.y() - v.y()))\r\n        return dist_x**2 + dist_y**2\r\n\r\n    def get_neuron_at_pos(self, widget_pos):\r\n        \"\"\"Finds a neuron at the given QPoint widget coordinates.\"\"\"\r\n        logical_pos = self._get_logical_coords(widget_pos)\r\n        neuron_radius = 50  # Increased to 50 for better click detection on all neurons\r\n        for name, pos in self.neuron_positions.items():\r\n            dist_sq = (logical_pos.x() - pos[0])**2 + (logical_pos.y() - pos[1])**2\r\n            if dist_sq <= neuron_radius**2:\r\n                return name\r\n        return None\r\n\r\n\r\n    # ------------------------------------------------------------------\r\n    # Binary neuron detection\r\n    # ------------------------------------------------------------------\r\n    def is_binary_neuron(self, name: str) -> bool:\r\n        \"\"\"Neurons that are strictly on/off (not continuous stats).\"\"\"\r\n        return name in {\r\n            'can_see_food', 'is_eating', 'is_sleeping', 'is_sick',\r\n            'pursuing_food', 'is_fleeing', 'is_startled',\r\n            'external_stimulus', 'plant_proximity'\r\n        }\r\n\r\n    # ------------------------------------------------------------------\r\n\r\n    def paintEvent(self, event):\r\n        \"\"\"\r\n        Optimized paintEvent that uses cached offscreen render.\r\n        \r\n        The heavy rendering is done in BrainRenderWorker. This method\r\n        just blits the cached image and draws any overlay elements\r\n        that need to be responsive (like hover effects).\r\n        \"\"\"\r\n        # Performance tracking\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _paint_start = time.perf_counter()\r\n            perf_tracker.increment(\"paint_calls\")\r\n        \r\n        painter = QtGui.QPainter(self)\r\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\r\n        \r\n        # Draw cached render if available\r\n        if self._cached_render and not self._cached_render.isNull():\r\n            # Scale cached image to widget size if needed\r\n            if (self._cached_render.width() != self.width() or \r\n                self._cached_render.height() != self.height()):\r\n                # Request new render at correct size\r\n                self._render_dirty = True\r\n                # Draw scaled version for now\r\n                scaled = self._cached_render.scaled(\r\n                    self.width(), self.height(),\r\n                    QtCore.Qt.KeepAspectRatio,\r\n                    QtCore.Qt.SmoothTransformation\r\n                )\r\n                painter.drawImage(0, 0, scaled)\r\n            else:\r\n                painter.drawImage(0, 0, self._cached_render)\r\n        else:\r\n            # No cached render yet - draw background and request render\r\n            bg_color = QtGui.QColor(*self.anim_background_colour)\r\n            painter.fillRect(self.rect(), bg_color)\r\n            self._render_dirty = True\r\n        \r\n        # Draw overlay elements that need immediate response\r\n        self._draw_overlays(painter)\r\n        \r\n        painter.end()\r\n        \r\n        # Performance tracking\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _paint_elapsed = (time.perf_counter() - _paint_start) * 1000\r\n            perf_tracker.record(\"paint_event\", _paint_elapsed)\r\n\r\n    def _draw_overlays(self, painter):\r\n        \"\"\"\r\n        Draw overlay elements that need immediate response.\r\n        These are drawn on top of the cached render.\r\n        \"\"\"\r\n        # Tutorial glow effect\r\n        if getattr(self, 'tutorial_glow_active', False):\r\n            self._draw_tutorial_glow(painter)\r\n        \r\n        # Connection Highlight\r\n        if self.hovered_connection:\r\n            self._draw_connection_highlight(painter)\r\n\r\n        # Neurogenesis highlights\r\n        if hasattr(self, 'neurogenesis_highlight'):\r\n            nh = self.neurogenesis_highlight\r\n            if nh.get('neuron') and time.time() - nh.get('start_time', 0) < nh.get('duration', 0):\r\n                self._draw_neurogenesis_highlight(painter)\r\n        \r\n        # Drag preview if dragging a neuron\r\n        if getattr(self, 'dragging', False) and getattr(self, 'dragged_neuron', None):\r\n            self._draw_drag_preview(painter)\r\n\r\n    def _draw_tutorial_glow(self, painter):\r\n        \"\"\"Draw tutorial glow border effect\"\"\"\r\n        opacity = getattr(self, '_tutorial_glow_opacity', 0.0)\r\n        if opacity > 0:\r\n            glow_color = QtGui.QColor(255, 215, 0, int(150 * opacity))\r\n            pen = QtGui.QPen(glow_color, 4)\r\n            painter.setPen(pen)\r\n            painter.setBrush(QtCore.Qt.NoBrush)\r\n            painter.drawRect(self.rect().adjusted(2, 2, -2, -2))\r\n\r\n    def _draw_neurogenesis_highlight(self, painter):\r\n        \"\"\"Draw highlight around newly created neurons\"\"\"\r\n        # Calculate scale (same as in render worker)\r\n        indicator_space = 0\r\n        base_width = 1024\r\n        base_height = 768 - indicator_space\r\n        scale_x = self.width() / base_width\r\n        scale_y = (self.height() - indicator_space) / max(1, base_height)\r\n        scale = max(0.01, min(scale_x, scale_y))\r\n        \r\n        offset_x = 0\r\n        if scale_x > scale_y:\r\n            content_width = base_width * scale\r\n            offset_x = (self.width() - content_width) / 2\r\n        \r\n        nh = self.neurogenesis_highlight\r\n        neuron_name = nh.get('neuron')\r\n        if neuron_name and neuron_name in self.neuron_positions:\r\n            pos = self.neuron_positions[neuron_name]\r\n            x = pos[0] * scale + offset_x\r\n            y = pos[1] * scale + indicator_space\r\n            \r\n            # Pulsing effect\r\n            elapsed = time.time() - nh.get('start_time', 0)\r\n            pulse = 0.5 + 0.5 * math.sin(elapsed * 4)\r\n            \r\n            radius = 40 * scale * (1 + pulse * 0.2)\r\n            alpha = int(200 * (1 - elapsed / nh.get('duration', 1)))\r\n            \r\n            painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, alpha), 3))\r\n            painter.setBrush(QtCore.Qt.NoBrush)\r\n            painter.drawEllipse(QtCore.QPointF(x, y), radius, radius)\r\n\r\n    def _draw_drag_preview(self, painter):\r\n        \"\"\"Draw preview when dragging a neuron (runs at full framerate)\"\"\"\r\n        if not self.dragged_neuron or self.dragged_neuron not in self.neuron_positions:\r\n            return\r\n\r\n        # 1. Calculate Scale (Must match _draw_neurogenesis_highlight and render worker)\r\n        indicator_space = 0\r\n        base_width = 1024\r\n        base_height = 768 - indicator_space\r\n        scale_x = self.width() / base_width\r\n        scale_y = (self.height() - indicator_space) / max(1, base_height)\r\n        scale = max(0.01, min(scale_x, scale_y))\r\n        \r\n        # 2. Prepare single-neuron state for static drawing\r\n        name = self.dragged_neuron\r\n        pos = self.neuron_positions[name]\r\n        val = self.state.get(name, 50)\r\n        \r\n        temp_positions = {name: pos}\r\n        temp_states = {name: val}\r\n        \r\n        # 3. Draw the neuron immediately (bypassing cached 10fps render)\r\n        # Note: We don't clear the background, so we draw ON TOP of the cached frame.\r\n        # This might cause a slight 'trail' behind the moving neuron if the background \r\n        # refresh is slow, but it guarantees the neuron stays stuck to the mouse cursor.\r\n        \r\n        # Check if it has a custom shape\r\n        shape = self.neuron_shapes.get(name, 'circle')\r\n        \r\n        x = pos[0] * scale\r\n        y = pos[1] * scale\r\n        radius = 20 * scale\r\n        \r\n        # Temporarily set painter to high quality\r\n        painter.save()\r\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\r\n        \r\n        # Use existing shape drawing logic\r\n        if shape == 'diamond':\r\n            self.draw_diamond_neuron(painter, x, y, radius, name, scale)\r\n        elif shape == 'square':\r\n            self.draw_square_neuron(painter, x, y, radius, name, scale)\r\n        elif shape == 'triangle':\r\n            self.draw_triangular_neuron(painter, x, y, radius, name, scale)\r\n        elif shape == 'hexagon':\r\n            self.draw_hexagon_neuron(painter, x, y, radius, name, scale)\r\n        else:\r\n            # Standard/Continuous or Binary\r\n            # Explicitly call the static method from the Mixin class\r\n            NetworkRenderingMixin.draw_neurons_static(\r\n                painter, temp_positions, temp_states,\r\n                visible_neurons={name}, \r\n                scale=scale, \r\n                base_font_size=self.neuron_label_font_size\r\n            )\r\n            \r\n        painter.restore()\r\n\r\n    def draw_neurons(self, painter, scale=1.0):\r\n        \"\"\"Main neuron drawing routine.\"\"\"\r\n        # ------------------------------------------------------------------\r\n        # Explicit list of true binary (on/off) neurons\r\n        # ------------------------------------------------------------------\r\n        BINARY_NEURONS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\",\r\n            \"is_sick\", \"is_fleeing\", \"pursuing_food\", \"is_startled\",\r\n            \"external_stimulus\", \"plant_proximity\"\r\n        }\r\n\r\n        # Font size for labels - use config value directly\r\n        label_font = QtGui.QFont(\"Arial\", self.neuron_label_font_size)\r\n        label_font.setBold(True)\r\n        painter.setFont(label_font)\r\n        font_metrics = painter.fontMetrics()\r\n\r\n        for name, pos in self.neuron_positions.items():\r\n            if name not in self.visible_neurons and not self.is_tutorial_mode:\r\n                continue\r\n            if name in self.excluded_neurons:\r\n                continue\r\n\r\n            x_logical, y_logical = pos\r\n            x = x_logical * scale\r\n            y = y_logical * scale\r\n            radius = 20 * scale\r\n\r\n            # Check shape early\r\n            shape = self.neuron_shapes.get(name, 'circle')\r\n\r\n            # --- BINARY NEURONS (always squares) ---\r\n            if name in BINARY_NEURONS or name == \"can_see_food\":\r\n                raw_value = self.state.get(name, 0)\r\n                \r\n                # Value calculation logic\r\n                value = 100.0 if float(raw_value) > 50 else 0.0\r\n                is_active = value > 50\r\n                color = QtGui.QColor(0, 255, 0) if is_active else QtGui.QColor(255, 0, 0)\r\n\r\n                # Draw Square\r\n                painter.setBrush(QtGui.QBrush(color))\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), max(1, int(2 * scale))))\r\n                \r\n                size = radius * 1.8 \r\n                rect = QtCore.QRectF(x - size/2, y - size/2, size, size)\r\n                painter.drawRect(rect)\r\n\r\n                # [NEW] Draw Checkmark or Cross inside\r\n                symbol = \"✓\" if is_active else \"✗\"\r\n                \r\n                painter.save()\r\n                symbol_font = QtGui.QFont(\"Arial\", int(size * 0.7))\r\n                symbol_font.setBold(True)\r\n                painter.setFont(symbol_font)\r\n                painter.setPen(QtGui.QColor(0, 0, 0)) # Black text\r\n                painter.drawText(rect, QtCore.Qt.AlignCenter, symbol)\r\n                painter.restore()\r\n\r\n                if name == \"can_see_food\":\r\n                    # Custom \"Small Black Label\" Logic for can_see_food\r\n                    \r\n                    # Get Localised Name\r\n                    from .localisation import Localisation\r\n                    loc = Localisation.instance()\r\n                    display_name = loc.get(name)\r\n                    if display_name == name: display_name = name.replace(\"_\", \" \").title()\r\n\r\n                    # Smaller Font (0.75x)\r\n                    small_font = painter.font()\r\n                    small_font.setPointSize(max(4, int(self.neuron_label_font_size * 0.75 * scale)))\r\n                    painter.setFont(small_font)\r\n                    fm = painter.fontMetrics()\r\n\r\n                    text_width = fm.horizontalAdvance(display_name)\r\n                    padding = 4 * scale\r\n                    rect_width = text_width + padding * 2\r\n                    rect_height = fm.height() + 2\r\n\r\n                    text_rect = QtCore.QRectF(\r\n                        x - rect_width / 2,\r\n                        y + size/2 + 4 * scale,\r\n                        rect_width,\r\n                        rect_height\r\n                    )\r\n\r\n                    # Black Background\r\n                    painter.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0))) \r\n                    painter.setPen(QtCore.Qt.NoPen)\r\n                    painter.drawRoundedRect(text_rect, 2, 2)\r\n\r\n                    # White Text\r\n                    painter.setPen(QtGui.QColor(255, 255, 255))\r\n                    painter.drawText(text_rect, QtCore.Qt.AlignCenter, display_name)\r\n\r\n                    # Restore Font\r\n                    painter.setFont(label_font)\r\n                else:\r\n                    # Standard Label for other binary neurons\r\n                    self._draw_standard_label(painter, name, x, y, scale, self.neuron_label_font_size)\r\n                \r\n                continue\r\n\r\n            # --- SHAPE-BASED NEURONS ---\r\n            elif shape == 'hexagon':\r\n                # --- HEXAGON LOGIC ---\r\n                color = QtGui.QColor(*self.state_colors.get(name, (160, 32, 240)))\r\n                \r\n                # Draw hexagon\r\n                painter.save()\r\n                painter.translate(x, y)\r\n                \r\n                sides = 6\r\n                polygon = QtGui.QPolygonF()\r\n                angle_step = 360.0 / sides\r\n                for i in range(sides):\r\n                    angle = math.radians(i * angle_step - 90)\r\n                    polygon.append(QtCore.QPointF(radius * math.cos(angle), \r\n                                                radius * math.sin(angle)))\r\n                \r\n                painter.setBrush(QtGui.QBrush(color))\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0)))\r\n                painter.drawPolygon(polygon)\r\n                \r\n                # Draw 'C' inside\r\n                font = QtGui.QFont(\"Arial\", int(14 * scale))\r\n                font.setBold(True)\r\n                painter.setFont(font)\r\n                painter.setPen(QtGui.QColor(255, 255, 255))\r\n                \r\n                rect_size = radius * 2\r\n                rect = QtCore.QRectF(-radius, -radius, rect_size, rect_size)\r\n                painter.drawText(rect, QtCore.Qt.AlignCenter, \"c\")\r\n                painter.restore()\r\n                \r\n                # SKIP External Label for connectors\r\n                continue\r\n            \r\n            elif shape == 'diamond':\r\n                color = QtGui.QColor(*self.state_colors.get(name, (152, 251, 152)))\r\n                self._draw_polygon_neuron(painter, x, y, 4, radius, color, name, scale, rotation=0)\r\n                continue\r\n            \r\n            elif shape == 'square':\r\n                color = QtGui.QColor(*self.state_colors.get(name, (152, 251, 152)))\r\n                self._draw_polygon_neuron(painter, x, y, 4, radius, color, name, scale, rotation=45)\r\n                continue\r\n            \r\n            elif shape == 'triangle':\r\n                color = QtGui.QColor(*self.state_colors.get(name, (255, 255, 150)))\r\n                self._draw_polygon_neuron(painter, x, y, 3, radius, color, name, scale, rotation=0)\r\n                continue\r\n            \r\n            else:  # circle (default)\r\n                # Color logic\r\n                raw_value = self.state.get(name, 0)\r\n                value = float(raw_value) if isinstance(raw_value, (int, float, bool)) else 50.0\r\n                value = max(0, min(100, value))\r\n                \r\n                if name in self.state_colors:\r\n                    color = QtGui.QColor(*self.state_colors[name])\r\n                else:\r\n                    color = QtGui.QColor(220, 220, 220)  # Grey\r\n\r\n                # Draw Circle\r\n                painter.setBrush(QtGui.QBrush(color))\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), max(1, int(2 * scale))))\r\n                painter.drawEllipse(QtCore.QPointF(x, y), radius, radius)\r\n\r\n                # Draw Label\r\n                self._draw_standard_label(painter, name, x, y, scale, self.neuron_label_font_size)\r\n\r\n    def _draw_standard_label(self, painter, name, x, y, scale, font_size=None):\r\n        \"\"\"Draw a standard neuron label with improved localisation fallback.\"\"\"\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n\r\n        if font_size is None:\r\n            font_size = self.neuron_label_font_size\r\n\r\n        # [NEW] Scale font for neurogenesis neurons\r\n        # If the neuron is NOT in the original list, it is a neurogenesis neuron.\r\n        is_neurogenesis = name not in self.original_neurons\r\n        \r\n        # Apply scaling if it's a neurogenesis neuron (0.75x)\r\n        effective_font_size = font_size * 0.75 if is_neurogenesis else font_size\r\n\r\n        label_font = QtGui.QFont(\"Arial\", int(effective_font_size * scale))\r\n        label_font.setBold(True)\r\n        painter.setFont(label_font)\r\n        font_metrics = painter.fontMetrics()\r\n\r\n        # Primary: exact key\r\n        display_name = loc.get(name)\r\n\r\n        # Fallback 1: try space-separated version\r\n        if display_name == name:  # Means no translation found\r\n            space_key = name.replace(\"_\", \" \")\r\n            display_name = loc.get(space_key, default=None)\r\n            if display_name is None:\r\n                display_name = space_key  # Use spaces for readability\r\n\r\n        # Fallback 2: neurogenesis pattern (e.g., novelty_1 → Novelty 1)\r\n        if display_name == name or display_name == name.replace(\"_\", \" \"):\r\n            match = re.match(r\"^([a_z]+)_(\\d+)$\", name)\r\n            if match:\r\n                base = match.group(1)\r\n                idx = match.group(2)\r\n                base_loc = loc.get(base)\r\n                if base_loc != base:  # Found translation for base\r\n                    display_name = f\"{base_loc} {idx}\"\r\n                else:\r\n                    display_name = f\"{base.capitalize()} {idx}\"\r\n\r\n        # Final fallback: clean title case\r\n        if display_name == name:\r\n            display_name = name.replace(\"_\", \" \").title()\r\n\r\n        text_width = font_metrics.horizontalAdvance(display_name)\r\n        padding = 10 * scale\r\n        rect_width = text_width + padding * 2\r\n        rect_height = font_metrics.height() + 4\r\n\r\n        text_rect = QtCore.QRectF(\r\n            x - rect_width / 2,\r\n            y + (20 * scale) + 5 * scale,\r\n            rect_width,\r\n            rect_height,\r\n        )\r\n\r\n        painter.setBrush(QtGui.QBrush(QtGui.QColor(26, 26, 26, 200)))\r\n        painter.setPen(QtCore.Qt.NoPen)\r\n        painter.drawRoundedRect(text_rect, 4, 4)\r\n\r\n        painter.setPen(QtGui.QColor(224, 224, 224))\r\n        painter.drawText(text_rect, QtCore.Qt.AlignCenter, display_name)\r\n\r\n    def _draw_neuron_label(self, painter, x, y, name, radius, scale, alpha=255):\r\n        \"\"\"Draw neuron label for polygon shapes.\"\"\"\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n        \r\n        # [NEW] Scale font for neurogenesis neurons\r\n        base_size = self.neuron_label_font_size\r\n        is_neurogenesis = name not in self.original_neurons\r\n        effective_size = base_size * 0.75 if is_neurogenesis else base_size\r\n\r\n        font = QtGui.QFont(\"Arial\", int(effective_size * scale))\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        fm = painter.fontMetrics()\r\n\r\n        # Step 1: Try exact key\r\n        label = loc.get(name)\r\n\r\n        # Step 2: Try space-separated key\r\n        if label == name:\r\n            space_key = name.replace(\"_\", \" \")\r\n            label = loc.get(space_key)\r\n            if label == space_key:\r\n                label = None  # Mark as not found\r\n\r\n        # Step 3: Neurogenesis pattern\r\n        if not label:\r\n            import re\r\n            match = re.match(r\"^([a-z_]+)_(\\d+)$\", name)\r\n\r\n    def _draw_neuron_label(self, painter, x, y, name, radius, scale, alpha=255):\r\n        \"\"\"Draw neuron label for polygon shapes.\"\"\"\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n        \r\n        # [NEW] Scale font for neurogenesis neurons\r\n        base_size = self.neuron_label_font_size\r\n        is_neurogenesis = name not in self.original_neurons\r\n        effective_size = base_size * 0.75 if is_neurogenesis else base_size\r\n\r\n        font = QtGui.QFont(\"Arial\", int(effective_size * scale))\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        fm = painter.fontMetrics()\r\n\r\n        # Step 1: Try exact key\r\n        label = loc.get(name)\r\n\r\n        # Step 2: Try space-separated key\r\n        if label == name:\r\n            space_key = name.replace(\"_\", \" \")\r\n            label = loc.get(space_key)\r\n            if label == space_key:\r\n                label = None  # Mark as not found\r\n\r\n        # Step 3: Neurogenesis pattern\r\n        if not label:\r\n            import re\r\n            match = re.match(r\"^([a-z_]+)_(\\d+)$\", name)\r\n            if match:\r\n                base_key = match.group(1)\r\n                num = match.group(2)\r\n                base_label = loc.get(base_key)\r\n                if base_label != base_key:\r\n                    label = f\"{base_label} {num}\"\r\n                else:\r\n                    label = f\"{base_key.replace('_', ' ').title()} {num}\"\r\n\r\n        # Final fallback\r\n        if not label:\r\n            label = name.replace(\"_\", \" \").title()\r\n\r\n        text_width = fm.horizontalAdvance(label)\r\n        padding = 10 * scale\r\n        \r\n        # Position label below the neuron (y + radius + padding)\r\n        rect_y = y + radius + (5 * scale)\r\n        \r\n        rect = QtCore.QRectF(\r\n            x - text_width / 2 - padding,\r\n            rect_y,\r\n            text_width + padding * 2,\r\n            fm.height() + 6\r\n        )\r\n\r\n        # Apply alpha transparency\r\n        bg_alpha = min(180, alpha)\r\n        painter.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0, bg_alpha)))\r\n        painter.setPen(QtCore.Qt.NoPen)\r\n        painter.drawRoundedRect(rect, 6, 6)\r\n\r\n        text_color = QtGui.QColor(255, 255, 255)\r\n        text_color.setAlpha(alpha)\r\n        painter.setPen(text_color)\r\n        painter.drawText(rect, QtCore.Qt.AlignCenter, label)\r\n\r\n    def _draw_standard_label(self, painter, name, x, y, scale, font_size=None):\r\n        \"\"\"Draw a standard neuron label with improved localisation fallback.\"\"\"\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n\r\n        if font_size is None:\r\n            font_size = self.neuron_label_font_size\r\n\r\n        label_font = QtGui.QFont(\"Arial\", int(font_size * scale))\r\n        label_font.setBold(True)\r\n        painter.setFont(label_font)\r\n        font_metrics = painter.fontMetrics()\r\n\r\n        # Primary: exact key\r\n        display_name = loc.get(name)\r\n\r\n        # Fallback 1: try space-separated version\r\n        if display_name == name:  # Means no translation found\r\n            space_key = name.replace(\"_\", \" \")\r\n            display_name = loc.get(space_key, default=None)\r\n            if display_name is None:\r\n                display_name = space_key  # Use spaces for readability\r\n\r\n        # Fallback 2: neurogenesis pattern (e.g., novelty_1 → Novelty 1)\r\n        if display_name == name or display_name == name.replace(\"_\", \" \"):\r\n            match = re.match(r\"^([a_z]+)_(\\d+)$\", name)\r\n            if match:\r\n                base = match.group(1)\r\n                idx = match.group(2)\r\n                base_loc = loc.get(base)\r\n                if base_loc != base:  # Found translation for base\r\n                    display_name = f\"{base_loc} {idx}\"\r\n                else:\r\n                    display_name = f\"{base.capitalize()} {idx}\"\r\n\r\n        # Final fallback: clean title case\r\n        if display_name == name:\r\n            display_name = name.replace(\"_\", \" \").title()\r\n\r\n        text_width = font_metrics.horizontalAdvance(display_name)\r\n        padding = 10 * scale\r\n        rect_width = text_width + padding * 2\r\n        rect_height = font_metrics.height() + 4\r\n\r\n        text_rect = QtCore.QRectF(\r\n            x - rect_width / 2,\r\n            y + (20 * scale) + 5 * scale,\r\n            rect_width,\r\n            rect_height,\r\n        )\r\n\r\n        painter.setBrush(QtGui.QBrush(QtGui.QColor(26, 26, 26, 200)))\r\n        painter.setPen(QtCore.Qt.NoPen)\r\n        painter.drawRoundedRect(text_rect, 4, 4)\r\n\r\n        painter.setPen(QtGui.QColor(224, 224, 224))\r\n        painter.drawText(text_rect, QtCore.Qt.AlignCenter, display_name)\r\n\r\n\r\n    def draw_binary_neuron(self, painter, x, y, value, label, scale=1.0):\r\n        color = (0, 0, 0) if value else (255, 255, 255)\r\n        painter.setBrush(QtGui.QBrush(QtGui.QColor(*color))); painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0)))\r\n        x_scaled, y_scaled, size = int(x * scale), int(y * scale), int(30 * scale)\r\n        painter.drawRect(x_scaled - size//2, y_scaled - size//2, size, size)\r\n        font = painter.font(); font.setPointSize(max(4, int(5 * scale))); painter.setFont(font)  # Reduced from 6, added max\r\n        label_width, label_height = int(150 * scale), int(20 * scale)\r\n        painter.drawText(x_scaled - label_width//2, y_scaled + int(30 * scale), label_width, label_height, QtCore.Qt.AlignCenter, label)\r\n\r\n    def draw_circular_neuron(self, painter, name, pos, value, scale=1.0, visible_neurons=None, excluded_neurons=None):\r\n        \"\"\"Fixed version with correct excluded_neurons handling.\"\"\"\r\n        if visible_neurons is None:\r\n            visible_neurons = self.visible_neurons\r\n        if excluded_neurons is None:\r\n            excluded_neurons = self.excluded_neurons\r\n\r\n        if name in excluded_neurons or name not in visible_neurons:\r\n            return\r\n\r\n        # Reuse static method for consistency\r\n        temp_states = {name: value}\r\n        temp_positions = {name: pos}\r\n        \r\n        NetworkRenderingMixin.draw_neurons_static(\r\n            painter, temp_positions, temp_states,\r\n            visible_neurons={name}, excluded_neurons=excluded_neurons,\r\n            scale=scale, base_font_size=self.neuron_label_font_size\r\n        )\r\n\r\n    def draw_triangular_neuron(self, painter, x, y, radius, label, scale=1.0, alpha=255):\r\n        \"\"\"Draw a triangular neuron with given radius\"\"\"\r\n        color = QtGui.QColor(*self.state_colors.get(label, (255, 255, 150)))\r\n        self._draw_polygon_neuron(painter, x, y, 3, radius, color, label, scale, alpha=alpha)\r\n\r\n    def draw_hexagon_neuron(self, painter, x, y, radius, label, scale=1.0, alpha=255):\r\n        \"\"\"Draw a purple hexagon neuron with a 'C' inside and NO external label\"\"\"\r\n        # 1. Setup Painter\r\n        painter.save()\r\n        painter.translate(x, y)\r\n        \r\n        # Color setup (Purple)\r\n        color_tuple = self.state_colors.get(label, (160, 32, 240))\r\n        color = QtGui.QColor(*color_tuple)\r\n        color.setAlpha(min(255, max(0, alpha)))\r\n        \r\n        pen_color = QtGui.QColor(0, 0, 0)\r\n        pen_color.setAlpha(min(255, max(0, alpha)))\r\n\r\n        painter.setBrush(QtGui.QBrush(color))\r\n        painter.setPen(QtGui.QPen(pen_color))\r\n\r\n        # 2. Draw Hexagon Polygon\r\n        sides = 6\r\n        polygon = QtGui.QPolygonF()\r\n        angle_step = 360.0 / sides\r\n        for i in range(sides):\r\n            angle = math.radians(i * angle_step - 90)\r\n            polygon.append(QtCore.QPointF(radius * math.cos(angle), \r\n                                        radius * math.sin(angle)))\r\n        painter.drawPolygon(polygon)\r\n\r\n        # 3. Draw the 'C' inside\r\n        font_size = int(14 * scale)\r\n        font = QtGui.QFont(\"Arial\", font_size)\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        \r\n        # White text\r\n        painter.setPen(QtGui.QColor(255, 255, 255, min(255, max(0, alpha))))\r\n        \r\n        rect_size = radius * 2\r\n        rect = QtCore.QRectF(-radius, -radius, rect_size, rect_size)\r\n        painter.drawText(rect, QtCore.Qt.AlignCenter, \"c\")\r\n\r\n        painter.restore()\r\n\r\n\r\n    def show_diagnostic_report(self):\r\n        if hasattr(self, 'brain_widget'): self.brain_widget.show_diagnostic_report()\r\n        else: print(\"Error: Brain widget not initialized\")\r\n\r\n    def _draw_polygon_neuron(self, painter, x, y, sides, radius, color, label, scale,\r\n                            rotation=0, alpha=255):\r\n        painter.save()\r\n        painter.translate(x, y)\r\n        painter.rotate(rotation)\r\n\r\n        alpha = max(0, min(255, alpha))            # ← clamp\r\n        colored = QtGui.QColor(color)\r\n        colored.setAlpha(alpha)\r\n\r\n        pen_color = QtGui.QColor(0, 0, 0)\r\n        pen_color.setAlpha(alpha)\r\n\r\n        painter.setBrush(QtGui.QBrush(colored))\r\n        painter.setPen(QtGui.QPen(pen_color))\r\n\r\n        polygon = QtGui.QPolygonF()\r\n        angle_step = 360.0 / sides\r\n        for i in range(sides):\r\n            angle = math.radians(i * angle_step - 90)\r\n            polygon.append(QtCore.QPointF(radius * math.cos(angle),\r\n                                        radius * math.sin(angle)))\r\n        painter.drawPolygon(polygon)\r\n        painter.restore()\r\n        self._draw_neuron_label(painter, x, y, label, radius, scale, alpha)\r\n\r\n    def _draw_neuron_label(self, painter, x, y, name, radius, scale, alpha=255):\r\n        \"\"\"Draw neuron label with robust localisation fallback.\"\"\"\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n\r\n        font = QtGui.QFont(\"Arial\", int(self.neuron_label_font_size * scale))\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        fm = painter.fontMetrics()\r\n\r\n        # Step 1: Try exact key\r\n        label = loc.get(name)\r\n\r\n        # Step 2: Try space-separated key\r\n        if label == name:\r\n            space_key = name.replace(\"_\", \" \")\r\n            label = loc.get(space_key)\r\n            if label == space_key:\r\n                label = None  # Mark as not found\r\n\r\n        # Step 3: Neurogenesis pattern\r\n        if not label:\r\n            import re\r\n            match = re.match(r\"^([a-z_]+)_(\\d+)$\", name)\r\n            if match:\r\n                base_key = match.group(1)\r\n                num = match.group(2)\r\n                base_label = loc.get(base_key)\r\n                if base_label != base_key:\r\n                    label = f\"{base_label} {num}\"\r\n                else:\r\n                    label = f\"{base_key.replace('_', ' ').title()} {num}\"\r\n\r\n        # Final fallback\r\n        if not label:\r\n            label = name.replace(\"_\", \" \").title()\r\n\r\n        text_width = fm.horizontalAdvance(label)\r\n        padding = 10 * scale\r\n        \r\n        # Position label below the neuron (y + radius + padding)\r\n        rect_y = y + radius + (5 * scale)\r\n        \r\n        rect = QtCore.QRectF(\r\n            x - text_width / 2 - padding,\r\n            rect_y,\r\n            text_width + padding * 2,\r\n            fm.height() + 6\r\n        )\r\n\r\n        # Apply alpha transparency\r\n        bg_alpha = min(180, alpha)\r\n        painter.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0, bg_alpha)))\r\n        painter.setPen(QtCore.Qt.NoPen)\r\n        painter.drawRoundedRect(rect, 6, 6)\r\n\r\n        text_color = QtGui.QColor(255, 255, 255)\r\n        text_color.setAlpha(alpha)\r\n        painter.setPen(text_color)\r\n        painter.drawText(rect, QtCore.Qt.AlignCenter, label)\r\n\r\n    def draw_neurogenesis_highlights(self, painter, scale):\r\n        if (self.neurogenesis_highlight['neuron'] and time.time() - self.neurogenesis_highlight['start_time'] < self.neurogenesis_highlight['duration']):\r\n            pos = self.neuron_positions.get(self.neurogenesis_highlight['neuron'])\r\n            if pos:\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0), int(3 * scale))); painter.setBrush(QtCore.Qt.NoBrush); radius = int(40 * scale)\r\n                x, y, width, height = int(pos[0] - radius), int(pos[1] - radius), int(radius * 2), int(radius * 2)\r\n                painter.drawEllipse(x, y, width, height)\r\n\r\n    def draw_square_neuron(self, painter, x, y, radius, label, scale=1.0, alpha=255):\r\n        \"\"\"Draw a square neuron with given radius\"\"\"\r\n        color = QtGui.QColor(*self.state_colors.get(label, (152, 251, 152)))\r\n        self._draw_polygon_neuron(painter, x, y, 4, radius, color, label, scale, rotation=45, alpha=alpha)\r\n\r\n    def draw_diamond_neuron(self, painter, x, y, radius, label, scale=1.0, alpha=255):\r\n        \"\"\"Draw a diamond-shaped neuron with given radius\"\"\"\r\n        color = QtGui.QColor(*self.state_colors.get(label, (152, 251, 152)))\r\n        self._draw_polygon_neuron(painter, x, y, 4, radius, color, label, scale, rotation=0, alpha=alpha)\r\n\r\n    def toggle_links(self, state):\r\n        \"\"\"\r\n        Public slot connected to the checkbox.\r\n        Triggers a staggered, organic fade animation for connections.\r\n        \"\"\"\r\n        from PyQt5 import QtCore\r\n        import time\r\n        import random\r\n        \r\n        want_visible = (state == QtCore.Qt.Checked)\r\n        \r\n        # CRITICAL: We must enable the render loop immediately so the fade animation can play\r\n        if want_visible:\r\n            self.show_links = True\r\n            \r\n        now = time.time()\r\n        \r\n        # Calculate staggering based on number of connections to ensure smooth waves\r\n        count = len(self.weights)\r\n        \r\n        for i, key in enumerate(self.weights.keys()):\r\n            self._link_targets[key] = 1.0 if want_visible else 0.0\r\n            \r\n            # Organic staggering: 0.0 to 1.2 seconds delay based on randomness\r\n            # This creates the \\\"not all at once\\\" effect\r\n            delay = random.uniform(0.0, 1.2)\r\n            \r\n            self._link_start_times[key] = now + delay\r\n            self._link_fade_speeds[key] = random.uniform(1.5, 4.0) # Variable fade speeds\r\n            \r\n            # Initialize opacity if tracking is missing\r\n            if key not in self._link_opacities:\r\n                self._link_opacities[key] = 0.0 if want_visible else 1.0\r\n        \r\n        # Ensure the fade timer is running to process the animation frames\r\n        if not self._link_fade_timer.isActive():\r\n            self._link_fade_timer.start()\r\n            \r\n        self.mark_render_dirty()\r\n\r\n\r\n    def toggle_weights(self, state):\r\n        self.show_weights = (state == QtCore.Qt.Checked)\r\n        self.mark_render_dirty()\r\n        self.update()\r\n\r\n    def toggle_capture_training_data(self, state):\r\n        self.capture_training_data_enabled = state\r\n\r\n    def mousePressEvent(self, event):\r\n        if event.button() != QtCore.Qt.LeftButton:\r\n            super().mousePressEvent(event)\r\n            return\r\n\r\n        logical_pos = self._get_logical_coords(event.pos())\r\n        neuron = self.get_neuron_at_pos(event.pos())\r\n        \r\n        if not neuron:\r\n            self.dragged_neuron = None\r\n            self.dragging = False\r\n            super().mousePressEvent(event)\r\n            return\r\n\r\n        # Allow dragging any neuron\r\n        self.dragged_neuron = neuron\r\n        self.dragging = True\r\n        self.drag_start_pos = event.pos()\r\n        \r\n        # Debug info\r\n        is_neuro = self.is_neurogenesis_neuron(neuron)\r\n        neuron_type = \"Neurogenesis\" if is_neuro else \"Core\"\r\n        \r\n        # Debug: Check if neuron is in functional_neurons\r\n        if hasattr(self, 'enhanced_neurogenesis') and neuron in self.enhanced_neurogenesis.functional_neurons:\r\n            print(f\"   Functional neuron details: {self.enhanced_neurogenesis.functional_neurons[neuron]}\")\r\n        \r\n        self.update()\r\n        super().mousePressEvent(event)\r\n\r\n    def handle_neuron_clicked(self, neuron_name):\r\n        print(f\"\")\r\n\r\n    def show_diagnostic_report(self):\r\n        dialog = DiagnosticReportDialog(self, self.parent())\r\n        dialog.exec_()\r\n\r\n    def mouseMoveEvent(self, event):\r\n        \"\"\"Handle mouse movement for tooltips, hover tracking, and dragging\"\"\"\r\n        if hasattr(self, 'tooltip_manager'):\r\n            self.tooltip_manager.show_tooltip_for_position(event)\r\n\r\n        # 1. Track hover state for neurons\r\n        neuron = self.get_neuron_at_pos(event.pos())\r\n        \r\n        # Determine if neuron hover state changed\r\n        if neuron != self.hovered_neuron:\r\n            self.hovered_neuron = neuron\r\n            self.hover_animation_time = time.time()\r\n            self.hover_value_opacity = 0.0\r\n            self.hover_value_display_active = neuron is not None\r\n            self.update() \r\n\r\n        # 2. [NEW] Track hover state for connections (only if not hovering a neuron)\r\n        if not neuron:\r\n            conn = self.get_connection_at_pos(event.pos())\r\n            if conn != self.hovered_connection:\r\n                self.hovered_connection = conn\r\n                self.update() # Trigger repaint for overlay\r\n        else:\r\n            # If hovering neuron, clear connection highlight\r\n            if self.hovered_connection is not None:\r\n                self.hovered_connection = None\r\n                self.update()\r\n\r\n        # 3. Dragging logic\r\n        if getattr(self, 'dragging', False) and getattr(self, 'dragged_neuron', None):\r\n            if self.is_neurogenesis_neuron(self.dragged_neuron):\r\n                logical_pos = self._get_logical_coords(event.pos())\r\n                self.neuron_positions[self.dragged_neuron] = (logical_pos.x(), logical_pos.y())\r\n                self.update()\r\n\r\n        super().mouseMoveEvent(event)\r\n\r\n    def leaveEvent(self, event):\r\n        \"\"\"Clear hover state when mouse leaves widget.\"\"\"\r\n        if self.hovered_neuron is not None:\r\n            self.hovered_neuron = None\r\n            self.hover_value_display_active = False\r\n            self.hover_value_opacity = 0.0\r\n            self.update()\r\n        super().leaveEvent(event)\r\n\r\n    def mouseDoubleClickEvent(self, event):\r\n        \"\"\"\r\n        Handle double-click on a neuron to open the Neuron Laboratory instead of the Inspector.\r\n        \"\"\"\r\n        if event.button() == QtCore.Qt.LeftButton:\r\n            neuron_name = self.get_neuron_at_pos(event.pos())\r\n            \r\n            if neuron_name:\r\n                print(f\"Double-clicked on neuron: {neuron_name}\")\r\n                \r\n                # Close the old Inspector if it's currently open (compatibility cleanup)\r\n                if hasattr(self, '_inspector') and self._inspector and self._inspector.isVisible():\r\n                    self._inspector.close()\r\n                    self._inspector = None # Clear the old reference\r\n\r\n                # Open or show the Neuron Laboratory\r\n                if self._laboratory is None:\r\n                    self._laboratory = NeuronLaboratory(self, parent=self.window() or self.parent()) \r\n                \r\n                self._laboratory.show()\r\n                self._laboratory.raise_() # Bring the window to the front\r\n\r\n                # CALL A NEW METHOD: Pass the neuron name for selection\r\n                if hasattr(self._laboratory, 'select_neuron_by_name'):\r\n                    self._laboratory.select_neuron_by_name(neuron_name)\r\n                \r\n                self.update() # Redraw the brain visualization\r\n                event.accept()\r\n                return\r\n            \r\n        super().mouseDoubleClickEvent(event)\r\n\r\n    def mouseReleaseEvent(self, event):\r\n        if event.button() == QtCore.Qt.LeftButton:\r\n            is_click = False\r\n            if self.dragged_neuron and self.drag_start_pos:\r\n                distance = (event.pos() - self.drag_start_pos).manhattanLength()\r\n                if distance < QtWidgets.QApplication.startDragDistance():\r\n                    is_click = True\r\n            if is_click:\r\n                self.neuronClicked.emit(self.dragged_neuron)\r\n            self.dragging = False\r\n            self.dragged_neuron = None\r\n            self.drag_start_pos = None\r\n            self.update()\r\n            # Only apply repulsion to neurogenesis neurons\r\n            #if hasattr(self, 'enhanced_neurogenesis'):\r\n            #    self.apply_repulsion_force()\r\n        super().mouseReleaseEvent(event)\r\n\r\n    def draw_strength_multiplier(self, painter, scale):\r\n        \"\"\"\r\n        Draw strength multipliers on neurons that have been strengthened.\r\n        Displays as \"2.5x\" above strengthened neurons with a badge.\r\n        \"\"\"\r\n        if not hasattr(self, 'enhanced_neurogenesis'):\r\n            return\r\n        \r\n        current_time = time.time()\r\n        \r\n        # Save painter state to avoid interfering with other drawings\r\n        painter.save()\r\n        \r\n        for name, neuron in self.enhanced_neurogenesis.functional_neurons.items():\r\n            # Verify attribute exists and neuron is strengthened\r\n            if not hasattr(neuron, 'strength_multiplier'):\r\n                continue\r\n                \r\n            if neuron.strength_multiplier <= 1.0 or name not in self.neuron_positions:\r\n                continue\r\n            \r\n            pos = self.neuron_positions[name]\r\n            x, y = pos\r\n            \r\n            # Position ABOVE the neuron (in logical coordinates)\r\n            label_y = y - 45  # Don't scale Y offset - already in logical space\r\n            \r\n            # Format multiplier text\r\n            multiplier_text = f\"{neuron.strength_multiplier:.1f}x\"  # FIX: typo here\r\n            \r\n            # Create font with proper scaling\r\n            font = QtGui.QFont()\r\n            font.setPointSize(int(9 * scale))\r\n            font.setBold(True)\r\n            painter.setFont(font)\r\n            \r\n            # Draw background circle/badge\r\n            font_metrics = painter.fontMetrics()\r\n            text_width = font_metrics.horizontalAdvance(multiplier_text)\r\n            badge_radius = max(12 * scale, (text_width / 2) + 5 * scale)\r\n            \r\n            painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 215, 0, 220)))\r\n            painter.setPen(QtGui.QPen(QtGui.QColor(139, 105, 20), max(1, int(2 * scale))))\r\n            painter.drawEllipse(QtCore.QPointF(x, label_y), badge_radius, badge_radius)\r\n            \r\n            # Draw text\r\n            painter.setPen(QtGui.QColor(0, 0, 0))\r\n            painter.drawText(\r\n                int(x - text_width / 2),\r\n                int(label_y - font_metrics.height() / 2),\r\n                int(text_width),\r\n                int(font_metrics.height()),\r\n                QtCore.Qt.AlignCenter,\r\n                multiplier_text\r\n            )\r\n            \r\n            # Optional: Add star indicator\r\n            star_radius = 8 * scale\r\n            star_x = x + 20 * scale\r\n            star_y = y - 20 * scale\r\n            \r\n            painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 215, 0, 200)))\r\n            painter.setPen(QtGui.QPen(QtGui.QColor(255, 165, 0), max(1, int(2 * scale))))\r\n            painter.drawEllipse(QtCore.QPointF(star_x, star_y), star_radius, star_radius)\r\n        \r\n        painter.restore()\r\n\r\n\r\n    def _is_click_on_neuron(self, point, neuron_pos, scale):\r\n        neuron_x, neuron_y = neuron_pos\r\n        scaled_x = neuron_x * scale; scaled_y = neuron_y * scale\r\n        return (abs(scaled_x - point.x()) <= 25 * scale and abs(scaled_y - point.y()) <= 25 * scale)\r\n\r\n    def is_point_inside_neuron(self, point, neuron_pos, scale):\r\n        neuron_x, neuron_y = neuron_pos\r\n        scaled_x = neuron_x * scale; scaled_y = neuron_y * scale\r\n        return ((scaled_x - 25 * scale) <= point.x() <= (scaled_x + 25 * scale) and (scaled_y - 25 * scale) <= point.y() <= (scaled_y + 25 * scale))\r\n\r\n    def reset_positions(self):\r\n        self.neuron_positions = self.original_neuron_positions.copy()\r\n        \r\n        # Check config for randomization preference\r\n        neuron_props = self.config.neurogenesis.get('neuron_properties', {})\r\n        if neuron_props.get('randomize_start_positions', False):\r\n            self._randomize_all_positions()\r\n            \r\n        self.update()\r\n\r\n    def _randomize_all_positions(self):\r\n        \"\"\"Randomize positions of all neurons within safe bounds.\"\"\"\r\n        import random\r\n        padding = self.config.neurogenesis.get('neuron_properties', {}).get('canvas_padding', 60)\r\n        \r\n        # Canvas dimensions (assumed roughly 1024x768 logical)\r\n        min_x, max_x = padding, 1024 - padding\r\n        min_y, max_y = padding, 768 - padding\r\n        \r\n        for name in self.neuron_positions:\r\n            rx = random.randint(min_x, max_x)\r\n            ry = random.randint(min_y, max_y)\r\n            self.neuron_positions[name] = (rx, ry)\r\n        \r\n        print(\"🎲 Randomized neuron positions\")\r\n        \r\n    def start_tutorial_glow(self, duration_ms=5000):\r\n        \"\"\"Start a glowing, pulsing border effect for tutorial purposes\"\"\"\r\n        self.tutorial_glow_active = True\r\n        self._tutorial_glow_opacity = 0.0\r\n        \r\n        # Create animation for pulsing effect\r\n        self.tutorial_glow_animation = QtCore.QPropertyAnimation(self, b\"tutorial_glow_opacity\")\r\n        self.tutorial_glow_animation.setDuration(500)  # 0.5 second pulse\r\n        self.tutorial_glow_animation.setStartValue(0.0)\r\n        self.tutorial_glow_animation.setEndValue(1.0)\r\n        self.tutorial_glow_animation.setLoopCount(-1)  # Infinite loop\r\n        self.tutorial_glow_animation.setEasingCurve(QtCore.QEasingCurve.InOutSine)\r\n        \r\n        # Start the animation\r\n        self.tutorial_glow_animation.start()\r\n        \r\n        # Set timer to stop after duration\r\n        self.tutorial_glow_timer = QtCore.QTimer()\r\n        self.tutorial_glow_timer.setSingleShot(True)\r\n        self.tutorial_glow_timer.timeout.connect(self.stop_tutorial_glow)\r\n        self.tutorial_glow_timer.start(duration_ms)\r\n    \r\n    def stop_tutorial_glow(self):\r\n        \"\"\"Stop the tutorial glow effect\"\"\"\r\n        self.tutorial_glow_active = False\r\n        if self.tutorial_glow_animation:\r\n            self.tutorial_glow_animation.stop()\r\n            self.tutorial_glow_animation = None\r\n        if self.tutorial_glow_timer:\r\n            self.tutorial_glow_timer.stop()\r\n            self.tutorial_glow_timer = None\r\n        self.update()\r\n    \r\n    def get_tutorial_glow_opacity(self):\r\n        \"\"\"Get current glow opacity (for Qt property animation)\"\"\"\r\n        return self._tutorial_glow_opacity\r\n    \r\n    def set_tutorial_glow_opacity(self, value):\r\n        \"\"\"Set glow opacity and trigger repaint (for Qt property animation)\"\"\"\r\n        self._tutorial_glow_opacity = value\r\n        self.update()\r\n    \r\n    # Qt property for animation\r\n    tutorial_glow_opacity = QtCore.pyqtProperty(float, get_tutorial_glow_opacity, set_tutorial_glow_opacity)\r\n\r\nimport time\r\n\r\nclass PerformanceProfiler:\r\n    \"\"\"Lightweight profiler for identifying slow code paths.\"\"\"\r\n    \r\n    def __init__(self):\r\n        self.timings = {}\r\n        self.call_counts = {}\r\n    \r\n    def start(self, name):\r\n        if name not in self.timings:\r\n            self.timings[name] = []\r\n            self.call_counts[name] = 0\r\n        self._current_start = time.perf_counter()\r\n        self._current_name = name\r\n    \r\n    def stop(self):\r\n        elapsed = (time.perf_counter() - self._current_start) * 1000\r\n        self.timings[self._current_name].append(elapsed)\r\n        self.call_counts[self._current_name] += 1\r\n        # Keep only last 100 samples\r\n        if len(self.timings[self._current_name]) > 100:\r\n            self.timings[self._current_name].pop(0)\r\n    \r\n    def report(self):\r\n        \"\"\"Print performance report.\"\"\"\r\n        print(\"\\n\" + \"=\"*60)\r\n        print(\"PERFORMANCE REPORT\")\r\n        print(\"=\"*60)\r\n        for name, times in sorted(self.timings.items()):\r\n            if times:\r\n                avg = sum(times) / len(times)\r\n                max_t = max(times)\r\n                calls = self.call_counts[name]\r\n                print(f\"{name:30} avg={avg:6.2f}ms  max={max_t:6.2f}ms  calls={calls}\")\r\n        print(\"=\"*60 + \"\\n\")\r\n\r\n# =============================================================================\r\n# NETWORK RENDERING MIXIN (Add to brain_widget.py)\r\n# =============================================================================\r\n\r\nclass NetworkRenderingMixin:\r\n    \"\"\"\r\n    Mixin providing static network rendering methods extracted from BrainWidget.\r\n    Used by both BrainWidget and save viewer's NetworkCanvas for consistent visuals.\r\n    \"\"\"\r\n    \r\n    @staticmethod\r\n    def draw_connections(self, painter, scale):\r\n        \"\"\"Draw connections with extended 2-second weight-change animations.\r\n        Links are forced INVISIBLE while core neurons are still being revealed.\"\"\"\r\n        if not self.show_links:\r\n            return\r\n\r\n        # ===== PERFORMANCE FIX: Skip if widget is hidden =====\r\n        if not self.isVisible():\r\n            return\r\n\r\n        # ===== PERFORMANCE TRACKING =====\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _conn_start = time.perf_counter()\r\n\r\n        # absolutely no connections until every core neuron is completely revealed (tutorial mode guard). \r\n        if self.is_tutorial_mode and len(self.visible_neurons) < len(self.original_neurons):\r\n            return\r\n        \r\n        # ===== NEURAL STYLE: Use dedicated neural renderer =====\r\n        if self.anim_neural_pulse_enabled:\r\n            self._draw_neural_connections(painter, scale)\r\n            return\r\n\r\n        current_time = time.time()\r\n        connections_drawn = 0\r\n        connections_skipped = 0\r\n\r\n        for key, weight in self.weights.items():\r\n            if not isinstance(key, tuple) or len(key) != 2:\r\n                continue\r\n            source, target = key\r\n\r\n            # skip tutorial-only connections when not in tutorial\r\n            tutorial_mode = getattr(self.parent(), 'tutorial_active', False)\r\n            is_tutorial_conn = (source.startswith('tutorial_neuron_') or\r\n                                target.startswith('tutorial_neuron_'))\r\n            if is_tutorial_conn and not tutorial_mode:\r\n                continue\r\n\r\n            # skip if either neuron is still revealing (tutorial safety)\r\n            if self.is_tutorial_mode and (\r\n                    not self.is_neuron_revealed(source) or\r\n                    not self.is_neuron_revealed(target)):\r\n                connections_skipped += 1\r\n                continue\r\n\r\n            if (source not in self.neuron_positions or\r\n                target not in self.neuron_positions or\r\n                source in self.excluded_neurons or\r\n                target in self.excluded_neurons):\r\n                continue\r\n\r\n            # skip connections involving non-visible core neurons (tutorial)\r\n            if self.is_tutorial_mode:\r\n                if source in self.original_neurons and source not in self.visible_neurons:\r\n                    connections_skipped += 1\r\n                    continue\r\n                if target in self.original_neurons and target not in self.visible_neurons:\r\n                    connections_skipped += 1\r\n                    continue\r\n\r\n            start = self.neuron_positions[source]\r\n            end   = self.neuron_positions[target]\r\n            start_point = QtCore.QPointF(float(start[0]), float(start[1]))\r\n            end_point   = QtCore.QPointF(float(end[0]),   float(end[1]))\r\n\r\n            # special styling:  Stress ↔ Anxiety  (uses animation style params)\r\n            is_stress_to_anxiety = (\r\n                (source.lower().startswith('stress') and target.lower() == 'anxiety') or\r\n                (target.lower().startswith('stress') and source.lower() == 'anxiety'))\r\n            if is_stress_to_anxiety:\r\n                pen = QtGui.QPen(QtGui.QColor(*self.anim_stress_colour))\r\n                pen.setWidth(int(self.anim_stress_width))\r\n                if self.anim_stress_dashed:\r\n                    pen.setStyle(QtCore.Qt.DashLine)\r\n                painter.setPen(pen)\r\n                painter.drawLine(start_point, end_point)\r\n                continue   # skip default drawing for this pair\r\n\r\n            # default appearance (uses animation style params)\r\n            anim_weight = weight\r\n            base_width  = self.anim_line_base_width * scale\r\n            line_width  = base_width\r\n            pen_style   = QtCore.Qt.SolidLine\r\n            animating   = False\r\n            pulse_progress = 0.0\r\n\r\n            # check for active weight-change animations (2-second window)\r\n            for anim in self.weight_animations:\r\n                if anim['pair'] == key:\r\n                    elapsed = current_time - anim['start_time']\r\n                    if elapsed < anim['duration']:\r\n                        progress = elapsed / anim['duration']\r\n                        anim_weight = anim['start_weight'] + progress * (\r\n                            anim['end_weight'] - anim['start_weight'])\r\n\r\n                        # growing / shrinking line width\r\n                        if progress < 0.5:\r\n                            line_width = base_width + (6.0 * scale * progress * 2)\r\n                        else:\r\n                            line_width = base_width + (6.0 * scale * (1 - progress) * 2)\r\n\r\n                        pulse_progress = progress * anim['pulse_speed']\r\n                        animating = True\r\n                        break\r\n\r\n            # colour & alpha\r\n            if animating:\r\n                r, g, b = anim['color']\r\n                alpha = int(255 * (1 - pulse_progress ** 2))\r\n                color = QtGui.QColor(r, g, b, alpha)\r\n            else:\r\n                base_alpha = int(self._link_opacities.get(key, 0.0) * 255)\r\n                \r\n                # ===== VIBRANT STYLE: Apply ambient pulse =====\r\n                if self.anim_ambient_pulse_enabled and base_alpha > 0:\r\n                    pulse_state = self._get_or_create_ambient_pulse(key)\r\n                    phase = pulse_state['phase']\r\n                    \r\n                    # Sinusoidal oscillation (0 to 1 range)\r\n                    pulse_factor = (math.sin(phase) + 1.0) / 2.0\r\n                    \r\n                    # Interpolate width\r\n                    w_min, w_max = self.anim_ambient_pulse_width_range\r\n                    width_mult = w_min + pulse_factor * (w_max - w_min)\r\n                    line_width = base_width * width_mult\r\n                    \r\n                    # Interpolate alpha\r\n                    a_min, a_max = self.anim_ambient_pulse_alpha_range\r\n                    pulse_alpha = int(a_min + pulse_factor * (a_max - a_min))\r\n                    # Combine with base opacity\r\n                    final_alpha = int((base_alpha / 255.0) * pulse_alpha)\r\n                    \r\n                    if weight > 0:\r\n                        color = QtGui.QColor(self.anim_line_col_pos[0], \r\n                                           self.anim_line_col_pos[1], \r\n                                           self.anim_line_col_pos[2], final_alpha)\r\n                    else:\r\n                        color = QtGui.QColor(self.anim_line_col_neg[0], \r\n                                           self.anim_line_col_neg[1], \r\n                                           self.anim_line_col_neg[2], final_alpha)\r\n                else:\r\n                    # Standard coloring (classic style or opacity 0)\r\n                    color = (QtGui.QColor(0, int(255 * abs(weight)), 0, base_alpha) if weight > 0 else\r\n                            QtGui.QColor(int(255 * abs(weight)), 0, 0, base_alpha))\r\n\r\n            # line style\r\n            if is_tutorial_conn and tutorial_mode:\r\n                color = QtGui.QColor(255, 255, 0, 180)\r\n                line_width = 3\r\n                pen_style = QtCore.Qt.DashLine\r\n            else:\r\n                pen_style = (QtCore.Qt.DashLine if weight < 0 else\r\n                            QtCore.Qt.SolidLine)\r\n                if abs(anim_weight) < 0.1:\r\n                    pen_style = QtCore.Qt.DotLine\r\n\r\n            painter.setPen(QtGui.QPen(color, line_width, pen_style))\r\n            painter.drawLine(start_point, end_point)\r\n\r\n            # pulse circle travelling along the wire (uses animation style params)\r\n            if self.anim_pulse_enabled and animating and pulse_progress < 1.0:\r\n                pulse_pos = pulse_progress\r\n                pulse_x = start_point.x() + pulse_pos * (end_point.x() - start_point.x())\r\n                pulse_y = start_point.y() + pulse_pos * (end_point.y() - start_point.y())\r\n                pulse_size = self.anim_pulse_diameter * scale * (1 - pulse_progress ** 2)\r\n\r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(*self.anim_pulse_colour, self.anim_pulse_alpha)))\r\n                painter.setPen(QtCore.Qt.NoPen)\r\n                painter.drawEllipse(QtCore.QPointF(pulse_x, pulse_y),\r\n                                    pulse_size, pulse_size)\r\n\r\n            # glow around the line (uses animation style params)\r\n            if self.anim_glow_enabled and animating and progress < self.anim_glow_fade_threshold:\r\n                glow_progress = progress / self.anim_glow_fade_threshold\r\n                glow_width = line_width + 4 * scale * (1 - glow_progress)\r\n                glow_color = QtGui.QColor(*self.anim_glow_colour, self.anim_glow_alpha)\r\n                painter.setPen(QtGui.QPen(glow_color, glow_width, pen_style))\r\n                painter.drawLine(start_point, end_point)\r\n            \r\n            # ===== SUBTLE STYLE: Draw communication glow packets =====\r\n            if self.anim_comm_glow_enabled:\r\n                self._draw_comm_glows_for_connection(\r\n                    painter, scale, key, start_point, end_point\r\n                )\r\n\r\n            # weight text (optional)\r\n            if self.show_weights and abs(weight) > 0.1:\r\n                # Calculate angle for rotation\r\n                dx = end_point.x() - start_point.x()\r\n                dy = end_point.y() - start_point.y()\r\n                angle_deg = math.degrees(math.atan2(dy, dx))\r\n                \r\n                # Ensure text is readable (not upside down)\r\n                if angle_deg > 90:\r\n                    angle_deg -= 180\r\n                elif angle_deg < -90:\r\n                    angle_deg += 180\r\n\r\n                midpoint = QtCore.QPointF((start_point.x() + end_point.x()) / 2,\r\n                                        (start_point.y() + end_point.y()) / 2)\r\n                \r\n                text_str = f\"{weight:.2f}\"\r\n                \r\n                # Font config\r\n                font_size = max(7, int(8 * scale))\r\n                padding = 4 * scale\r\n                \r\n                font = painter.font()\r\n                font.setPointSize(font_size)\r\n                font.setBold(True)\r\n                painter.setFont(font)\r\n                \r\n                fm = painter.fontMetrics()\r\n                text_w = fm.horizontalAdvance(text_str)\r\n                text_h = fm.height()\r\n\r\n                painter.save()\r\n                painter.translate(midpoint)\r\n                painter.rotate(angle_deg)\r\n                \r\n                # Draw background pill\r\n                rect = QtCore.QRectF(-text_w/2 - padding, -text_h/2, \r\n                                     text_w + padding*2, text_h)\r\n                \r\n                painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 255, 255, 220)))\r\n                painter.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, 150), 1))\r\n                painter.drawRoundedRect(rect, 4, 4)\r\n                \r\n                # Draw text\r\n                text_color = QtGui.QColor(0, 100, 0) if weight >= 0 else QtGui.QColor(150, 0, 0)\r\n                painter.setPen(text_color)\r\n                painter.drawText(rect, QtCore.Qt.AlignCenter, text_str)\r\n                \r\n                painter.restore()\r\n\r\n            connections_drawn += 1\r\n\r\n        # debug guard: warn only when something looks wrong\r\n        if connections_drawn == 0 and len(self.weights) > 0:\r\n            print(f\"🔴 NO CONNECTIONS DRAWN! tutorial_mode={self.is_tutorial_mode}, \"\r\n                f\"visible_neurons={len(self.visible_neurons)}, original_neurons={len(self.original_neurons)}, \"\r\n                f\"total_weights={len(self.weights)}, skipped={connections_skipped}\")\r\n        \r\n        # ===== END PERFORMANCE TRACKING =====\r\n        if _PERF_TRACKING_AVAILABLE:\r\n            _conn_elapsed = (time.perf_counter() - _conn_start) * 1000\r\n            perf_tracker.record(\"draw_connections\", _conn_elapsed)\r\n    \r\n    @staticmethod\r\n    def draw_neurons_static(\r\n        painter, neuron_positions, neuron_states,\r\n        visible_neurons=None, excluded_neurons=None,\r\n        scale=1.0, base_font_size=6):\r\n        \"\"\"\r\n        Static neuron drawing with full localisation fallback support.\r\n        \"\"\"\r\n        import re\r\n        from .localisation import Localisation\r\n        loc = Localisation.instance()\r\n\r\n        BINARY_NEURONS = {\r\n            \"can_see_food\", \"is_eating\", \"is_sleeping\",\r\n            \"is_sick\", \"is_fleeing\", \"pursuing_food\", \"is_startled\"\r\n        }\r\n\r\n        if visible_neurons is None:\r\n            visible_neurons = set(neuron_positions.keys())\r\n        if excluded_neurons is None:\r\n            excluded_neurons = set()\r\n\r\n        label_font = QtGui.QFont(\"Arial\", int(base_font_size * scale))\r\n        label_font.setBold(True)\r\n        painter.setFont(label_font)\r\n        font_metrics = painter.fontMetrics()\r\n\r\n        for name, pos in neuron_positions.items():\r\n            if name in excluded_neurons or name not in visible_neurons:\r\n                continue\r\n\r\n            raw_value = neuron_states.get(name, 0)\r\n\r\n            # Determine color and value (binary vs continuous)\r\n            if name in BINARY_NEURONS:\r\n                value = 100.0 if float(raw_value) > 50 else 0.0\r\n                is_active = value > 50\r\n                color = QtGui.QColor(0, 255, 0) if is_active else QtGui.QColor(255, 0, 0)\r\n            else:\r\n                value = float(raw_value) if isinstance(raw_value, (int, float, bool)) else 50.0\r\n                value = max(0, min(100, value))\r\n                normalized = value / 100.0\r\n                if normalized > 0.7:\r\n                    color = QtGui.QColor(76, 175, 80)\r\n                elif normalized > 0.4:\r\n                    color = QtGui.QColor(255, 193, 7)\r\n                else:\r\n                    color = QtGui.QColor(244, 67, 54)\r\n\r\n            x = pos[0] * scale\r\n            y = pos[1] * scale\r\n            radius = 20 * scale\r\n\r\n            painter.setBrush(QtGui.QBrush(color))\r\n            painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), max(1, int(2 * scale))))\r\n            painter.drawEllipse(QtCore.QPointF(x, y), radius, radius)\r\n\r\n            # === Localisation with multi-level fallback ===\r\n            display_name = loc.get(name)\r\n\r\n            # Fallback 1: space-separated key\r\n            if display_name == name:\r\n                space_key = name.replace(\"_\", \" \")\r\n                display_name = loc.get(space_key)\r\n                if display_name == space_key:\r\n                    display_name = None\r\n\r\n            # Fallback 2: neurogenesis numbered neuron\r\n            if not display_name:\r\n                match = re.match(r\"^([a-z_]+)_(\\d+)$\", name)\r\n                if match:\r\n                    base = match.group(1)\r\n                    idx = match.group(2)\r\n                    base_loc = loc.get(base)\r\n                    if base_loc != base:\r\n                        display_name = f\"{base_loc} {idx}\"\r\n                    else:\r\n                        display_name = f\"{base.replace('_', ' ').title()} {idx}\"\r\n\r\n            # Final fallback\r\n            if not display_name:\r\n                display_name = name.replace(\"_\", \" \").title()\r\n\r\n            text_width = font_metrics.horizontalAdvance(display_name)\r\n            padding = 10 * scale\r\n            rect_width = text_width + padding * 2\r\n            rect_height = font_metrics.height() + 4\r\n\r\n            text_rect = QtCore.QRectF(\r\n                x - rect_width / 2,\r\n                y + radius + 5 * scale,\r\n                rect_width,\r\n                rect_height,\r\n            )\r\n\r\n            painter.setBrush(QtGui.QBrush(QtGui.QColor(26, 26, 26, 200)))\r\n            painter.setPen(QtCore.Qt.NoPen)\r\n            painter.drawRoundedRect(text_rect, 4, 4)\r\n\r\n            painter.setPen(QtGui.QColor(224, 224, 224))\r\n            painter.drawText(text_rect, QtCore.Qt.AlignCenter, display_name)\r\n\r\n    \r\n    @staticmethod\r\n    def draw_layers_static(painter, layers, scale=1.0):\r\n        \"\"\"Draw layer background rectangles if layer structure exists\"\"\"\r\n        if not layers:\r\n            return\r\n            \r\n        for layer in layers:\r\n            y_pos = layer.get('y_position', 0)\r\n            name = layer.get('name', 'Layer')\r\n            layer_type = layer.get('layer_type', 'hidden')\r\n            \r\n            # Logical dimensions (match brain_widget's coordinate system)\r\n            rect_height = 120\r\n            rect_top = (y_pos - rect_height / 2)\r\n            rect_left = -200  # Extend beyond visible area\r\n            rect_width = 2000\r\n            \r\n            # Determine layer color based on type\r\n            if layer_type == 'input':\r\n                color = QtGui.QColor(220, 255, 220, 30)\r\n                border_color = QtGui.QColor(180, 220, 180, 60)\r\n            elif layer_type == 'output':\r\n                color = QtGui.QColor(255, 220, 220, 30)\r\n                border_color = QtGui.QColor(220, 180, 180, 60)\r\n            else:  # hidden\r\n                color = QtGui.QColor(230, 230, 255, 40)\r\n                border_color = QtGui.QColor(200, 200, 240, 60)\r\n            \r\n            # Draw rectangle\r\n            rect = QtCore.QRectF(rect_left, rect_top, rect_width, rect_height)\r\n            painter.setBrush(QtGui.QBrush(color))\r\n            painter.setPen(QtGui.QPen(border_color, 1, QtCore.Qt.DashLine))\r\n            painter.drawRect(rect)\r\n            \r\n            # Draw label\r\n            font = QtGui.QFont(\"Arial\", int(10 * scale))\r\n            font.setBold(True)\r\n            painter.setFont(font)\r\n            painter.setPen(QtGui.QColor(150, 150, 170))\r\n            painter.drawText(QtCore.QPointF(20, rect_top + 20), name)\r\n\r\n\r\n"
  },
  {
    "path": "src/brain_worker.py",
    "content": "import sys\nimport time\nimport random\nimport traceback\nimport math\nfrom queue import Queue, Empty\nfrom heapq import nlargest\n\nfrom PyQt5.QtCore import QThread, pyqtSignal, QMutex, QMutexLocker, QWaitCondition\n\nclass BrainWorker(QThread):\n    \"\"\"\n    Background worker for handling expensive brain logic operations:\n    - Neurogenesis checks\n    - Hebbian learning calculations\n    - State decay and update processing\n    \"\"\"\n    \n    # Signals for results (emitted to main thread)\n    neurogenesis_result = pyqtSignal(dict)\n    hebbian_result = pyqtSignal(dict)\n    state_update_result = pyqtSignal(dict)\n    error_occurred = pyqtSignal(str)\n    \n    def __init__(self, brain_widget=None):\n        super().__init__()\n        self.brain_widget = brain_widget  # Optional weak ref if needed, usually avoided for thread safety\n        self._running = True\n        self._paused = False\n        \n        # Thread-safe queues for tasks\n        self.task_queue = Queue()\n        \n        # Cache for thread-safe access to brain state\n        self._cache_mutex = QMutex()\n        self.cache = {\n            'state': {},\n            'weights': {},\n            'positions': {},\n            'config': None,\n            'excluded_neurons': set(),\n            'connector_neurons': set(),\n            'learning_rate': 0.1,\n            'new_neurons': set(),\n            'custom_neurons': set()\n        }\n\n        # History tracking to prevent Hebbian loops\n        self._last_hebbian_pairs = []\n        \n        self.wait_condition = QWaitCondition()\n        self.queue_mutex = QMutex()\n\n    def update_cache(self, state, weights, positions, config, excluded_neurons=None, \n                     connector_neurons=None, learning_rate=0.1, new_neurons=None,\n                     custom_neurons=None):\n        \"\"\"\n        Update the local cache of brain state.\n        Called from main thread before triggering heavy tasks.\n        \"\"\"\n        with QMutexLocker(self._cache_mutex):\n            # Deep copy or safe copy important structures\n            self.cache['state'] = state.copy()\n            self.cache['weights'] = weights.copy()\n            self.cache['positions'] = positions.copy()\n            self.cache['config'] = config\n            self.cache['excluded_neurons'] = excluded_neurons if excluded_neurons else set()\n            self.cache['connector_neurons'] = connector_neurons if connector_neurons else set()\n            self.cache['learning_rate'] = learning_rate\n            self.cache['new_neurons'] = new_neurons if new_neurons else set()\n            self.cache['custom_neurons'] = custom_neurons if custom_neurons else set()\n\n    def queue_neurogenesis_check(self, state_context):\n        self._add_task('neurogenesis', {'state': state_context})\n\n    def queue_hebbian_learning(self):\n        # print(\"🧵 BrainWorker: Hebbian learning queued\")  # Uncomment for verbose queuing logs\n        self._add_task('hebbian', {})\n\n    def queue_state_update(self, update_data):\n        self._add_task('state_update', update_data)\n\n    def _add_task(self, task_type, data):\n        with QMutexLocker(self.queue_mutex):\n            self.task_queue.put((task_type, data))\n            self.wait_condition.wakeOne()\n\n    def stop(self):\n        self._running = False\n        with QMutexLocker(self.queue_mutex):\n            self.wait_condition.wakeAll()\n\n    # --- Pause and Resume Methods ---\n    def pause(self):\n        \"\"\"Pause the worker thread.\"\"\"\n        with QMutexLocker(self.queue_mutex):\n            self._paused = True\n    \n    def resume(self):\n        \"\"\"Resume the worker thread.\"\"\"\n        with QMutexLocker(self.queue_mutex):\n            self._paused = False\n            self.wait_condition.wakeAll()\n    # --------------------------------\n\n    def run(self):\n        print(\"🧵 BrainWorker thread started\")\n        \n        while self._running:\n            task = None\n            \n            # Wait for task\n            with QMutexLocker(self.queue_mutex):\n                # Check pause state first\n                while self._paused and self._running:\n                    self.wait_condition.wait(self.queue_mutex)\n\n                if not self._running:\n                    break\n\n                if self.task_queue.empty():\n                    self.wait_condition.wait(self.queue_mutex, 200) # Timeout allows checking _running\n                    # Re-check pause after wait\n                    if self._paused: \n                        continue\n                    if self.task_queue.empty():\n                        continue\n                \n                try:\n                    task = self.task_queue.get_nowait()\n                except Empty:\n                    continue\n\n            if not task:\n                continue\n\n            task_type, data = task\n            \n            try:\n                if task_type == 'neurogenesis':\n                    self._perform_neurogenesis_check(data)\n                elif task_type == 'hebbian':\n                    self._perform_hebbian_learning()\n                elif task_type == 'state_update':\n                    self._process_state_update(data)\n            except Exception as e:\n                error_msg = f\"Error in {task_type}: {str(e)}\\n{traceback.format_exc()}\"\n                print(error_msg)\n                self.error_occurred.emit(error_msg)\n                \n        print(\"🧵 BrainWorker thread stopped\")\n\n    def _perform_neurogenesis_check(self, data):\n        \"\"\"Check if neurogenesis conditions are met based on cached config.\"\"\"\n        state = data.get('state', {})\n        \n        with QMutexLocker(self._cache_mutex):\n            config = self.cache['config']\n        \n        if not config:\n            return\n\n        neuro_config = getattr(config, 'neurogenesis', {})\n        \n        triggers = {\n            'novelty': state.get('novelty_exposure', 0) > neuro_config.get('novelty_threshold', 3.0),\n            'stress': state.get('sustained_stress', 0) > neuro_config.get('stress_threshold', 1.2) or state.get('anxiety', 0) > 75,\n            'reward': state.get('recent_rewards', 0) > neuro_config.get('reward_threshold', 3.5)\n        }\n        \n        # Priority logic\n        trigger_type = None\n        trigger_val = 0\n        \n        if triggers['stress']:\n            trigger_type = 'stress'\n            trigger_val = state.get('sustained_stress', 0)\n        elif triggers['novelty']:\n            trigger_type = 'novelty'\n            trigger_val = state.get('novelty_exposure', 0)\n        elif triggers['reward']:\n            trigger_type = 'reward'\n            trigger_val = state.get('recent_rewards', 0)\n            \n        if trigger_type:\n            # Emit result back to main thread to finalize creation\n            self.neurogenesis_result.emit({\n                'should_create': True,\n                'neuron_type': trigger_type,\n                'trigger_value': trigger_val,\n                'state_context': state,\n                'is_emergency': (trigger_type == 'stress' and state.get('anxiety', 0) > 90)\n            })\n        else:\n            self.neurogenesis_result.emit({'should_create': False})\n\n    def _perform_hebbian_learning(self):\n        \"\"\"Perform Hebbian learning calculations using cached state.\"\"\"\n        # Retrieve snapshot of cache\n        with QMutexLocker(self._cache_mutex):\n            state = self.cache['state']\n            weights = self.cache['weights']\n            neuron_list = list(self.cache['positions'].keys())\n            excluded = self.cache['excluded_neurons']\n            connector_neurons = self.cache['connector_neurons']\n            config = self.cache['config']\n            base_learning_rate = self.cache['learning_rate']\n            new_neurons = self.cache['new_neurons']\n\n        # DEBUG: Check if we have the basics\n        if not config:\n            print(\"⚠️ BrainWorker: Hebbian skipped - No 'config' in cache yet.\")\n            return\n        \n        if not neuron_list:\n            print(\"⚠️ BrainWorker: Hebbian skipped - No neurons in cache 'positions'.\")\n            return\n\n        # PURE_INPUTS (Sensors) - Do not include in Hebbian learning\n        PURE_INPUTS = {\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\", \n            \"plant_proximity\"\n        }\n\n        # Filter available neurons\n        # Exclude system neurons, connector neurons AND pure inputs\n        learning_candidates = [\n            n for n in neuron_list \n            if n not in excluded and n not in connector_neurons and n not in PURE_INPUTS\n        ]\n        \n        # DEBUG: Check if we have enough candidates\n        if len(learning_candidates) < 2:\n            print(f\"⚠️ BrainWorker: Hebbian skipped - Not enough candidates ({len(learning_candidates)}).\")\n            # print(f\"   Excluded: {len(excluded)}, Connectors: {len(connector_neurons)}, Total: {len(neuron_list)}\")\n            self.hebbian_result.emit({'updated_pairs': []})\n            return\n\n        # Calculate scores\n        scored_pairs = []\n        for i, n1 in enumerate(learning_candidates):\n            for n2 in learning_candidates[i + 1:]:\n                # Base Score: Sum of activations\n                v1 = self._get_neuron_value(state.get(n1, 50))\n                v2 = self._get_neuron_value(state.get(n2, 50))\n                score = v1 + v2\n\n                # 1. Add Random Noise to break deterministic loops\n                score += random.uniform(0, 40)\n\n                # 2. Cooldown Penalty: Check if pair was used in last cycle\n                # Sort tuple to ensure (A,B) is treated same as (B,A)\n                pair_key = tuple(sorted((n1, n2)))\n                if pair_key in self._last_hebbian_pairs:\n                    score -= 500  # Massive penalty ensures rotation\n\n                scored_pairs.append((score, n1, n2, v1, v2))\n\n        if not scored_pairs:\n            print(\"⚠️ BrainWorker: Hebbian skipped - No valid pairs formed.\")\n            return\n\n        # Select top pairs\n        top_k = 2  # Default\n        if hasattr(config, 'neurogenesis'):\n            top_k = config.neurogenesis.get('max_hebbian_pairs', 2)\n            \n        top_pairs = nlargest(top_k, scored_pairs)\n        \n        # Update history for next run\n        self._last_hebbian_pairs = [tuple(sorted((n1, n2))) for _, n1, n2, _, _ in top_pairs]\n        \n        weight_updates = {}\n        updated_pairs_list = []\n        \n        hebbian_config = getattr(config, 'hebbian', {})\n        decay_rate = hebbian_config.get('weight_decay', 0.01)\n        \n        for _, n1, n2, v1, v2 in top_pairs:\n            pair = (n1, n2)\n            reverse_pair = (n2, n1)\n            \n            # Find existing weight key\n            use_pair = None\n            if pair in weights:\n                use_pair = pair\n            elif reverse_pair in weights:\n                use_pair = reverse_pair\n            \n            if not use_pair:\n                # CREATE NEW CONNECTION if it doesn't exist\n                # This allows Hebbian learning to form new pathways between neurons\n                # that are frequently co-active, not just strengthen existing ones\n                use_pair = pair\n                old_w = 0.0  # New connections start at zero\n                # Mark this as a new connection that needs to be created\n                weight_updates[use_pair] = {\n                    'old_weight': old_w,\n                    'new_weight': 0.0,  # Will be updated below\n                    'is_new_connection': True  # Signal to brain_widget to create this\n                }\n            else:\n                old_w = weights[use_pair]\n            \n            # Boost learning rate for new neurons\n            lr = base_learning_rate\n            if n1 in new_neurons or n2 in new_neurons:\n                lr *= 2.0\n                \n            # Hebbian rule: delta = lr * act1 * act2\n            delta = lr * (v1 / 100.0) * (v2 / 100.0)\n            \n            new_w = old_w + delta - (old_w * decay_rate)\n            new_w = max(-1.0, min(1.0, new_w))\n            \n            # Check if this was a new connection we're creating\n            is_new = use_pair in weight_updates and weight_updates[use_pair].get('is_new_connection', False)\n            \n            weight_updates[use_pair] = {\n                'old_weight': old_w,\n                'new_weight': new_w,\n                'is_new_connection': is_new\n            }\n            updated_pairs_list.append(use_pair)\n\n        if updated_pairs_list:\n            print(f\"🧠 Hebbian: updated {len(updated_pairs_list)} pair(s)\")\n\n        self.hebbian_result.emit({\n            'updated_pairs': updated_pairs_list,\n            'weight_updates': weight_updates\n        })\n\n    def _process_state_update(self, data):\n        \"\"\"\n        Process state decay and noise logic.\n        \"\"\"\n        if data.get('health_check'):\n            self.state_update_result.emit({'health_check': True})\n            return\n\n        with QMutexLocker(self._cache_mutex):\n            current_state = self.cache['state']\n            weights = self.cache['weights']\n            excluded = self.cache['excluded_neurons']\n\n        updated_state = {}\n        \n        # PURE_INPUTS (Sensors) - Do not decay these\n        PURE_INPUTS = {\n            \"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \n            \"pursuing_food\", \"is_fleeing\", \"is_startled\", \"external_stimulus\", \n            \"plant_proximity\"\n        }\n\n        # 1. Decay and Noise\n        for neuron, val in current_state.items():\n            if neuron in excluded or neuron in PURE_INPUTS:\n                continue\n            \n            # Simple decay towards baseline\n            if isinstance(val, (int, float)):\n                # Decay factor\n                decay = 0.95 \n                noise = random.uniform(-0.5, 0.5)\n                \n                new_val = val * decay + noise\n                updated_state[neuron] = new_val\n\n        # 2. Connection effects (Simplified delta calculation)\n        connection_deltas = {}\n        \n        for (src, dst), w in weights.items():\n            if src in current_state and dst in current_state:\n                if dst in PURE_INPUTS: continue\n                \n                src_val = current_state[src]\n                if isinstance(src_val, (int, float)):\n                    effect = src_val * w * 0.1 # Small timestep factor\n                    connection_deltas[dst] = connection_deltas.get(dst, 0) + effect\n                    \n        # Apply deltas\n        for neuron, delta in connection_deltas.items():\n            if neuron in updated_state:\n                updated_state[neuron] += delta\n            elif neuron in current_state and neuron not in PURE_INPUTS:\n                updated_state[neuron] = current_state[neuron] + delta\n\n        # Clamp\n        final_state = {}\n        for k, v in updated_state.items():\n            final_state[k] = max(-100, min(100, v))\n\n        self.state_update_result.emit({'processed_state': final_state})\n\n    def _get_neuron_value(self, val):\n        if isinstance(val, (int, float)):\n            return float(val)\n        if isinstance(val, bool):\n            return 100.0 if val else 0.0\n        return 0.0"
  },
  {
    "path": "src/certificate.py",
    "content": "\"\"\"\r\nOld feature, currently unused - for future reimplementation\r\n\r\nGenerate a certificate with top statistics\r\n\"\"\"\r\n\r\n\r\nimport datetime\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\n\r\nclass SquidCertificateWindow(QtWidgets.QDialog):\r\n    def __init__(self, parent=None, tamagotchi_logic=None):\r\n        super().__init__(parent)\r\n        self.tamagotchi_logic = tamagotchi_logic\r\n        self.setWindowTitle(\"Squid Certificate\")\r\n        self.setMinimumSize(800, 1000)  # Increased minimum size\r\n        \r\n        layout = QtWidgets.QVBoxLayout(self)\r\n        \r\n        self.certificate_view = QtWidgets.QTextBrowser()\r\n        self.certificate_view.setOpenExternalLinks(False)\r\n        layout.addWidget(self.certificate_view)\r\n        \r\n        # Add print button with larger text\r\n        print_button = QtWidgets.QPushButton(\"Print Certificate\")\r\n        print_button.setStyleSheet(\"font-size: 18px; padding: 10px;\")\r\n        print_button.clicked.connect(self.print_certificate)\r\n        layout.addWidget(print_button, alignment=QtCore.Qt.AlignRight)\r\n        \r\n        self.update_certificate()\r\n    \r\n    def update_certificate(self):\r\n        if not self.tamagotchi_logic or not self.tamagotchi_logic.squid:\r\n            return\r\n            \r\n        squid = self.tamagotchi_logic.squid\r\n        current_date = datetime.datetime.now().strftime(\"%B %d, %Y\")\r\n        personality = str(squid.personality).split('.')[-1].lower().capitalize()\r\n        squid_name = getattr(squid, 'name', 'Squid')\r\n        \r\n        certificate_html = f\"\"\"\r\n        <html>\r\n        <head>\r\n            <style>\r\n                body {{\r\n                    font-family: 'Times New Roman', Times, serif;\r\n                    margin: 0;\r\n                    padding: 0;\r\n                    background-color: #f9f7e8;\r\n                    color: #333;\r\n                }}\r\n                .certificate {{\r\n                    width: 100%;\r\n                    max-width: 900px;\r\n                    margin: 0 auto;\r\n                    padding: 40px;\r\n                    border: 25px solid #a28e5c;\r\n                    background-color: #fff;\r\n                    box-shadow: 0 0 15px rgba(0,0,0,0.3);\r\n                    position: relative;\r\n                    overflow: hidden;\r\n                }}\r\n                .header {{\r\n                    text-align: center;\r\n                    margin-bottom: 30px;\r\n                    border-bottom: 3px solid #a28e5c;\r\n                    padding-bottom: 20px;\r\n                }}\r\n                .title {{\r\n                    font-size: 52px;  /* Increased from 42px */\r\n                    font-weight: bold;\r\n                    color: #7a693a;\r\n                    text-transform: uppercase;\r\n                    letter-spacing: 3px;\r\n                    margin: 0;\r\n                    line-height: 1.2;\r\n                }}\r\n                .subtitle {{\r\n                    font-size: 28px;  /* Increased from 22px */\r\n                    color: #666;\r\n                    margin: 10px 0 0;\r\n                }}\r\n                .content {{\r\n                    text-align: center;\r\n                    margin: 30px 0;\r\n                    font-size: 26px;  /* Increased from 20px */\r\n                    line-height: 1.5;\r\n                }}\r\n                .name {{\r\n                    font-size: 64px;  /* Increased from 50px */\r\n                    font-family: 'Brush Script MT', cursive;\r\n                    color: #333;\r\n                    margin: 20px 0;\r\n                    text-decoration: underline;\r\n                    text-decoration-color: #a28e5c;\r\n                    text-decoration-thickness: 2px;\r\n                }}\r\n                .stats {{\r\n                    margin: 30px 0;\r\n                    text-align: left;\r\n                    font-size: 24px;  /* Increased from 18px */\r\n                    line-height: 1.6;\r\n                }}\r\n                .stats h2 {{\r\n                    font-size: 36px;  /* Increased from 30px */\r\n                    color: #7a693a;\r\n                    border-bottom: 2px solid #e2d9bc;\r\n                    padding-bottom: 8px;\r\n                }}\r\n                .stats table {{\r\n                    font-size: 26px;  /* Increased from 20px */\r\n                    width: 100%;\r\n                    margin: 20px 0;\r\n                }}\r\n                .stats table td {{\r\n                    padding: 12px 0;\r\n                }}\r\n                .stats ul {{\r\n                    font-size: 26px;  /* Increased from 20px */\r\n                    padding-left: 30px;\r\n                }}\r\n                .stats li {{\r\n                    margin-bottom: 15px;\r\n                }}\r\n                .seal {{\r\n                    width: 150px;  /* Increased from 120px */\r\n                    height: 150px;  /* Increased from 120px */\r\n                    border-radius: 50%;\r\n                    background-color: #e2d9bc;\r\n                    border: 3px solid #7a693a;\r\n                    margin: 0 auto;\r\n                    display: flex;\r\n                    align-items: center;\r\n                    justify-content: center;\r\n                    font-weight: bold;\r\n                    font-size: 32px;  /* Increased from 24px */\r\n                    color: #7a693a;\r\n                    transform: rotate(-15deg);\r\n                    position: relative;\r\n                    margin-top: 30px;\r\n                    box-shadow: 0 4px 8px rgba(0,0,0,0.2);\r\n                }}\r\n                .date {{\r\n                    font-size: 28px;  /* Increased from 22px */\r\n                    font-style: italic;\r\n                    margin-top: 40px;\r\n                }}\r\n                .personality {{\r\n                    font-size: 36px;  /* New style for personality */\r\n                    font-weight: bold;\r\n                    color: #7a693a;\r\n                    margin: 20px 0;\r\n                    text-transform: uppercase;\r\n                    letter-spacing: 1px;\r\n                }}\r\n            </style>\r\n        </head>\r\n        <body>\r\n            <div class=\"certificate\">\r\n                <div class=\"header\">\r\n                    <h1 class=\"title\">Certificate of Squidship</h1>\r\n                    <p class=\"subtitle\">Presented by the International Dosidicus Society</p>\r\n                </div>\r\n                \r\n                <div class=\"content\">\r\n                    <p>This certifies that</p>\r\n                    <p class=\"name\">{squid_name}</p>\r\n                    <p>is an officially recognized Dosidicus electronicae of the</p>\r\n                    <p class=\"personality\">{personality} Personality Type</p>\r\n                </div>\r\n                \r\n                <div class=\"stats\">\r\n                    <h2>Statistics</h2>\r\n                    <table>\r\n                        <tr>\r\n                            <td><b>Happiness:</b> {squid.happiness}/100</td>\r\n                            <td><b>Hunger:</b> {squid.hunger}/100</td>\r\n                        </tr>\r\n                        <tr>\r\n                            <td><b>Cleanliness:</b> {squid.cleanliness}/100</td>\r\n                            <td><b>Sleepiness:</b> {squid.sleepiness}/100</td>\r\n                        </tr>\r\n                        <tr>\r\n                            <td><b>Anxiety:</b> {squid.anxiety}/100</td>\r\n                            <td><b>Curiosity:</b> {squid.curiosity}/100</td>\r\n                        </tr>\r\n                    </table>\r\n                </div>\r\n                \r\n                <div class=\"stats\">\r\n                    <h2>Achievements:</h2>\r\n                    <ul>\r\n                        <li>Successfully hatched a squid</li>\r\n                        <li>Fed the squid over 10 times</li>\r\n                        <li>Reached a happiness level above 80</li>\r\n                        <li>Survived a difficult situation</li>\r\n                    </ul>\r\n                </div>\r\n                \r\n                <div style=\"text-align: center;\">\r\n                    <p class=\"date\">Issued on this day, {current_date}</p>\r\n                    <div class=\"seal\">OFFICIAL</div>\r\n                </div>\r\n            </div>\r\n        </body>\r\n        </html>\r\n        \"\"\"\r\n        \r\n        self.certificate_view.setHtml(certificate_html)\r\n    \r\n    def print_certificate(self):\r\n        from PyQt5 import QtPrintSupport\r\n        printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution)\r\n        dialog = QtPrintSupport.QPrintDialog(printer, self)\r\n        if dialog.exec_() == QtWidgets.QDialog.Accepted:\r\n            self.certificate_view.print_(printer)\r\n"
  },
  {
    "path": "src/compute_backend.py",
    "content": "\"\"\"\r\nOptional NPU / AI Accelerator support via ONNX Runtime\r\n===========================================================================\r\n\r\nA thin abstraction over the core matrix operations used by the\r\nbrain engine (forward pass MatMul, Hebbian outer-product update, zeros\r\ninitialisation) so that an ONNX Runtime execution provider can be dropped\r\nin without touching the rest of the codebase.\r\n\r\nBackend selection\r\n-----------------\r\nSet  [Compute] backend = onnx  in config.ini to enable ONNX.\r\nLeave as  backend = numpy  (or omit the section entirely) for the default\r\nNumPy path.  ONNX silently falls back to NumPy if onnxruntime is not\r\ninstalled.\r\n\r\nPublic API\r\n----------\r\n    get_backend(backend_name=None)  →  ComputeBackend instance (singleton)\r\n\r\nComputeBackend methods used by brain_widget / brain_worker:\r\n    .zeros(shape)                              → ndarray-compatible 2-D matrix\r\n    .forward(weights_matrix, inputs)           → 1-D result of weights @ inputs\r\n    .hebbian(associations, sample, lr)         → updated associations matrix\r\n    .get_value(matrix, i, j)                   → float scalar\r\n\r\nAuthor note: dynamic neurogenesis is supported because the ONNX MatMul\r\ngraph is rebuilt whenever the network shape changes.\r\n\"\"\"\r\n\r\nfrom __future__ import annotations\r\n\r\n# ---------------------------------------------------------------------------\r\n# Base class\r\n# ---------------------------------------------------------------------------\r\n\r\nclass ComputeBackend:\r\n    \"\"\"Abstract base class for compute backends.\"\"\"\r\n\r\n    def zeros(self, shape: tuple):\r\n        \"\"\"Return a zero-filled matrix of *shape* compatible with this backend.\"\"\"\r\n        raise NotImplementedError\r\n\r\n    def forward(self, weights_matrix, inputs):\r\n        \"\"\"Dense matrix-vector product:  result = weights_matrix @ inputs.\"\"\"\r\n        raise NotImplementedError\r\n\r\n    def hebbian(self, associations, sample: list, learning_rate: float):\r\n        \"\"\"\r\n        Hebbian outer-product update applied to *associations* in place.\r\n\r\n        For each pair (i, j) where i < j:\r\n            associations[i][j] += learning_rate * sample[i] * sample[j]\r\n            associations[j][i]  = associations[i][j]   (keep symmetric)\r\n\r\n        Returns the updated associations matrix.\r\n        \"\"\"\r\n        raise NotImplementedError\r\n\r\n    def get_value(self, matrix, i: int, j: int) -> float:\r\n        \"\"\"Return matrix[i][j] as a plain Python float.\"\"\"\r\n        raise NotImplementedError\r\n\r\n    @property\r\n    def name(self) -> str:\r\n        raise NotImplementedError\r\n\r\n\r\n# ---------------------------------------------------------------------------\r\n# NumPy backend  (default, always available)\r\n# ---------------------------------------------------------------------------\r\n\r\nclass NumpyBackend(ComputeBackend):\r\n    \"\"\"\r\n    Default compute backend.  Uses NumPy for all operations – identical\r\n    behaviour to the pre-integration codebase.\r\n    \"\"\"\r\n\r\n    def __init__(self):\r\n        import numpy as _np  # already a project dependency\r\n        self._np = _np\r\n        print(\"🧠 Compute backend: NumPy\")\r\n\r\n    # --- ComputeBackend interface -------------------------------------------\r\n\r\n    def zeros(self, shape: tuple):\r\n        return self._np.zeros(shape)\r\n\r\n    def forward(self, weights_matrix, inputs):\r\n        return weights_matrix @ inputs\r\n\r\n    def hebbian(self, associations, sample: list, learning_rate: float):\r\n        np = self._np\r\n        arr = np.array(sample, dtype=np.float64)\r\n        delta = np.outer(arr, arr) * learning_rate\r\n        n = len(sample)\r\n        for i in range(n):\r\n            for j in range(i + 1, n):\r\n                associations[i][j] += delta[i][j]\r\n                associations[j][i]  = associations[i][j]\r\n        return associations\r\n\r\n    def get_value(self, matrix, i: int, j: int) -> float:\r\n        return float(matrix[i][j])\r\n\r\n    @property\r\n    def name(self) -> str:\r\n        return \"numpy\"\r\n\r\n\r\n# ---------------------------------------------------------------------------\r\n# ONNX Runtime backend  (optional)\r\n# ---------------------------------------------------------------------------\r\n\r\nclass ONNXBackend(ComputeBackend):\r\n    \"\"\"\r\n    Optional ONNX Runtime backend.\r\n\r\n    * Lazy-imports  onnx  and  onnxruntime  so Dosidicus starts fine even\r\n      when neither package is installed.\r\n    * Automatically selects the best available execution provider:\r\n        DirectML   → AMD / Intel / NVIDIA via Windows ML (NPU-capable)\r\n        OpenVINO   → Intel NPU\r\n        QNN        → Qualcomm Snapdragon X\r\n        CPU        → universal fallback\r\n    * Rebuilds the ONNX MatMul graph whenever the network shape changes,\r\n      which means dynamic neurogenesis is fully supported.\r\n    * Falls back to NumPy arithmetic internally for the Hebbian update\r\n      (the association matrix is small; ONNX overhead would dominate).\r\n    \"\"\"\r\n\r\n    # Execution-provider preference order\r\n    _PROVIDER_PRIORITY = [\r\n        'QNNExecutionProvider',          # Qualcomm Snapdragon X Elite NPU\r\n        'DmlExecutionProvider',          # DirectML  (AMD XDNA, Intel Arc, NVIDIA)\r\n        'OpenVINOExecutionProvider',     # Intel NPU / iGPU\r\n        'CPUExecutionProvider',          # Always available\r\n    ]\r\n\r\n    def __init__(self):\r\n        self._available   = False\r\n        self._session     = None\r\n        self._last_shape  = None\r\n        self._provider    = 'CPUExecutionProvider'\r\n        self._np          = None\r\n\r\n        try:\r\n            import numpy as np\r\n            import onnx           # noqa: F401  (confirms package present)\r\n            import onnxruntime as ort\r\n\r\n            self._np  = np\r\n            self._ort = ort\r\n\r\n            available_providers = ort.get_available_providers()\r\n            for pref in self._PROVIDER_PRIORITY:\r\n                if pref in available_providers:\r\n                    self._provider = pref\r\n                    break\r\n\r\n            self._available = True\r\n            print(f\"🧠 Compute backend: ONNX Runtime  [{self._provider}]\")\r\n            print(f\"   Available providers: {available_providers}\")\r\n\r\n        except ImportError as exc:\r\n            print(f\"⚠️  ONNX Runtime not available ({exc}).\")\r\n            print(\"   Falling back to NumPy backend.  \"\r\n                  \"Install 'onnxruntime' (or 'onnxruntime-directml') to enable NPU support.\")\r\n\r\n    # --- Internal helpers ---------------------------------------------------\r\n\r\n    def _build_session(self, rows: int, cols: int):\r\n        \"\"\"\r\n        Build (or rebuild) an ONNX MatMul session for a (rows, cols) weight\r\n        matrix applied to a cols-element input vector → rows-element output.\r\n\r\n        Called automatically from forward() when the shape changes.\r\n        \"\"\"\r\n        from onnx import helper, TensorProto\r\n\r\n        W = helper.make_tensor_value_info('weights', TensorProto.FLOAT, [rows, cols])\r\n        V = helper.make_tensor_value_info('inputs',  TensorProto.FLOAT, [cols])\r\n        Y = helper.make_tensor_value_info('output',  TensorProto.FLOAT, [rows])\r\n\r\n        node  = helper.make_node('MatMul', inputs=['weights', 'inputs'], outputs=['output'])\r\n        graph = helper.make_graph([node], 'dosidicus_forward', [W, V], [Y])\r\n        model = helper.make_model(\r\n            graph,\r\n            opset_imports=[helper.make_opsetid('', 17)]\r\n        )\r\n\r\n        model_bytes = model.SerializeToString()\r\n\r\n        sess_opts = self._ort.SessionOptions()\r\n        sess_opts.log_severity_level = 3  # suppress verbose ONNX logs\r\n\r\n        self._session    = self._ort.InferenceSession(\r\n            model_bytes,\r\n            sess_options=sess_opts,\r\n            providers=[self._provider, 'CPUExecutionProvider']\r\n        )\r\n        self._last_shape = (rows, cols)\r\n\r\n    # --- ComputeBackend interface -------------------------------------------\r\n\r\n    def zeros(self, shape: tuple):\r\n        return self._np.zeros(shape)\r\n\r\n    def forward(self, weights_matrix, inputs):\r\n        if not self._available:\r\n            # Transparent NumPy fallback\r\n            return weights_matrix @ inputs\r\n\r\n        np = self._np\r\n        w  = np.array(weights_matrix, dtype=np.float32)\r\n        v  = np.array(inputs,         dtype=np.float32)\r\n\r\n        rows, cols = w.shape\r\n        if (rows, cols) != self._last_shape:\r\n            # Network topology changed (neurogenesis) → rebuild graph\r\n            self._build_session(rows, cols)\r\n\r\n        result = self._session.run(['output'], {'weights': w, 'inputs': v})\r\n        return result[0]\r\n\r\n    def hebbian(self, associations, sample: list, learning_rate: float):\r\n        \"\"\"\r\n        For the Hebbian update we stay in NumPy.\r\n        The association matrix is small (N × N where N ≈ 10–100 neurons);\r\n        the ONNX round-trip cost far exceeds the compute benefit at this size.\r\n        \"\"\"\r\n        np  = self._np\r\n        arr = np.array(sample, dtype=np.float64)\r\n        delta = np.outer(arr, arr) * learning_rate\r\n        n = len(sample)\r\n        for i in range(n):\r\n            for j in range(i + 1, n):\r\n                associations[i][j] += delta[i][j]\r\n                associations[j][i]  = associations[i][j]\r\n        return associations\r\n\r\n    def get_value(self, matrix, i: int, j: int) -> float:\r\n        return float(matrix[i][j])\r\n\r\n    @property\r\n    def name(self) -> str:\r\n        if self._available:\r\n            return f\"onnx [{self._provider}]\"\r\n        return \"numpy (onnx unavailable)\"\r\n\r\n\r\n# ---------------------------------------------------------------------------\r\n# Factory / singleton\r\n# ---------------------------------------------------------------------------\r\n\r\n_backend_instance: ComputeBackend | None = None\r\n\r\n\r\ndef get_backend(backend_name: str | None = None) -> ComputeBackend:\r\n    \"\"\"\r\n    Return the (singleton) compute backend.\r\n\r\n    backend_name : 'numpy' | 'onnx'\r\n        If None, reads [Compute] backend from config.ini.\r\n        Defaults to 'numpy' if the section / key is absent.\r\n\r\n    The ONNX backend silently falls back to NumPy if onnxruntime is not\r\n    installed, so callers never need to guard against ImportError.\r\n    \"\"\"\r\n    global _backend_instance\r\n    if _backend_instance is not None:\r\n        return _backend_instance\r\n\r\n    if backend_name is None:\r\n        backend_name = _read_backend_from_config()\r\n\r\n    backend_name = backend_name.strip().lower()\r\n\r\n    if backend_name == 'onnx':\r\n        candidate = ONNXBackend()\r\n        # If ONNX packages are missing, silently use NumPy\r\n        _backend_instance = candidate if candidate._available else NumpyBackend()\r\n    else:\r\n        _backend_instance = NumpyBackend()\r\n\r\n    return _backend_instance\r\n\r\n\r\ndef reset_backend():\r\n    \"\"\"Force re-initialisation on next get_backend() call (useful for tests).\"\"\"\r\n    global _backend_instance\r\n    _backend_instance = None\r\n\r\n\r\n# ---------------------------------------------------------------------------\r\n# Config reader (standalone – avoids circular import with config_manager)\r\n# ---------------------------------------------------------------------------\r\n\r\ndef _read_backend_from_config() -> str:\r\n    \"\"\"Read [Compute] backend from config.ini, returns 'numpy' on any error.\"\"\"\r\n    try:\r\n        import configparser\r\n        import os\r\n        import sys\r\n\r\n        config = configparser.ConfigParser()\r\n\r\n        if getattr(sys, 'frozen', False):\r\n            base_path = os.path.dirname(sys.executable)\r\n        else:\r\n            # __file__ is  src/compute_backend.py  → go up one level\r\n            src_dir   = os.path.dirname(os.path.abspath(__file__))\r\n            base_path = os.path.dirname(src_dir)\r\n\r\n        config_path = os.path.join(base_path, 'config.ini')\r\n        config.read(config_path)\r\n        return config.get('Compute', 'backend', fallback='numpy')\r\n\r\n    except Exception:\r\n        return 'numpy'\r\n"
  },
  {
    "path": "src/config_manager.py",
    "content": "import configparser\r\nimport os\r\nimport random\r\nimport sys\r\nfrom PyQt5 import QtCore\r\nfrom ast import literal_eval\r\n\r\nclass ConfigManager:\r\n    \"\"\"\r\n    Configuration manager for Dosidicus-2.\r\n    \r\n    Handles loading/saving of config.ini and provides typed accessors\r\n    for all configuration values including animation styles.\r\n    \"\"\"\r\n    \r\n    # Available animation styles - UPDATED to include all styles\r\n    ANIMATION_STYLES = ['vibrant', 'subtle', 'neural', 'none', 'electric', 'zen', 'neon']\r\n    \r\n    def __init__(self, config_filename=\"config.ini\"):\r\n        # Check if we are running as a frozen executable (Auto-Py-To-Exe)\r\n        if getattr(sys, 'frozen', False):\r\n            # If frozen, the config file should be next to the .exe\r\n            base_path = os.path.dirname(sys.executable)\r\n        else:\r\n            # If running from source (Dev), we are in /src/config_manager.py\r\n            # So we need to go up one level to find config.ini\r\n            current_dir = os.path.dirname(os.path.abspath(__file__))\r\n            base_path = os.path.dirname(current_dir) # Go up to parent folder\r\n\r\n        self.config_path = os.path.join(base_path, config_filename)\r\n        \r\n        self.config = configparser.ConfigParser()\r\n        self.load_config()\r\n\r\n    def load_config(self):\r\n        if not os.path.exists(self.config_path):\r\n            self.create_default_config()\r\n        self.config.read(self.config_path)\r\n\r\n    def create_default_config(self):\r\n        \r\n        self.config['General'] = {\r\n            'language': 'en'\r\n        }\r\n\r\n        # Debug section\r\n        self.config['Debug'] = {\r\n            'multiplayer_debug': 'False'\r\n        }\r\n\r\n        # Rock Interactions\r\n        self.config['RockInteractions'] = {\r\n            'pickup_probability': '0.7',\r\n            'throw_probability': '0.4',\r\n            'min_carry_duration': '4.0',\r\n            'max_carry_duration': '10.0',\r\n            'cooldown_after_throw': '10.0',\r\n            'happiness_boost': '9',\r\n            'satisfaction_boost': '11',\r\n            'anxiety_reduction': '9',\r\n            'memory_decay_rate': '0.98',\r\n            'max_rock_memories': '10'\r\n        }\r\n\r\n        # Poop Interactions - NEW SECTION\r\n        self.config['PoopInteractions'] = {\r\n            'pickup_probability': '0.2',\r\n            'throw_probability': '0.3',\r\n            'min_carry_duration': '2.0',\r\n            'max_carry_duration': '9.0',\r\n            'cooldown_after_throw': '10.0',\r\n        }\r\n\r\n        # Decorations Message System - NEW SECTION\r\n        self.config['Decorations'] = {\r\n            'message_enabled': 'True',\r\n            'message_max_shows': '3',\r\n            'message_min_interval': '120.0',  # 2 minutes in seconds\r\n            'message_max_interval': '300.0'   # 5 minutes in seconds\r\n        }\r\n\r\n        # Neurogenesis\r\n        self.config['Neurogenesis'] = {\r\n            'enabled': 'True',\r\n            'showmanship': 'True',\r\n            'pruning_enabled': 'True',  # NEW KEY\r\n            'cooldown': '60.0',\r\n            'per_type_cooldown': '30.0',\r\n            'max_novelty_neurons': '5',  # NEW KEY\r\n            'pattern_threshold': '3',  # NEW KEY\r\n            'experience_buffer_size': '30',  # NEW KEY\r\n            'min_utility_for_keep': '0.2',  # NEW KEY\r\n            'max_neurons': '100',\r\n            'initial_neuron_count': '7',\r\n            'max_hebbian_pairs': '2'\r\n        }\r\n\r\n        # Specialist Neuron Cap (Important!)\r\n        self.config['Neurogenesis.SpecialisationCaps'] = {\r\n            'novelty_object_investigation': '8',\r\n        }\r\n\r\n        # Neurogenesis Triggers\r\n        self.config['Neurogenesis.Novelty'] = {\r\n            'enabled': 'True',\r\n            'threshold': '3.0',\r\n            'decay_rate': '0.85',\r\n            'max_counter': '10.0',\r\n            'min_curiosity': '0.3',\r\n            'adventurous_modifier': '1.2',\r\n            'timid_modifier': '0.8'\r\n        }\r\n\r\n        self.config['Neurogenesis.Stress'] = {\r\n            'enabled': 'True',\r\n            'threshold': '2.0',\r\n            'decay_rate': '0.85',\r\n            'max_counter': '10.0',\r\n            'min_anxiety': '0.4',\r\n            'timid_modifier': '1.5',\r\n            'energetic_modifier': '0.7'\r\n        }\r\n\r\n        self.config['Neurogenesis.Reward'] = {\r\n            'enabled': 'True',\r\n            'threshold': '2.5',\r\n            'decay_rate': '0.85',\r\n            'max_counter': '8.0',\r\n            'min_satisfaction': '0.5',\r\n            'boost_multiplier': '1.1'\r\n        }\r\n\r\n        # Neuron Properties\r\n        self.config['Neurogenesis.NeuronProperties'] = {\r\n            'base_activation': '0.5',\r\n            'position_variance': '75',\r\n            'default_connections': 'True',\r\n            'connection_strength': '0.35',\r\n            'reciprocal_strength': '0.15',\r\n            'randomize_start_positions': 'True',  # NEW KEY\r\n            'canvas_padding': '60',  # NEW KEY\r\n            'centering_force': '0.02',  # NEW KEY\r\n            'force_bounds': 'True'  # NEW KEY\r\n        }\r\n\r\n        # Appearance\r\n        self.config['Neurogenesis.Appearance'] = {\r\n            'novelty_color': '255,255,150',\r\n            'stress_color': '255,150,150',\r\n            'reward_color': '150,255,150',\r\n            'novelty_shape': 'triangle',\r\n            'stress_shape': 'square',\r\n            'reward_shape': 'circle'\r\n        }\r\n\r\n        # Visual Effects\r\n        self.config['Neurogenesis.VisualEffects'] = {\r\n            'highlight_duration': '5.0',\r\n            'highlight_radius': '40',\r\n            'pulse_effect': 'True',\r\n            'pulse_speed': '0.5',\r\n            'animation_style': 'pattern_1'  # NEW KEY\r\n        }\r\n\r\n        # Hebbian Learning - NEW SECTION\r\n        self.config['Hebbian'] = {\r\n            'learning_interval': '30',\r\n            'base_learning_rate': '0.1',\r\n            'weight_decay': '0.01',\r\n            'min_weight': '-1.0',\r\n            'max_weight': '1.0',\r\n            'max_hebbian_pairs': '2'\r\n        }\r\n\r\n        # Link Blink - NEW SECTION\r\n        self.config['LinkBlink'] = {\r\n            'interval_min': '8.0',\r\n            'interval_max': '20.0',\r\n            'blink_duration': '2.0'\r\n        }\r\n\r\n        # ----------  ANIMATION STYLES  ----------\r\n        # All animation style configurations - UPDATED\r\n        self.config['Animation'] = {\r\n            'style': 'vibrant'\r\n        }\r\n\r\n        self.config['Animation.Electric'] = {\r\n            'use_thick_lines': 'True',\r\n            'line_base_width': '2.5',\r\n            'line_colour_positive': '0,255,255',\r\n            'line_colour_negative': '255,0,255',\r\n            'line_alpha': '255'\r\n        }\r\n\r\n        self.config['Animation.Zen'] = {\r\n            'use_thick_lines': 'False',\r\n            'line_base_width': '1.0',\r\n            'line_colour_positive': '60,60,60',\r\n            'line_colour_negative': '120,120,120',\r\n            'line_alpha': '120'\r\n        }\r\n\r\n        self.config['Animation.Neon'] = {\r\n            'use_thick_lines': 'True',\r\n            'line_base_width': '1.8',\r\n            'line_colour_positive': '0,255,120',\r\n            'line_colour_negative': '255,50,205',\r\n            'line_alpha': '240'\r\n        }\r\n\r\n        # Display Settings\r\n        self.config['Display'] = {\r\n            'neuron_label_font_size': '8',\r\n            'neuron_radius': '14',  # Updated to match current default\r\n            'connection_line_width': '1.5',\r\n            'button_font_size': '16',\r\n            'button_width': '140',\r\n            'button_height': '50',\r\n            'button_spacing': '20'\r\n        }\r\n\r\n        # Designer Settings\r\n        self.config['Designer'] = {\r\n            'designer_min_neuron_distance': '100',\r\n            'designer_max_neuron_distance': '600'\r\n        }\r\n\r\n        with open(self.config_path, 'w') as f:\r\n            self.config.write(f)\r\n\r\n    def get_hebbian_pairs_per_cycle(self):\r\n        \"\"\"Return the number of neuron pairs that should be updated each Hebbian cycle.\"\"\"\r\n        # Check Hebbian section first, fallback to Neurogenesis\r\n        if self.config.has_section('Hebbian'):\r\n            return self.config.getint('Hebbian', 'max_hebbian_pairs', fallback=2)\r\n        return self.config.getint('Neurogenesis', 'max_hebbian_pairs', fallback=2)\r\n\r\n    def get_showmanship_enabled(self):\r\n        \"\"\"Return whether showmanship (dramatic neuron creation) is enabled.\r\n        \r\n        When enabled, the ShowmanNeurogenesis wrapper will create neurons\r\n        during dramatic moments even if normal thresholds aren't met.\r\n        When disabled, only the base EnhancedNeurogenesis logic applies.\r\n        \"\"\"\r\n        return self.config.getboolean('Neurogenesis', 'showmanship', fallback=True)\r\n\r\n\r\n    def get_facts_enabled(self):\r\n        return self.config.getboolean('Facts', 'enabled', fallback=True)\r\n\r\n    def get_fact_interval_ms(self):\r\n        mins = self.config.getint('Facts', 'interval_minutes', fallback=5)\r\n        return mins * 60 * 1000\r\n\r\n    def get_fact_display_ms(self):\r\n        secs = self.config.getint('Facts', 'display_seconds', fallback=18)\r\n        return secs * 1000\r\n\r\n    def get_rock_config(self):\r\n        return {\r\n            'pickup_prob': float(self.config['RockInteractions']['pickup_probability']),\r\n            'throw_prob': float(self.config['RockInteractions']['throw_probability']),\r\n            'min_carry_duration': float(self.config['RockInteractions']['min_carry_duration']),\r\n            'max_carry_duration': float(self.config['RockInteractions']['max_carry_duration']),\r\n            'cooldown_after_throw': float(self.config['RockInteractions']['cooldown_after_throw']),\r\n            'happiness_boost': int(self.config['RockInteractions']['happiness_boost']),\r\n            'satisfaction_boost': int(self.config['RockInteractions']['satisfaction_boost']),\r\n            'anxiety_reduction': int(self.config['RockInteractions']['anxiety_reduction']),\r\n            'memory_decay_rate': float(self.config['RockInteractions']['memory_decay_rate']),\r\n            'max_rock_memories': int(self.config['RockInteractions']['max_rock_memories'])\r\n        }\r\n    \r\n    def get_poop_config(self):\r\n        \"\"\"NEW METHOD: Get poop interaction configuration\"\"\"\r\n        # Fallback dictionary in case section is missing\r\n        defaults = {\r\n            'pickup_prob': 0.2,\r\n            'throw_prob': 0.3,\r\n            'min_carry_duration': 2.0,\r\n            'max_carry_duration': 9.0,\r\n            'cooldown_after_throw': 10.0,\r\n        }\r\n\r\n        if self.config.has_section('PoopInteractions'):\r\n            section = self.config['PoopInteractions']\r\n            return {\r\n                'pickup_prob': float(section.get('pickup_probability', defaults['pickup_prob'])),\r\n                'throw_prob': float(section.get('throw_probability', defaults['throw_prob'])),\r\n                'min_carry_duration': float(section.get('min_carry_duration', defaults['min_carry_duration'])),\r\n                'max_carry_duration': float(section.get('max_carry_duration', defaults['max_carry_duration'])),\r\n                'cooldown_after_throw': float(section.get('cooldown_after_throw', defaults['cooldown_after_throw']))\r\n            }\r\n        return defaults\r\n    \r\n    def get_decorations_config(self):\r\n        \"\"\"NEW METHOD: Get decorations message system configuration\"\"\"\r\n        return {\r\n            'message_enabled': self.config.getboolean('Decorations', 'message_enabled', fallback=True),\r\n            'message_max_shows': self.config.getint('Decorations', 'message_max_shows', fallback=3),\r\n            'message_min_interval': self.config.getfloat('Decorations', 'message_min_interval', fallback=120.0),\r\n            'message_max_interval': self.config.getfloat('Decorations', 'message_max_interval', fallback=300.0)\r\n        }\r\n    \r\n    def get_specialisation_caps(self):\r\n        \"\"\"Return dict {specialisation_name: int_max}\"\"\"\r\n        caps = {}\r\n        if self.config.has_section('Neurogenesis.SpecialisationCaps'):\r\n            for spec, val in self.config.items('Neurogenesis.SpecialisationCaps'):\r\n                caps[spec] = int(val)\r\n        return caps\r\n\r\n    def get_neurogenesis_config(self):\r\n        \"\"\"Returns the complete neurogenesis configuration as a dictionary\"\"\"\r\n        return {\r\n            'general': {\r\n                'enabled': self.config.getboolean('Neurogenesis', 'enabled', fallback=True),\r\n                'showmanship': self.config.getboolean('Neurogenesis', 'showmanship', fallback=True),\r\n                'pruning_enabled': self.config.getboolean('Neurogenesis', 'pruning_enabled', fallback=True),  # NEW KEY\r\n                'cooldown': self.config.getfloat('Neurogenesis', 'cooldown', fallback=60.0),\r\n                'per_type_cooldown': self.config.getfloat('Neurogenesis', 'per_type_cooldown', fallback=30.0),\r\n                'max_novelty_neurons': self.config.getint('Neurogenesis', 'max_novelty_neurons', fallback=5),  # NEW KEY\r\n                'pattern_threshold': self.config.getint('Neurogenesis', 'pattern_threshold', fallback=3),  # NEW KEY\r\n                'experience_buffer_size': self.config.getint('Neurogenesis', 'experience_buffer_size', fallback=30),  # NEW KEY\r\n                'min_utility_for_keep': self.config.getfloat('Neurogenesis', 'min_utility_for_keep', fallback=0.2),  # NEW KEY\r\n                'max_neurons': self.config.getint('Neurogenesis', 'max_neurons', fallback=100),\r\n                'initial_neuron_count': self.config.getint('Neurogenesis', 'initial_neuron_count', fallback=7),\r\n                'max_hebbian_pairs': self.config.getint('Neurogenesis', 'max_hebbian_pairs', fallback=2)\r\n            },\r\n            'triggers': {\r\n                'novelty': {\r\n                    'enabled': self.config.getboolean('Neurogenesis.Novelty', 'enabled', fallback=True),\r\n                    'threshold': self.config.getfloat('Neurogenesis.Novelty', 'threshold', fallback=3.0),\r\n                    'decay_rate': self.config.getfloat('Neurogenesis.Novelty', 'decay_rate', fallback=0.85),\r\n                    'max_counter': self.config.getfloat('Neurogenesis.Novelty', 'max_counter', fallback=10.0),\r\n                    'min_curiosity': self.config.getfloat('Neurogenesis.Novelty', 'min_curiosity', fallback=0.3),\r\n                    'personality_modifiers': {\r\n                        'adventurous': self.config.getfloat('Neurogenesis.Novelty', 'adventurous_modifier', fallback=1.2),\r\n                        'timid': self.config.getfloat('Neurogenesis.Novelty', 'timid_modifier', fallback=0.8)\r\n                    }\r\n                },\r\n                'stress': {\r\n                    'enabled': self.config.getboolean('Neurogenesis.Stress', 'enabled', fallback=True),\r\n                    'threshold': self.config.getfloat('Neurogenesis.Stress', 'threshold', fallback=2.0),\r\n                    'decay_rate': self.config.getfloat('Neurogenesis.Stress', 'decay_rate', fallback=0.85),\r\n                    'max_counter': self.config.getfloat('Neurogenesis.Stress', 'max_counter', fallback=10.0),\r\n                    'min_anxiety': self.config.getfloat('Neurogenesis.Stress', 'min_anxiety', fallback=0.4),\r\n                    'personality_modifiers': {\r\n                        'timid': self.config.getfloat('Neurogenesis.Stress', 'timid_modifier', fallback=1.5),\r\n                        'energetic': self.config.getfloat('Neurogenesis.Stress', 'energetic_modifier', fallback=0.7)\r\n                    }\r\n                },\r\n                'reward': {\r\n                    'enabled': self.config.getboolean('Neurogenesis.Reward', 'enabled', fallback=True),\r\n                    'threshold': self.config.getfloat('Neurogenesis.Reward', 'threshold', fallback=2.5),\r\n                    'decay_rate': self.config.getfloat('Neurogenesis.Reward', 'decay_rate', fallback=0.85),\r\n                    'max_counter': self.config.getfloat('Neurogenesis.Reward', 'max_counter', fallback=8.0),\r\n                    'min_satisfaction': self.config.getfloat('Neurogenesis.Reward', 'min_satisfaction', fallback=0.5),\r\n                    'boost_multiplier': self.config.getfloat('Neurogenesis.Reward', 'boost_multiplier', fallback=1.1)\r\n                }\r\n            },\r\n            'neuron_properties': {\r\n                'base_activation': self.config.getfloat('Neurogenesis.NeuronProperties', 'base_activation', fallback=0.5),\r\n                'position_variance': self.config.getint('Neurogenesis.NeuronProperties', 'position_variance', fallback=75),\r\n                'default_connections': self.config.getboolean('Neurogenesis.NeuronProperties', 'default_connections', fallback=True),\r\n                'connection_strength': self.config.getfloat('Neurogenesis.NeuronProperties', 'connection_strength', fallback=0.35),\r\n                'reciprocal_strength': self.config.getfloat('Neurogenesis.NeuronProperties', 'reciprocal_strength', fallback=0.15),\r\n                'randomize_start_positions': self.config.getboolean('Neurogenesis.NeuronProperties', 'randomize_start_positions', fallback=True),  # NEW KEY\r\n                'canvas_padding': self.config.getint('Neurogenesis.NeuronProperties', 'canvas_padding', fallback=60),  # NEW KEY\r\n                'centering_force': self.config.getfloat('Neurogenesis.NeuronProperties', 'centering_force', fallback=0.02),  # NEW KEY\r\n                'force_bounds': self.config.getboolean('Neurogenesis.NeuronProperties', 'force_bounds', fallback=True)  # NEW KEY\r\n            },\r\n            'designer': {\r\n                'min_neuron_distance': self.config.getint('Designer', 'designer_min_neuron_distance', fallback=100),\r\n                'max_neuron_distance': self.config.getint('Designer', 'designer_max_neuron_distance', fallback=600)\r\n            },\r\n            'appearance': {\r\n                'colors': {\r\n                    'novelty': [int(x) for x in self.config.get('Neurogenesis.Appearance', 'novelty_color', fallback='255,255,150').split(',')],\r\n                    'stress': [int(x) for x in self.config.get('Neurogenesis.Appearance', 'stress_color', fallback='255,150,150').split(',')],\r\n                    'reward': [int(x) for x in self.config.get('Neurogenesis.Appearance', 'reward_color', fallback='150,255,150').split(',')]\r\n                },\r\n                'shapes': {\r\n                    'novelty': self.config.get('Neurogenesis.Appearance', 'novelty_shape', fallback='triangle'),\r\n                    'stress': self.config.get('Neurogenesis.Appearance', 'stress_shape', fallback='square'),\r\n                    'reward': self.config.get('Neurogenesis.Appearance', 'reward_shape', fallback='circle')\r\n                }\r\n            },\r\n            'visual_effects': {\r\n                'highlight_duration': self.config.getfloat('Neurogenesis.VisualEffects', 'highlight_duration', fallback=5.0),\r\n                'highlight_radius': self.config.getint('Neurogenesis.VisualEffects', 'highlight_radius', fallback=40),\r\n                'pulse_effect': self.config.getboolean('Neurogenesis.VisualEffects', 'pulse_effect', fallback=True),\r\n                'pulse_speed': self.config.getfloat('Neurogenesis.VisualEffects', 'pulse_speed', fallback=0.5),\r\n                'animation_style': self.config.get('Neurogenesis.VisualEffects', 'animation_style', fallback='pattern_1')  # NEW KEY\r\n            }\r\n        }\r\n\r\n    def get_hebbian_config(self):\r\n        \"\"\"NEW METHOD: Get Hebbian learning configuration\"\"\"\r\n        return {\r\n            'learning_interval': self.config.getint('Hebbian', 'learning_interval', fallback=30),\r\n            'base_learning_rate': self.config.getfloat('Hebbian', 'base_learning_rate', fallback=0.1),\r\n            'weight_decay': self.config.getfloat('Hebbian', 'weight_decay', fallback=0.01),\r\n            'min_weight': self.config.getfloat('Hebbian', 'min_weight', fallback=-1.0),\r\n            'max_weight': self.config.getfloat('Hebbian', 'max_weight', fallback=1.0),\r\n            'max_hebbian_pairs': self.config.getint('Hebbian', 'max_hebbian_pairs', fallback=2)\r\n        }\r\n\r\n    def get_linkblink_config(self):\r\n        \"\"\"NEW METHOD: Get link blink animation configuration\"\"\"\r\n        return {\r\n            'interval_min': self.config.getfloat('LinkBlink', 'interval_min', fallback=8.0),\r\n            'interval_max': self.config.getfloat('LinkBlink', 'interval_max', fallback=20.0),\r\n            'blink_duration': self.config.getfloat('LinkBlink', 'blink_duration', fallback=2.0)\r\n        }\r\n\r\n    def get_random_carry_duration(self):\r\n        \"\"\"Returns random duration between min and max carry duration\"\"\"\r\n        config = self.get_rock_config()\r\n        return random.uniform(config['min_carry_duration'], config['max_carry_duration'])\r\n\r\n    def _parse_config_value(self, value):\r\n        \"\"\"Parse configuration values that might contain comments\"\"\"\r\n        # Remove everything after comment markers\r\n        for comment_marker in [';', '#', '//']:\r\n            if comment_marker in value:\r\n                value = value.split(comment_marker)[0]\r\n        \r\n        value = value.strip()\r\n        \r\n        # Try to convert to appropriate type\r\n        if value.lower() == 'true':\r\n            return True\r\n        elif value.lower() == 'false':\r\n            return False\r\n        elif value.isdigit():\r\n            return int(value)\r\n        try:\r\n            return float(value)\r\n        except ValueError:\r\n            return value\r\n\r\n    # =========================================================================\r\n    # ANIMATION STYLE CONFIGURATION\r\n    # =========================================================================\r\n    \r\n    def get_animation_style(self) -> str:\r\n        \"\"\"\r\n        Get the current animation style name.\r\n        \r\n        Returns one of: 'vibrant', 'subtle', 'neural', 'none', 'electric', 'zen', 'neon'\r\n        \"\"\"\r\n        if not self.config.has_section('Animation'):\r\n            return 'vibrant'  # Default style\r\n        return self.config.get('Animation', 'style', fallback='vibrant').lower()\r\n    \r\n    def set_animation_style(self, style_name: str) -> bool:\r\n        \"\"\"\r\n        Set the animation style and save to config.\r\n        \r\n        Args:\r\n            style_name: One of 'vibrant', 'subtle', 'neural', 'electric', 'zen', 'neon'\r\n            \r\n        Returns:\r\n            True if style was set, False if invalid style name\r\n        \"\"\"\r\n        style_lower = style_name.lower()\r\n        if style_lower not in self.ANIMATION_STYLES:\r\n            print(f\"⚠️ Invalid animation style: {style_name}. \"\r\n                  f\"Available: {self.ANIMATION_STYLES}\")\r\n            return False\r\n        \r\n        if not self.config.has_section('Animation'):\r\n            self.config.add_section('Animation')\r\n        \r\n        self.config.set('Animation', 'style', style_lower)\r\n        self._save_config()\r\n        return True\r\n    \r\n    def get_animation_config(self, style_name: str = None) -> dict:\r\n        \"\"\"\r\n        Get animation configuration for a specific style.\r\n        \r\n        This provides config values that can override the dataclass defaults\r\n        in animation_styles.py, allowing user customization via config.ini.\r\n        \r\n        Args:\r\n            style_name: Style to get config for (defaults to current style)\r\n            \r\n        Returns:\r\n            Dictionary of animation parameters\r\n        \"\"\"\r\n        if style_name is None:\r\n            style_name = self.get_animation_style()\r\n        \r\n        section_name = f'Animation.{style_name.capitalize()}'\r\n        \r\n        # Default values for each style\r\n        defaults = {\r\n            'vibrant': {\r\n                'use_thick_lines': 'True',\r\n                'pulse_colour': '255,220,50',\r\n                'pulse_alpha': '240',\r\n                'pulse_duration': '1.8',\r\n                'pulse_speed': '1.0',\r\n                'line_base_width': '1.5',\r\n                'line_colour_positive': '50,220,50',\r\n                'line_colour_negative': '255,60,60',\r\n                'line_alpha': '220',\r\n            },\r\n            'subtle': {\r\n                'use_thick_lines': 'False',\r\n                'pulse_colour': '200,200,150',\r\n                'pulse_alpha': '160',\r\n                'pulse_duration': '2.5',\r\n                'pulse_speed': '0.8',\r\n                'line_base_width': '0.8',\r\n                'line_colour_positive': '80,160,80',\r\n                'line_colour_negative': '180,80,80',\r\n                'line_alpha': '140',\r\n            },\r\n            'neural': {\r\n                'use_thick_lines': 'False',\r\n                'pulse_colour': '180,230,255',\r\n                'pulse_alpha': '200',\r\n                'pulse_duration': '0.9',\r\n                'pulse_speed': '1.1',\r\n                'line_base_width': '1.0',\r\n                'line_colour_positive': '100,200,255',\r\n                'line_colour_negative': '255,120,100',\r\n                'line_alpha': '180',\r\n                'neural_base_colour_positive': '100,200,255',\r\n                'neural_base_colour_negative': '255,120,100',\r\n                'neural_base_alpha': '180',\r\n            },\r\n            'electric': {\r\n                'use_thick_lines': 'True',\r\n                'pulse_colour': '255,255,255',\r\n                'pulse_alpha': '255',\r\n                'pulse_duration': '0.15',\r\n                'pulse_speed': '4.0',\r\n                'line_base_width': '2.5',\r\n                'line_colour_positive': '0,255,255',\r\n                'line_colour_negative': '255,0,255',\r\n                'line_alpha': '255',\r\n            },\r\n            'zen': {\r\n                'use_thick_lines': 'False',\r\n                'pulse_colour': '200,200,200',\r\n                'pulse_alpha': '120',\r\n                'pulse_duration': '3.0',\r\n                'pulse_speed': '0.5',\r\n                'line_base_width': '1.0',\r\n                'line_colour_positive': '60,60,60',\r\n                'line_colour_negative': '120,120,120',\r\n                'line_alpha': '120',\r\n            },\r\n            'neon': {\r\n                'use_thick_lines': 'True',\r\n                'pulse_colour': '255,255,255',\r\n                'pulse_alpha': '255',\r\n                'pulse_duration': '0.3',\r\n                'pulse_speed': '3.5',\r\n                'line_base_width': '1.8',\r\n                'line_colour_positive': '0,255,120',\r\n                'line_colour_negative': '255,50,205',\r\n                'line_alpha': '240',\r\n            }\r\n        }\r\n        \r\n        style_defaults = defaults.get(style_name.lower(), defaults['vibrant'])\r\n        \r\n        # Override with config file values if section exists\r\n        if self.config.has_section(section_name):\r\n            for key in style_defaults:\r\n                if self.config.has_option(section_name, key):\r\n                    style_defaults[key] = self.config.get(section_name, key)\r\n        \r\n        return style_defaults\r\n    \r\n    def get_available_animation_styles(self) -> list:\r\n        \"\"\"Return list of available animation style names.\"\"\"\r\n        return list(self.ANIMATION_STYLES)\r\n    \r\n    def get_display_config(self):\r\n        \"\"\"Get display configuration settings\"\"\"\r\n        return {\r\n            'neuron_label_font_size': self.config.getint('Display', 'neuron_label_font_size', fallback=8),\r\n            'neuron_radius': self.config.getint('Display', 'neuron_radius', fallback=14),\r\n            'connection_line_width': self.config.getfloat('Display', 'connection_line_width', fallback=1.5),\r\n            'button_font_size': self.config.getint('Display', 'button_font_size', fallback=16),\r\n            'button_width': self.config.getint('Display', 'button_width', fallback=140),\r\n            'button_height': self.config.getint('Display', 'button_height', fallback=50),\r\n            'button_spacing': self.config.getint('Display', 'button_spacing', fallback=20)\r\n        }\r\n    \r\n    def is_designer_position_valid(self, x, y, existing_positions, center_x=0, center_y=0):\r\n        \"\"\"\r\n        Checks if a position is valid based on designer config constraints.\r\n        \r\n        Args:\r\n            x, y: The proposed coordinates.\r\n            existing_positions: List of tuples [(x,y), ...] or objects with .x, .y attributes.\r\n            center_x, center_y: The origin point to measure max distance from (default 0,0).\r\n            \r\n        Returns:\r\n            True if valid, False if it violates designer constraints.\r\n        \"\"\"\r\n        import math\r\n        \r\n        # Load constraints from the new Designer section\r\n        min_dist = self.config.getint('Designer', 'designer_min_neuron_distance', fallback=100)\r\n        max_dist = self.config.getint('Designer', 'designer_max_neuron_distance', fallback=600)\r\n\r\n        # 1. Check Max Distance (Radius from center)\r\n        dist_from_center = math.sqrt((x - center_x)**2 + (y - center_y)**2)\r\n        if dist_from_center > max_dist:\r\n            return False\r\n\r\n        # 2. Check Min Distance (Proximity to other neurons)\r\n        for pos in existing_positions:\r\n            # Handle both tuples and objects with .x .y attributes\r\n            if hasattr(pos, 'x') and hasattr(pos, 'y'):\r\n                ex, ey = pos.x, pos.y\r\n            else:\r\n                ex, ey = pos[0], pos[1]\r\n                \r\n            dist_to_neighbor = math.sqrt((x - ex)**2 + (y - ey)**2)\r\n            if dist_to_neighbor < min_dist:\r\n                return False\r\n\r\n        return True\r\n\r\n\r\n    def _save_config(self):\r\n        \"\"\"Save the current configuration to file.\"\"\"\r\n        try:\r\n            with open(self.config_path, 'w') as f:\r\n                self.config.write(f)\r\n        except Exception as e:\r\n            print(f\"⚠️ Failed to save config: {e}\")\r\n\r\n    def get_language(self):\r\n        if not self.config.has_section('General'):\r\n            return 'en'\r\n        return self.config.get('General', 'language', fallback='en').lower()"
  },
  {
    "path": "src/custom_brain_loader.py",
    "content": "import os\r\nimport json\r\nimport shutil\r\nimport time\r\nfrom pathlib import Path\r\nfrom typing import Optional, Dict\r\nfrom PyQt5 import QtWidgets, QtCore, QtGui\r\nfrom .brain_neuron_outputs import NeuronOutputBinding, OutputTriggerMode\r\n\r\n\r\n# Global tracker for the currently loaded custom brain\r\n_current_custom_brain: Optional[Dict] = None\r\n_current_custom_brain_name: Optional[str] = None\r\n_current_custom_brain_file: Optional[str] = None\r\n\r\n\r\ndef add_load_brain_button(network_tab, checkbox_layout):\r\n    \"\"\"\r\n    Add a Load Brain button to the NetworkTab.\r\n    \r\n    Args:\r\n        network_tab: The NetworkTab instance (self)\r\n        checkbox_layout: The checkbox layout to add the button to\r\n    \"\"\"\r\n    # Add some spacing\r\n    checkbox_layout.addSpacing(20)\r\n    \r\n    # Create the button\r\n    load_btn = QtWidgets.QPushButton(\"Load Brain...\")\r\n    load_btn.setToolTip(\"Load a custom brain architecture\")\r\n    load_btn.setStyleSheet(\"\"\"\r\n        QPushButton {\r\n            background-color: #5c6bc0;\r\n            color: white;\r\n            border: none;\r\n            border-radius: 3px;\r\n            padding: 4px 12px;\r\n            font-weight: bold;\r\n            font-size: 9pt;\r\n        }\r\n        QPushButton:hover {\r\n            background-color: #7986cb;\r\n        }\r\n        QPushButton:pressed {\r\n            background-color: #3f51b5;\r\n        }\r\n    \"\"\")\r\n    \r\n    # Create loader and connect\r\n    loader = BrainLoader(network_tab)\r\n    network_tab._brain_loader = loader\r\n    load_btn.clicked.connect(loader.show_dialog)\r\n    \r\n    checkbox_layout.addWidget(load_btn)\r\n    \r\n    # --- RESET BUTTON (with “Are you sure?” guard) ---\r\n    reset_btn = QtWidgets.QPushButton(\"↺\")\r\n    reset_btn.setToolTip(\"Reset to default brain\")\r\n    reset_btn.setFixedSize(50, 50)\r\n    reset_btn.setStyleSheet(\"\"\"\r\n        QPushButton {\r\n            background-color: #78909c;\r\n            color: white;\r\n            border: none;\r\n            border-radius: 3px;\r\n            font-weight: bold;\r\n        }\r\n        QPushButton:hover { background-color: #90a4ae; }\r\n    \"\"\")\r\n\r\n    def _confirm_reset():\r\n        ans = QtWidgets.QMessageBox.question(\r\n            network_tab,           # parent widget\r\n            \"Confirm Reset\",\r\n            \"Reset all neuron positions to their default locations?\\n\\n\"\r\n            \"Network structure (connections and weights) will be preserved.\",\r\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,\r\n            QtWidgets.QMessageBox.No\r\n        )\r\n        if ans == QtWidgets.QMessageBox.Yes:\r\n            try:\r\n                loader.reset_positions_to_default()\r\n            except Exception as e:\r\n                QtWidgets.QMessageBox.critical(\r\n                    network_tab, \"Error\", f\"Failed to reset positions:\\n{e}\"\r\n                )\r\n    \r\n    reset_btn.clicked.connect(_confirm_reset)\r\n    checkbox_layout.addWidget(reset_btn)\r\n\r\n\r\n# =============================================================================\r\n# SAVE/LOAD INTEGRATION API\r\n# =============================================================================\r\n\r\ndef get_custom_brain_save_data() -> Optional[Dict]:\r\n    \"\"\"\r\n    Get custom brain data for saving with game state.\r\n    Returns None if using default brain.\r\n    \r\n    Call this from tamagotchi_logic.save_game() to include in save_data.\r\n    \"\"\"\r\n    global _current_custom_brain, _current_custom_brain_name\r\n    \r\n    if _current_custom_brain is None:\r\n        return None\r\n    \r\n    return {\r\n        'is_custom_brain': True,\r\n        'brain_name': _current_custom_brain_name,\r\n        'brain_definition': _current_custom_brain,\r\n        'source_file': _current_custom_brain_file,\r\n    }\r\n\r\n\r\ndef has_custom_brain() -> bool:\r\n    \"\"\"Check if a custom brain is currently loaded.\"\"\"\r\n    return _current_custom_brain is not None\r\n\r\n\r\ndef get_custom_brain_name() -> Optional[str]:\r\n    \"\"\"Get the name of the currently loaded custom brain.\"\"\"\r\n    return _current_custom_brain_name\r\n\r\n\r\ndef validate_custom_brain_save(save_data: Dict) -> tuple[bool, str]:\r\n    \"\"\"\r\n    Validate that a save file's custom brain can be restored.\r\n    \r\n    Returns:\r\n        (is_valid, message) - is_valid=True if loadable, message explains any issues\r\n    \"\"\"\r\n    custom_brain_data = save_data.get('custom_brain')\r\n    \r\n    if not custom_brain_data:\r\n        return True, \"Default brain (no custom brain)\"\r\n    \r\n    if not custom_brain_data.get('is_custom_brain'):\r\n        return True, \"Default brain\"\r\n    \r\n    brain_def = custom_brain_data.get('brain_definition')\r\n    if not brain_def:\r\n        return False, \"Save contains custom brain flag but no brain definition!\"\r\n    \r\n    # Check if brain definition looks valid\r\n    if 'positions' not in brain_def and 'neurons' not in brain_def:\r\n        return False, \"Custom brain definition is corrupted (no neurons)\"\r\n    \r\n    brain_name = custom_brain_data.get('brain_name', 'Unknown')\r\n    return True, f\"Custom brain: {brain_name}\"\r\n\r\n\r\ndef restore_custom_brain_from_save(save_data: Dict, brain_widget) -> tuple[bool, str]:\r\n    \"\"\"\r\n    Restore a custom brain from save data.\r\n    \r\n    Args:\r\n        save_data: The loaded save data dict\r\n        brain_widget: The BrainWidget to apply the brain to\r\n        \r\n    Returns:\r\n        (success, message)\r\n    \"\"\"\r\n    global _current_custom_brain, _current_custom_brain_name, _current_custom_brain_file\r\n    \r\n    custom_brain_data = save_data.get('custom_brain')\r\n    \r\n    if not custom_brain_data or not custom_brain_data.get('is_custom_brain'):\r\n        # No custom brain - reset to default\r\n        _current_custom_brain = None\r\n        _current_custom_brain_name = None\r\n        _current_custom_brain_file = None\r\n        return True, \"Using default brain\"\r\n    \r\n    brain_def = custom_brain_data.get('brain_definition')\r\n    brain_name = custom_brain_data.get('brain_name', 'Unknown')\r\n    \r\n    if not brain_def:\r\n        return False, \"Custom brain definition missing from save!\"\r\n    \r\n    try:\r\n        # Create a temporary loader to apply the brain\r\n        loader = BrainLoader.__new__(BrainLoader)\r\n        loader.brain_widget = brain_widget\r\n        loader.network_tab = None\r\n        \r\n        # Parse and apply\r\n        parsed = loader._parse(brain_def)\r\n        if not parsed:\r\n            return False, f\"Could not parse custom brain '{brain_name}'\"\r\n        \r\n        loader._apply(parsed)\r\n        \r\n        # Update global tracking\r\n        _current_custom_brain = brain_def\r\n        _current_custom_brain_name = brain_name\r\n        _current_custom_brain_file = custom_brain_data.get('source_file')\r\n        \r\n        return True, f\"Restored custom brain: {brain_name}\"\r\n        \r\n    except Exception as e:\r\n        return False, f\"Error restoring custom brain: {e}\"\r\n\r\n\r\ndef show_custom_brain_load_warning(parent, save_data: Dict) -> bool:\r\n    \"\"\"\r\n    Show warning dialog when loading a save with custom brain.\r\n    \r\n    Returns True if user wants to proceed, False to cancel.\r\n    \"\"\"\r\n    # VALIDATION FIX: Ensure parent is a QWidget or None\r\n    if parent and not isinstance(parent, QtWidgets.QWidget):\r\n        if hasattr(parent, 'mainwindow'):\r\n            parent = parent.mainwindow\r\n        elif hasattr(parent, 'central_widget'):\r\n            parent = parent.central_widget\r\n        else:\r\n            # Fallback to None (desktop parent) to prevent crash\r\n            parent = None\r\n\r\n    custom_brain_data = save_data.get('custom_brain')\r\n    \r\n    if not custom_brain_data or not custom_brain_data.get('is_custom_brain'):\r\n        return True  # No custom brain, no warning needed\r\n    \r\n    brain_name = custom_brain_data.get('brain_name', 'Unknown')\r\n    source_file = custom_brain_data.get('source_file', 'Unknown')\r\n    \r\n    # Validate the brain can be restored\r\n    is_valid, message = validate_custom_brain_save(save_data)\r\n    \r\n    if not is_valid:\r\n        QtWidgets.QMessageBox.critical(\r\n            parent,\r\n            \"Cannot Load Save\",\r\n            f\"This save uses a custom brain that cannot be restored:\\n\\n\"\r\n            f\"Brain: {brain_name}\\n\"\r\n            f\"Error: {message}\\n\\n\"\r\n            f\"The save file may be corrupted.\"\r\n        )\r\n        return False\r\n    \r\n    # Show info dialog\r\n    reply = QtWidgets.QMessageBox.question(\r\n        parent,\r\n        \"Custom Brain Save\",\r\n        f\"This save uses a custom brain architecture:\\n\\n\"\r\n        f\"🧠 Brain: {brain_name}\\n\"\r\n        f\"📁 Original file: {source_file}\\n\\n\"\r\n        f\"The brain definition is embedded in the save and will be restored.\\n\\n\"\r\n        f\"Continue loading?\",\r\n        QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,\r\n        QtWidgets.QMessageBox.Yes\r\n    )\r\n    \r\n    return reply == QtWidgets.QMessageBox.Yes\r\n\r\n\r\n# =============================================================================\r\n# BRAIN LOADER CLASS\r\n# =============================================================================\r\n\r\nclass BrainLoader:\r\n    \"\"\"Handles brain file loading.\"\"\"\r\n    \r\n    def __init__(self, network_tab):\r\n        self.network_tab = network_tab\r\n        self.brain_widget = network_tab.brain_widget\r\n        self.brains_folder = self._find_brains_folder()\r\n    \r\n    def _find_brains_folder(self) -> Path:\r\n        \"\"\"Find or create custom_brains folder (one level up from Brain/).\"\"\"\r\n        candidates = [\r\n            Path(__file__).parent.parent / \"custom_brains\",  # Up from Brain/ to project root\r\n            Path.cwd() / \"custom_brains\",\r\n            Path(__file__).parent / \"custom_brains\",  # Fallback to Brain/custom_brains\r\n        ]\r\n        for p in candidates:\r\n            if p.exists():\r\n                return p\r\n        \r\n        # Create at project root level (one up from Brain/)\r\n        default = candidates[0]\r\n        default.mkdir(parents=True, exist_ok=True)\r\n        return default\r\n    \r\n    def show_dialog(self):\r\n        \"\"\"Show the brain selection dialog.\"\"\"\r\n        dialog = BrainSelectDialog(self.brains_folder, self.network_tab)\r\n        if dialog.exec_() == QtWidgets.QDialog.Accepted and dialog.selected_file:\r\n            self.load_file(dialog.selected_file)\r\n    \r\n    def load_file(self, filepath: str) -> bool:\r\n        \"\"\"Load and apply a brain file.\"\"\"\r\n        global _current_custom_brain, _current_custom_brain_name, _current_custom_brain_file\r\n        \r\n        try:\r\n            print(f\"🧠 Loading brain: {filepath}\")\r\n            \r\n            with open(filepath, 'r') as f:\r\n                data = json.load(f)\r\n            \r\n            brain = self._parse(data)\r\n            if not brain:\r\n                raise ValueError(\"Could not parse brain format\")\r\n            \r\n            self._apply(brain)\r\n            \r\n            # 1. Send bindings to the live game logic\r\n            if hasattr(self.network_tab, 'tamagotchi_logic'):\r\n                logic = self.network_tab.tamagotchi_logic\r\n                if hasattr(logic, 'neuron_output_monitor') and logic.neuron_output_monitor:\r\n                    logic.neuron_output_monitor.load_bindings_from_brain(brain)\r\n                    print(f\"  ✓ Loaded {len(brain.get('output_bindings', []))} output bindings into Monitor\")\r\n\r\n                # --- FIX: Force-sync live sensor values immediately ---\r\n                # This prevents the saved \"0\" value from overwriting the real sensor input\r\n                # in the first frame after loading.\r\n                if hasattr(logic, 'brain_hooks'):\r\n                    print(\"  -> Syncing live sensor inputs...\")\r\n                    input_values = logic.brain_hooks.get_input_neuron_values()\r\n                    \r\n                    # Update widget state directly\r\n                    bw = self.brain_widget\r\n                    if hasattr(bw, 'state'):\r\n                        bw.state.update(input_values)\r\n                    \r\n                    # Update internal simulation neurons if present\r\n                    if hasattr(bw, 'neurons'):\r\n                        for k, v in input_values.items():\r\n                            if k in bw.neurons:\r\n                                bw.neurons[k]['activation'] = v\r\n                # --- END FIX ---\r\n\r\n            # 2. UI OVERLAY SETUP\r\n            # Check if the stats area exists. If not, create it manually.\r\n            if hasattr(self.network_tab, '_create_functional_stats_area'):\r\n                if not hasattr(self.network_tab, 'functional_stats_area') or self.network_tab.functional_stats_area is None:\r\n                    print(\"  -> Creating functional stats area for overlay...\")\r\n                    self.network_tab._create_functional_stats_area()\r\n            \r\n            # Ensure the label has text so the widget has non-zero height!\r\n            if hasattr(self.network_tab, 'functional_stats_label') and not self.network_tab.functional_stats_label.text():\r\n                self.network_tab.functional_stats_label.setText(\"<b>🧠 Custom Brain Loaded</b><br>External Bindings Active\")\r\n                self.network_tab.functional_stats_label.show()\r\n\r\n            # Initialize overlay if missing\r\n            if hasattr(self.network_tab, 'functional_stats_area'):\r\n                if not hasattr(self.network_tab, 'binding_overlay') or self.network_tab.binding_overlay is None:\r\n                    try:\r\n                        from .brain_network_tab_banners import BindingOverlay\r\n                        self.network_tab.binding_overlay = BindingOverlay(self.network_tab, self.network_tab.functional_stats_area)\r\n                        print(\"  -> BindingOverlay initialized\")\r\n                    except ImportError as e:\r\n                        print(f\"  ! Could not import BindingOverlay: {e}\")\r\n\r\n            # Update the overlay with data\r\n            if hasattr(self.network_tab, 'binding_overlay') and self.network_tab.binding_overlay:\r\n                bindings = brain.get('output_bindings', [])\r\n                if bindings:\r\n                    print(f\"  -> Sending {len(bindings)} bindings to overlay\")\r\n                    self.network_tab.binding_overlay.update_bindings(bindings)\r\n\r\n            # 3. Finalize\r\n            _current_custom_brain = data\r\n            _current_custom_brain_name = Path(filepath).stem\r\n            _current_custom_brain_file = filepath\r\n            \r\n            self._update_worker()\r\n            \r\n            if hasattr(self.network_tab, 'update_metrics_display'):\r\n                self.network_tab.update_metrics_display()\r\n            \r\n            if self.brain_widget:\r\n                self.brain_widget.update()\r\n            \r\n            name = Path(filepath).stem\r\n            QtWidgets.QMessageBox.information(\r\n                self.network_tab, \"Loaded\",\r\n                f\"✅ {name}\\n\\nNeurons: {len(brain['positions'])}\\nConnections: {len(brain['weights'])}\\n\\n\"\r\n                f\"This brain will be saved with your game.\"\r\n            )\r\n            return True\r\n            \r\n        except Exception as e:\r\n            import traceback\r\n            print(f\"❌ Failed to load brain: {e}\")\r\n            traceback.print_exc()\r\n            QtWidgets.QMessageBox.critical(self.network_tab, \"Error\", f\"Failed to load:\\n{e}\")\r\n            return False\r\n    \r\n    def reset_to_default(self):\r\n        \"\"\"Reset to the default brain.\"\"\"\r\n        global _current_custom_brain, _current_custom_brain_name, _current_custom_brain_file\r\n        \r\n        bw = self.brain_widget\r\n        if not bw or not hasattr(bw, '_orig_backup'):\r\n            QtWidgets.QMessageBox.warning(\r\n                self.network_tab, \"Cannot Reset\",\r\n                \"No original brain backup available.\"\r\n            )\r\n            return\r\n        \r\n        backup = bw._orig_backup\r\n        \r\n        # Restore from backup\r\n        if hasattr(bw, 'neuron_positions'):\r\n            bw.neuron_positions.clear()\r\n            bw.neuron_positions.update(backup['positions'])\r\n        \r\n        if hasattr(bw, 'weights'):\r\n            bw.weights.clear()\r\n            bw.weights.update(backup['weights'])\r\n        \r\n        if hasattr(bw, 'state'):\r\n            # Only restore non-status neurons\r\n            status = {'is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction'}\r\n            saved_status = {k: v for k, v in bw.state.items() if k in status}\r\n            bw.state.clear()\r\n            bw.state.update(backup['state'])\r\n            bw.state.update(saved_status)\r\n        \r\n        if hasattr(bw, 'connections'):\r\n            bw.connections = list(bw.weights.keys())\r\n        \r\n        # Clear custom brain tracking\r\n        _current_custom_brain = None\r\n        _current_custom_brain_name = None\r\n        _current_custom_brain_file = None\r\n        \r\n        self._update_worker()\r\n        \r\n        if hasattr(self.network_tab, 'update_metrics_display'):\r\n            self.network_tab.update_metrics_display()\r\n        \r\n        bw.update()\r\n        \r\n        QtWidgets.QMessageBox.information(\r\n            self.network_tab, \"Reset\",\r\n            \"Brain reset to default configuration.\"\r\n        )\r\n    \r\n    def _parse(self, data: Dict) -> Optional[Dict]:\r\n        \"\"\"Parse brain file to standard format.\"\"\"\r\n        result = {'positions': {}, 'weights': {}, 'state': {}, 'excluded': set(), 'output_bindings': []}\r\n        \r\n        # Format 1: Dosidicus format (connections as dict with \"->\")\r\n        if 'neurons' in data and isinstance(data.get('connections'), dict):\r\n            for name, nd in data['neurons'].items():\r\n                pos = nd.get('position', [0, 0])\r\n                result['positions'][name] = tuple(pos) if isinstance(pos, list) else pos\r\n            for key, w in data.get('connections', {}).items():\r\n                if '->' in str(key):\r\n                    s, t = str(key).split('->')\r\n                    result['weights'][(s, t)] = w\r\n            result['state'] = data.get('state', {})\r\n            result['excluded'] = set(data.get('excluded_neurons', []))\r\n        \r\n        # Format 2: Designer format (connections as list)\r\n        elif 'neurons' in data and isinstance(data.get('connections'), list):\r\n            for name, nd in data['neurons'].items():\r\n                pos = nd.get('position', [0, 0])\r\n                result['positions'][name] = tuple(pos) if isinstance(pos, list) else pos\r\n                result['state'][name] = nd.get('activation', 50.0)\r\n            for c in data.get('connections', []):\r\n                if c.get('source') and c.get('target'):\r\n                    result['weights'][(c['source'], c['target'])] = c.get('weight', 0.1)\r\n            result['excluded'] = set(data.get('excluded_neurons', []))\r\n        \r\n        # Format 3: Layer format\r\n        elif 'layers' in data:\r\n            for i, layer in enumerate(data['layers']):\r\n                neurons = layer.get('neurons', [])\r\n                y = 80 + i * 150\r\n                for j, name in enumerate(neurons):\r\n                    x = 100 + (j + 1) * (700 // (len(neurons) + 1))\r\n                    result['positions'][name] = (x, y)\r\n                    result['state'][name] = 50.0\r\n            for c in data.get('custom_connections', []):\r\n                if c.get('source') and c.get('target'):\r\n                    result['weights'][(c['source'], c['target'])] = c.get('weight', 0.1)\r\n        else:\r\n            return None\r\n        \r\n        # Parse output bindings\r\n        result['output_bindings'] = data.get('output_bindings', [])\r\n        \r\n        # Defaults\r\n        for n in result['positions']:\r\n            if n not in result['state']:\r\n                result['state'][n] = 50.0\r\n        \r\n        status = {'is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction'}\r\n        result['excluded'].update(status)\r\n        \r\n        return result if result['positions'] else None\r\n    \r\n    def _apply(self, brain: dict):\r\n        \"\"\"Apply parsed brain data to the current brain widget.\"\"\"\r\n        bw = self.brain_widget\r\n        \r\n        # Apply positions\r\n        if 'positions' in brain:\r\n            if hasattr(bw, 'neuron_positions'):\r\n                bw.neuron_positions.clear()\r\n                bw.neuron_positions.update(brain['positions'])\r\n                \r\n        # --- FIX: Ensure mandatory sensors are present ---\r\n        # If the custom brain deleted core sensors, the game logic will break.\r\n        # We restore them using original positions (or default) so inputs still work.\r\n        MANDATORY_SENSORS = {\r\n            'can_see_food', 'external_stimulus', 'plant_proximity', \r\n            'is_eating', 'is_sleeping', 'is_sick', 'is_fleeing', 'is_startled', \r\n            'pursuing_food'\r\n        }\r\n        \r\n        restored_sensors = 0\r\n        if hasattr(bw, 'original_neuron_positions'):\r\n            for sensor in MANDATORY_SENSORS:\r\n                if sensor not in bw.neuron_positions:\r\n                    if sensor in bw.original_neuron_positions:\r\n                        bw.neuron_positions[sensor] = bw.original_neuron_positions[sensor]\r\n                        # Also ensure it has a state entry\r\n                        if hasattr(bw, 'state') and sensor not in bw.state:\r\n                            bw.state[sensor] = 0.0\r\n                        restored_sensors += 1\r\n        \r\n        if restored_sensors > 0:\r\n            print(f\"🔧 Restored {restored_sensors} mandatory sensor neurons missing from custom brain\")\r\n        \r\n        # Apply weights/connections\r\n        if 'weights' in brain:\r\n            if hasattr(bw, 'weights'):\r\n                bw.weights.clear()\r\n                bw.weights.update(brain['weights'])\r\n        \r\n        # Apply state if present (neuron activations)\r\n        if 'state' in brain:\r\n            if hasattr(bw, 'state'):\r\n                bw.state.clear()\r\n                bw.state.update(brain['state'])\r\n\r\n        # Populate bw.neurons for synchronous simulation fallback\r\n        if hasattr(bw, 'neurons'):\r\n            bw.neurons = {}\r\n            for name in bw.neuron_positions:\r\n                # Check if this is a binary/sensor neuron\r\n                is_sensor = hasattr(bw, 'is_binary_neuron') and bw.is_binary_neuron(name)\r\n                \r\n                # FIX: Sensors get 1.0 decay so they persist. 0.0 decay zeroes them out instantly.\r\n                bw.neurons[name] = {\r\n                    'activation': bw.state.get(name, 50.0),\r\n                    'decay': 1.0 if is_sensor else 0.9, \r\n                    'noise': 0.0\r\n                }\r\n        \r\n        # Apply excluded neurons\r\n        if 'excluded' in brain:\r\n            if hasattr(bw, 'excluded_neurons'):\r\n                bw.excluded_neurons = list(brain['excluded'])\r\n        \r\n        # === OUTPUT BINDINGS (Neuron → Behavior) ===\r\n        output_bindings_data = brain.get('output_bindings', [])\r\n        \r\n        if output_bindings_data:\r\n            # Find the NeuronOutputMonitor\r\n            monitor = None\r\n            logic = getattr(bw, 'tamagotchi_logic', None)\r\n            if logic and hasattr(logic, 'neuron_output_monitor'):\r\n                monitor = logic.neuron_output_monitor\r\n            # Fallback if attached differently\r\n            elif hasattr(bw, 'parent') and hasattr(bw.parent(), 'tamagotchi_logic'):\r\n                monitor = bw.parent().tamagotchi_logic.neuron_output_monitor\r\n            \r\n            if monitor:\r\n                # Load the bindings\r\n                monitor.bindings = []  # Clear old ones\r\n                loaded_count = 0\r\n                for bind_data in output_bindings_data:\r\n                    try:\r\n                        binding = NeuronOutputBinding.from_dict(bind_data)\r\n                        monitor.bindings.append(binding)\r\n                        loaded_count += 1\r\n                    except Exception as e:\r\n                        print(f\"[BrainLoader] Failed to load binding: {e}\")\r\n                \r\n                print(f\"[NeuronOutputMonitor] Loaded {loaded_count} neuron output bindings\")\r\n                \r\n                # Use the actual monitor() method to trigger initial thresholds\r\n                if hasattr(bw, 'state') and bw.state:\r\n                    # Convert state values to float where needed\r\n                    activations = {}\r\n                    for name, value in bw.state.items():\r\n                        try:\r\n                            activations[name] = float(value)\r\n                        except (TypeError, ValueError):\r\n                            activations[name] = 100.0 if value else 0.0\r\n                    \r\n                    # Trigger the monitor with current activations\r\n                    monitor.monitor(activations)\r\n                    print(\"[NeuronOutputMonitor] Initial neuron activations processed via monitor()\")\r\n                else:\r\n                    print(\"[NeuronOutputMonitor] ⚠️ No state data available for initial trigger\")\r\n            else:\r\n                print(\"[NeuronOutputMonitor] ⚠️ Monitor not found — bindings saved but not activated\")\r\n        \r\n        # Keep a copy on the widget for saving/exporting later\r\n        bw.output_bindings = output_bindings_data\r\n        \r\n        # Update related attributes safely\r\n        if hasattr(bw, 'connections'):\r\n            bw.connections = list(brain['weights'].keys())\r\n        if hasattr(bw, 'visible_neurons'):\r\n            bw.visible_neurons = set(bw.neuron_positions.keys()) if hasattr(bw, 'neuron_positions') else set()\r\n        if hasattr(bw, 'original_neuron_positions') and hasattr(bw, 'neuron_positions'):\r\n            bw.original_neuron_positions = dict(bw.neuron_positions)\r\n        if hasattr(bw, 'original_neurons') and hasattr(bw, 'neuron_positions'):\r\n            status = {'is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction'}\r\n            bw.original_neurons = [n for n in bw.neuron_positions if n not in status]\r\n        if hasattr(bw, 'communication_events') and hasattr(bw, 'neuron_positions'):\r\n            bw.communication_events = {n: 0 for n in bw.neuron_positions}\r\n    \r\n    def _update_worker(self):\r\n        \"\"\"Update BrainWorker cache if available.\"\"\"\r\n        try:\r\n            worker = None\r\n            parent = self.network_tab.parent() if self.network_tab else None\r\n            while parent:\r\n                if hasattr(parent, 'brain_worker'):\r\n                    worker = parent.brain_worker\r\n                    break\r\n                parent = parent.parent()\r\n            \r\n            if not worker:\r\n                print(\"⚠️ BrainWorker not found - skipping cache update\")\r\n                return\r\n            \r\n            # Try to update worker cache - different versions may have different APIs\r\n            bw = self.brain_widget\r\n            \r\n            if hasattr(worker, 'update_cache') and callable(getattr(worker, 'update_cache')):\r\n                worker.update_cache(\r\n                    state=bw.state,\r\n                    weights=bw.weights,\r\n                    neuron_positions=bw.neuron_positions,\r\n                    config=getattr(bw, 'config', None),\r\n                    excluded_neurons=getattr(bw, 'excluded_neurons', []),\r\n                    learning_rate=getattr(bw, 'learning_rate', 0.1),\r\n                    new_neurons=bw.neurogenesis_data.get('new_neurons', []) if hasattr(bw, 'neurogenesis_data') else []\r\n                )\r\n                print(\"✅ BrainWorker cache updated\")\r\n            elif hasattr(worker, 'set_brain_data') and callable(getattr(worker, 'set_brain_data')):\r\n                worker.set_brain_data(bw.state, bw.weights, bw.neuron_positions)\r\n                print(\"✅ BrainWorker data updated\")\r\n            else:\r\n                # Just update the worker's direct references if it has them\r\n                if hasattr(worker, 'state'):\r\n                    worker.state = bw.state\r\n                if hasattr(worker, 'weights'):\r\n                    worker.weights = bw.weights\r\n                if hasattr(worker, 'neuron_positions'):\r\n                    worker.neuron_positions = bw.neuron_positions\r\n                print(\"✅ BrainWorker references updated\")\r\n                \r\n        except Exception as e:\r\n            print(f\"⚠️ Could not update BrainWorker: {e}\")\r\n            # Non-fatal - brain still loads, just learning might need restart\r\n\r\n    def reset_positions_to_default(self):\r\n        \"\"\"Reset only neuron positions to defaults, preserving network structure\"\"\"\r\n        bw = self.brain_widget\r\n        if not bw or not hasattr(bw, '_orig_backup'):\r\n            raise ValueError(\"No original brain backup available\")\r\n        \r\n        # Reset positions only\r\n        for neuron in list(bw.neuron_positions.keys()):\r\n            if neuron in bw._orig_backup['positions']:\r\n                bw.neuron_positions[neuron] = bw._orig_backup['positions'][neuron]\r\n        \r\n        # Update displays\r\n        if hasattr(self.network_tab, 'update_metrics_display'):\r\n            self.network_tab.update_metrics_display()\r\n        \r\n        # Repaint\r\n        bw.update()\r\n        \r\n        print(\"✅ Neuron positions reset to defaults\")\r\n\r\n\r\n# =============================================================================\r\n# BRAIN SELECTION DIALOG\r\n# =============================================================================\r\n\r\nclass BrainSelectDialog(QtWidgets.QDialog):\r\n    \"\"\"Simple dialog to select a brain file.\"\"\"\r\n    \r\n    def __init__(self, brains_folder: Path, parent=None):\r\n        super().__init__(parent)\r\n        self.brains_folder = brains_folder\r\n        self.selected_file = None\r\n        self.setWindowTitle(\"Load Custom Brain\")\r\n        self.setMinimumSize(450, 350)\r\n        self._setup_ui()\r\n        self._refresh()\r\n    \r\n    def _setup_ui(self):\r\n        layout = QtWidgets.QVBoxLayout(self)\r\n        \r\n        # List\r\n        self.list = QtWidgets.QListWidget()\r\n        self.list.setStyleSheet(\"font-size: 11pt;\")\r\n        self.list.itemDoubleClicked.connect(self.accept)\r\n        self.list.currentRowChanged.connect(self._on_select)\r\n        layout.addWidget(self.list)\r\n        \r\n        # Info\r\n        self.info = QtWidgets.QLabel(\"Select a brain file\")\r\n        self.info.setStyleSheet(\"background: #f0f0f0; padding: 8px; border-radius: 4px;\")\r\n        self.info.setWordWrap(True)\r\n        layout.addWidget(self.info)\r\n        \r\n        # Buttons\r\n        btns = QtWidgets.QHBoxLayout()\r\n        \r\n        browse = QtWidgets.QPushButton(\"Browse...\")\r\n        browse.clicked.connect(self._browse)\r\n        btns.addWidget(browse)\r\n        \r\n        folder = QtWidgets.QPushButton(\"Open Folder\")\r\n        folder.clicked.connect(self._open_folder)\r\n        btns.addWidget(folder)\r\n        \r\n        btns.addStretch()\r\n        \r\n        cancel = QtWidgets.QPushButton(\"Cancel\")\r\n        cancel.clicked.connect(self.reject)\r\n        btns.addWidget(cancel)\r\n        \r\n        load = QtWidgets.QPushButton(\"Load\")\r\n        load.setStyleSheet(\"background: #5c6bc0; color: white; font-weight: bold;\")\r\n        load.clicked.connect(self.accept)\r\n        btns.addWidget(load)\r\n        \r\n        layout.addLayout(btns)\r\n        \r\n        # Hint\r\n        hint = QtWidgets.QLabel(f\"Folder: {self.brains_folder}\")\r\n        hint.setStyleSheet(\"color: #888; font-size: 9pt;\")\r\n        layout.addWidget(hint)\r\n    \r\n    def _refresh(self):\r\n        self.list.clear()\r\n        files = list(self.brains_folder.glob(\"*.json\")) if self.brains_folder.exists() else []\r\n        \r\n        if not files:\r\n            item = QtWidgets.QListWidgetItem(\"(No brain files - click Browse)\")\r\n            item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable)\r\n            self.list.addItem(item)\r\n            return\r\n        \r\n        for f in sorted(files):\r\n            item = QtWidgets.QListWidgetItem(f\"🧠 {f.stem}\")\r\n            item.setData(QtCore.Qt.UserRole, str(f))\r\n            self.list.addItem(item)\r\n    \r\n    def _on_select(self, row):\r\n        item = self.list.item(row)\r\n        if not item:\r\n            return\r\n        path = item.data(QtCore.Qt.UserRole)\r\n        if not path:\r\n            return\r\n        \r\n        try:\r\n            with open(path) as f:\r\n                data = json.load(f)\r\n            neurons = len(data.get('neurons', {}))\r\n            conns = data.get('connections', {})\r\n            conn_count = len(conns) if isinstance(conns, (dict, list)) else 0\r\n            meta = data.get('metadata', {})\r\n            name = meta.get('name', Path(path).stem)\r\n            desc = meta.get('description', '')\r\n            self.info.setText(f\"<b>{name}</b><br>Neurons: {neurons} | Connections: {conn_count}<br>{desc}\")\r\n        except:\r\n            self.info.setText(\"Could not read file\")\r\n    \r\n    def _browse(self):\r\n        path, _ = QtWidgets.QFileDialog.getOpenFileName(\r\n            self, \"Select Brain\", str(self.brains_folder), \"JSON (*.json)\"\r\n        )\r\n        if path:\r\n            self.selected_file = path\r\n            self.accept()\r\n    \r\n    def _open_folder(self):\r\n        import subprocess, sys\r\n        folder = str(self.brains_folder)\r\n        if sys.platform == 'win32':\r\n            subprocess.run(['explorer', folder])\r\n        elif sys.platform == 'darwin':\r\n            subprocess.run(['open', folder])\r\n        else:\r\n            subprocess.run(['xdg-open', folder])\r\n    \r\n    def accept(self):\r\n        if not self.selected_file:\r\n            item = self.list.currentItem()\r\n            if item:\r\n                self.selected_file = item.data(QtCore.Qt.UserRole)\r\n        if self.selected_file:\r\n            super().accept()"
  },
  {
    "path": "src/decision_engine.py",
    "content": "# DECISION ENGINE - https://github.com/ViciousSquid/Dosidicus\r\n# exploration of emergent behavioural complexity via dynamic, biologically-inspired neural architecture rather than a static state machine. \r\n# Version 4.0 — Fully Neural-Driven Decision Making | December 2025\r\n\r\nimport random\r\nimport math\r\nfrom .personality import Personality\r\n\r\n\r\nclass DecisionEngine:\r\n    \"\"\"\r\n    Neural-first decision engine.\r\n    All perception flows through BrainNeuronHooks → no manual vision checks.\r\n    Behaviour emerges purely from the current brain state + memory + personality.\r\n    \"\"\"\r\n\r\n    def __init__(self, squid):\r\n        self.squid = squid\r\n        self.last_decision_data = {}\r\n\r\n    def get_decision_data(self):\r\n        \"\"\"Return last decision trace for visualization in Brain Tool → Decisions tab\"\"\"\r\n        return self.last_decision_data.copy()\r\n\r\n    def make_decision(self):\r\n        logic = self.squid.tamagotchi_logic\r\n\r\n        # =================================================================\r\n        # 1. BUILD FULL BRAIN STATE FROM HOOKS\r\n        # =================================================================\r\n        decision_data = {\r\n            'inputs': {},\r\n            'brain_state': {},\r\n            'base_weights': {},\r\n            'memory_influences': {},\r\n            'urgency_multipliers': {},\r\n            'personality_modifiers': {},\r\n            'adjusted_weights': {},\r\n            'final_decision': '',\r\n            'confidence': 0.0,\r\n            'personality': self.squid.personality.value if isinstance(self.squid.personality, Personality) else str(self.squid.personality),\r\n            'timestamp': time.time()\r\n        }\r\n\r\n        # --- Get dynamic perceptual inputs via hooks ---\r\n        if not hasattr(logic, 'brain_hooks'):\r\n            perceptual_inputs = {}\r\n        else:\r\n            try:\r\n                perceptual_inputs = logic.brain_hooks.get_input_neuron_values()\r\n                logic.brain_hooks.update_decay()  # Critical: decay temporal sensors\r\n            except Exception as e:\r\n                print(f\"[DecisionEngine] Hook error: {e}\")\r\n                perceptual_inputs = {}\r\n\r\n        decision_data['inputs'] = perceptual_inputs\r\n\r\n        # --- Get full current brain state (core + learned + input neurons) ---\r\n        try:\r\n            brain_state = logic.brain_window.brain_widget.state.copy()\r\n        except:\r\n            brain_state = {}\r\n\r\n        # Ensure perceptual inputs are present even if brain_widget hasn't updated yet\r\n        brain_state.update(perceptual_inputs)\r\n        decision_data['brain_state'] = brain_state\r\n\r\n        # =================================================================\r\n        # 2. MEMORY INFLUENCE\r\n        # =================================================================\r\n        active_memories = self.squid.memory_manager.get_active_memories_data(6)\r\n        memory_mod = {\r\n            \"eating\": 1.0,\r\n            \"playing\": 1.0,\r\n            \"exploring\": 1.0,\r\n            \"approaching_plant\": 1.0,\r\n            \"throwing\": 1.0,\r\n        }\r\n\r\n        for mem in active_memories:\r\n            effect = sum(v for v in mem.get('raw_value', {}).values() if isinstance(v, (int, float))) if isinstance(mem.get('raw_value'), dict) else 0\r\n\r\n            if mem['category'] == 'food' and effect > 0:\r\n                memory_mod['eating'] *= 1.25\r\n            if 'rock' in mem['key'] and effect > 0:\r\n                memory_mod['playing'] *= 1.2\r\n                memory_mod['throwing'] *= 1.15\r\n            if 'poop' in mem['key'] and effect > 0:\r\n                memory_mod['playing'] *= 1.1\r\n            if 'plant' in mem['key'] and effect > 0:\r\n                memory_mod['approaching_plant'] *= 1.3\r\n            if 'startled' in mem['key']:\r\n                memory_mod['exploring'] *= 0.7\r\n                memory_mod['approaching_plant'] *= 1.4\r\n\r\n        decision_data['memory_influences'] = memory_mod\r\n\r\n        # =================================================================\r\n        # 3. URGENCY (NON-LINEAR PHYSIOLOGICAL DRIVE)\r\n        # =================================================================\r\n        hunger_level = brain_state.get('hunger', 50)\r\n        sleepiness = brain_state.get('sleepiness', 50)\r\n\r\n        urgency = {\r\n            \"eating\": math.pow(1.6, hunger_level / 25),\r\n            \"sleeping\": math.pow(1.7, sleepiness / 25),\r\n        }\r\n        decision_data['urgency_multipliers'] = urgency\r\n\r\n        # Immediate overrides\r\n        if sleepiness >= 95:\r\n            self.squid.go_to_sleep()\r\n            decision_data['final_decision'] = \"exhausted\"\r\n            decision_data['confidence'] = 1.0\r\n            self.last_decision_data = decision_data\r\n            return \"exhausted\"\r\n\r\n        if self.squid.is_sleeping:\r\n            decision_data['final_decision'] = \"sleeping peacefully\"\r\n            decision_data['confidence'] = 1.0\r\n            self.last_decision_data = decision_data\r\n            return \"sleeping peacefully\"\r\n\r\n        if brain_state.get('external_stimulus', 0) > 90:\r\n            decision_data['final_decision'] = \"startled!\"\r\n            decision_data['confidence'] = 1.0\r\n            self.last_decision_data = decision_data\r\n            return \"startled!\"\r\n\r\n        # =================================================================\r\n        # 4. BUILD DECISION WEIGHTS FROM BRAIN STATE\r\n        # =================================================================\r\n        weights = {}\r\n\r\n        # Exploration\r\n        threat = brain_state.get('threat_level', brain_state.get('anxiety', 50))\r\n        external = brain_state.get('external_stimulus', 0)\r\n        weights[\"exploring\"] = (\r\n            brain_state.get(\"curiosity\", 50) *\r\n            max(0.1, 1.0 - threat / 140) *\r\n            (0.2 if external > 80 else 1.0)\r\n        )\r\n\r\n        # Eating\r\n        can_see_food = brain_state.get('can_see_food', 0)\r\n        weights[\"eating\"] = (\r\n            hunger_level *\r\n            (3.0 if can_see_food > 80 else 0.3) *\r\n            urgency[\"eating\"]\r\n        )\r\n\r\n        # Plant seeking (comfort)\r\n        near_plant = brain_state.get('plant_proximity', 0) > 40\r\n        weights[\"approaching_plant\"] = (\r\n            (brain_state.get(\"anxiety\", 50) / 40) *\r\n            (3.0 if near_plant else 0.5) *\r\n            (4.0 if self.squid.personality == Personality.TIMID else 1.8)\r\n        )\r\n\r\n        # Play / Object interaction\r\n        carrying = self.squid.carrying_rock or self.squid.carrying_poop\r\n        weights[\"playing\"] = (\r\n            brain_state.get(\"satisfaction\", 50) *\r\n            brain_state.get(\"curiosity\", 50) / 50 *\r\n            (2.2 if carrying else 1.0) *\r\n            (0.4 if brain_state.get('is_sick', 0) > 50 else 1.0)\r\n        )\r\n\r\n        # Throwing (only if carrying)\r\n        weights[\"throwing\"] = (\r\n            brain_state.get(\"satisfaction\", 50) * 1.3\r\n            if carrying else 0\r\n        )\r\n\r\n        # Sleeping (non-exhausted)\r\n        weights[\"sleeping\"] = sleepiness * urgency[\"sleeping\"] * 0.6\r\n\r\n        # Fleeing from threat\r\n        if threat > 75 or external > 85:\r\n            weights[\"fleeing\"] = threat * 1.8\r\n\r\n        decision_data['base_weights'] = weights.copy()\r\n\r\n        # =================================================================\r\n        # 5. APPLY MEMORY + PERSONALITY MODIFIERS\r\n        # =================================================================\r\n        for action, mod in memory_mod.items():\r\n            if action in weights:\r\n                weights[action] *= mod\r\n\r\n        # Apply personality\r\n        if self.squid.personality == Personality.ADVENTUROUS:\r\n            weights[\"exploring\"] *= 1.4\r\n            weights[\"playing\"] *= 1.3\r\n        elif self.squid.personality == Personality.TIMID:\r\n            weights[\"exploring\"] *= 0.6\r\n            weights[\"approaching_plant\"] *= 1.5\r\n        elif self.squid.personality == Personality.GREEDY:\r\n            weights[\"eating\"] *= 1.6\r\n        elif self.squid.personality == Personality.LAZY:\r\n            weights[\"playing\"] *= 0.5\r\n            weights[\"exploring\"] *= 0.7\r\n        elif self.squid.personality == Personality.ENERGETIC:\r\n            weights[\"playing\"] *= 1.5\r\n            weights[\"exploring\"] *= 1.2\r\n\r\n        # Anxiety amplifies comfort-seeking\r\n        if brain_state.get(\"anxiety\", 50) > 60:\r\n            weights[\"approaching_plant\"] *= 1.8 + (brain_state.get(\"anxiety\", 0) - 60) / 80\r\n\r\n        decision_data['personality_modifiers'] = {k: v/weights.get(k,1) for k,v in weights.items() if k in weights}\r\n\r\n        # Add randomness\r\n        for k in weights:\r\n            weights[k] *= random.uniform(0.88, 1.12)\r\n\r\n        decision_data['adjusted_weights'] = weights.copy()\r\n\r\n        # =================================================================\r\n        # 6. SELECT AND EXECUTE DECISION\r\n        # =================================================================\r\n        if not any(weights.values()):\r\n            final = \"exploring\"\r\n        else:\r\n            final = max(weights, key=weights.get)\r\n\r\n        # Confidence calculation\r\n        sorted_w = sorted(weights.values(), reverse=True)\r\n        confidence = 1.0 if len(sorted_w) < 2 else (sorted_w[0] - sorted_w[1]) / sorted_w[0]\r\n\r\n        decision_data['final_decision'] = final\r\n        decision_data['confidence'] = confidence\r\n        self.last_decision_data = decision_data\r\n\r\n        result = self._execute_neural_decision(final, brain_state)\r\n        self.last_decision_data['final_decision'] = result  # actual outcome\r\n        return result\r\n\r\n    def _execute_neural_decision(self, decision: str, brain_state: dict):\r\n        \"\"\"Execute decision based purely on neural signals — no redundant scanning\"\"\"\r\n        s = self.squid\r\n\r\n        if decision == \"eating\" and brain_state.get('can_see_food', 0) > 70:\r\n            # Find closest food using existing logic method\r\n            food = s.tamagotchi_logic.food_items\r\n            if food:\r\n                closest = min(food, key=lambda f: s.distance_to(f.pos().x(), f.pos().y()))\r\n                s.move_towards(closest.pos().x(), closest.pos().y())\r\n                dist = s.distance_to(closest.pos().x(), closest.pos().y())\r\n                if dist < 60:\r\n                    return \"eating\"\r\n                elif dist < 120:\r\n                    return \"approaching food\"\r\n                else:\r\n                    return \"eyeing food\"\r\n\r\n        elif decision == \"approaching_plant\" and brain_state.get('plant_proximity', 0) > 30:\r\n            # Use decoration cache or scene scan fallback\r\n            plants = [item for item in s.tamagotchi_logic.user_interface.scene.items()\r\n                      if getattr(item, 'category', '') == 'plant']\r\n            if plants:\r\n                closest = min(plants, key=lambda p: s.distance_to(p.sceneBoundingRect().center().x(),\r\n                                                                 p.sceneBoundingRect().center().y()))\r\n                s.move_towards(closest.sceneBoundingRect().center().x(),\r\n                               closest.sceneBoundingRect().center().y())\r\n                return \"seeking comfort in plant\"\r\n\r\n        elif decision in (\"playing\", \"throwing\"):\r\n            if s.carrying_rock:\r\n                if s.throw_rock(random.choice([\"left\", \"right\"])):\r\n                    return \"playfully tossing rock\"\r\n            elif s.carrying_poop:\r\n                if s.throw_poop(random.choice([\"left\", \"right\"])):\r\n                    return \"flinging poop playfully\"\r\n            # Approach nearest rock/poop if visible via brain\r\n            elif brain_state.get('can_see_food', 0) == 0:  # crude proxy, but better than nothing\r\n                targets = [item for item in s.tamagotchi_logic.user_interface.scene.items()\r\n                          if getattr(item, 'category', '') in ('rock', 'poop')]\r\n                if targets:\r\n                    closest = min(targets, key=lambda t: s.distance_to(t.sceneBoundingRect().center().x(),\r\n                                                                     t.sceneBoundingRect().center().y()))\r\n                    s.move_towards(closest.sceneBoundingRect().center().x(),\r\n                                   closest.sceneBoundingRect().center().y())\r\n                    return \"seeking toy\"\r\n\r\n        elif decision == \"sleeping\":\r\n            s.go_to_sleep()\r\n            return \"settling down to sleep\"\r\n\r\n        elif decision == \"fleeing\" or brain_state.get('external_stimulus', 0) > 85:\r\n            s.flee_from_center()\r\n            return \"fleeing!\"\r\n\r\n        # Default: explore with personality flavor\r\n        flavors = {\r\n            Personality.TIMID: [\"cautiously peeking\", \"nervously watching\"],\r\n            Personality.ADVENTUROUS: [\"boldly exploring\", \"seeking adventure\"],\r\n            Personality.GREEDY: [\"hunting for food\", \"scouting\"],\r\n            Personality.LAZY: [\"lounging\", \"drifting lazily\"],\r\n            Personality.ENERGETIC: [\"zooming around\", \"bouncing energetically\"],\r\n            Personality.STUBBORN: [\"patrolling territory\", \"standing ground\"],\r\n        }.get(s.personality, [\"wandering\", \"exploring curiously\"])\r\n\r\n        style = random.choice(flavors)\r\n        if \"zoom\" in style or \"bounc\" in style:\r\n            s.move_erratically()\r\n        elif \"loung\" in style or \"drift\" in style:\r\n            s.move_slowly()\r\n        else:\r\n            s.move_randomly()\r\n\r\n        return style\r\n"
  },
  {
    "path": "src/decoration_stats.json",
    "content": "{\r\n  \"plant01.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -2,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant02.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant03.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -2,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant04.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant05.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant06.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"category\": \"plant\",\r\n    \"anxiety\": -2\r\n  },\r\n  \"plant07.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant08.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant09.png\": {\r\n    \"happiness\": 1,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -2,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant10.png\": {\r\n    \"happiness\": 1,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant11.png\": {\r\n    \"happiness\": 1,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"plant12.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -5,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"sml_plant01.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"sml_plant02.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -5,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"rock03.png\": {\r\n    \"happiness\": 4,\r\n    \"satisfaction\": 4,\r\n    \"category\": \"rock\"\r\n  },\r\n  \"rock01.png\": {\r\n    \"happiness\": 4,\r\n    \"satisfaction\": 4,\r\n    \"category\": \"rock\"\r\n  },\r\n  \"rock02.png\": {\r\n    \"happiness\": 4,\r\n    \"satisfaction\": 3,\r\n    \"category\": \"rock\"\r\n  },\r\n  \"Znewplant01.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant02.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant03.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"anxiety\": -2,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant04.png\": {\r\n    \"happiness\": 1,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -3,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant05.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -5,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant06.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 1,\r\n    \"category\": \"plant\",\r\n    \"anxiety\": -2\r\n  },\r\n  \"Znewplant07.png\": {\r\n    \"happiness\": 1,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -4,\r\n    \"category\": \"plant\"\r\n  },\r\n  \"Znewplant08.png\": {\r\n    \"happiness\": 2,\r\n    \"cleanliness\": 2,\r\n    \"anxiety\": -5,\r\n    \"category\": \"plant\"\r\n  }\r\n}"
  },
  {
    "path": "src/designer_canvas.py",
    "content": "\"\"\"\r\nCanvas implementation for Brain Designer.\r\nHandles visual representation and interaction.\r\n\"\"\"\r\n\r\nimport math\r\nimport json\r\nimport os\r\nfrom PyQt5.QtWidgets import (\r\n    QGraphicsItem, QGraphicsView, QGraphicsScene, QGraphicsEllipseItem, \r\n    QGraphicsLineItem, QGraphicsTextItem, QGraphicsRectItem, QStyle, QToolTip,\r\n    QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QPushButton,\r\n    QCheckBox, QMessageBox\r\n)\r\nfrom PyQt5.QtCore import Qt, QPointF, QRectF, QLineF, pyqtSignal, QTimer\r\nfrom PyQt5.QtGui import (\r\n    QPainter, QPen, QBrush, QColor, QFont, QFontMetrics, QPainterPath, \r\n    QRadialGradient, QTransform, QCursor, QPainterPathStroker, QPolygonF\r\n)\r\n\r\nfrom .designer_core import BrainDesign\r\nfrom .designer_constants import (\r\n    NeuronType, CORE_NEURON_RING_COLOR, INPUT_SENSOR_RING_COLOR, \r\n    CUSTOM_NEURON_RING_COLOR, PROTECTED_RING_WIDTH, NORMAL_RING_WIDTH, \r\n    DEFAULT_LAYER_HEIGHT\r\n)\r\n\r\n\r\nclass SmartConnectionItem(QGraphicsItem):\r\n    \"\"\"Visual representation of a neural connection with organic growth animation.\"\"\"\r\n    \r\n    def __init__(self, source_pos, target_pos, weight=0.5, source_name=\"\", target_name=\"\", parent=None):\r\n        super().__init__(parent)\r\n        self.source_pos = source_pos\r\n        self.target_pos = target_pos\r\n        self.weight = weight\r\n        self.source_name = source_name\r\n        self.target_name = target_name\r\n        \r\n        # Visual states\r\n        self.is_selected = False\r\n        self.is_hovered = False\r\n        self.is_dying = False  # If True, connection is retreating/fading out\r\n        \r\n        # Organic Animation States\r\n        self.growth = 0.0      # 0.0 = at source, 1.0 = full connection\r\n        self.opacity = 0.0     # 0.0 = invisible, 1.0 = fully visible\r\n        \r\n        # Randomized \"Personality\" for this vine\r\n        self.growth_speed = 0.02 + (hash(str(source_pos) + str(target_pos)) % 50) * 0.0006\r\n        self.growth = -0.1 # Start slightly delayed\r\n        \r\n        # Pulse animation (standard)\r\n        self.pulse_phase = 0.0\r\n        \r\n        self.setAcceptHoverEvents(True)\r\n        self.setZValue(-5) \r\n        self.hit_thickness = 25 \r\n\r\n    def boundingRect(self):\r\n        extra = 30\r\n        # protect against null-length line\r\n        if self.source_pos == self.target_pos:\r\n            return QRectF(self.source_pos.x() - extra, self.source_pos.y() - extra,\r\n                        2 * extra, 2 * extra)\r\n        return QRectF(self.source_pos, self.target_pos).normalized() \\\r\n            .adjusted(-extra, -extra, extra, extra)\r\n\r\n    def shape(self):\r\n        \"\"\"Custom hit area for connection lines.\"\"\"\r\n        path = QPainterPath()\r\n        path.moveTo(self.source_pos)\r\n        path.lineTo(self.target_pos)\r\n        \r\n        # Create a stroker to widen the hit area\r\n        stroker = QPainterPathStroker()\r\n        stroker.setWidth(self.hit_thickness)\r\n        stroker.setCapStyle(Qt.RoundCap)\r\n        return stroker.createStroke(path)\r\n    \r\n    def hoverEnterEvent(self, event):\r\n        self.is_hovered = True\r\n        self.update()\r\n        super().hoverEnterEvent(event)\r\n\r\n    def hoverLeaveEvent(self, event):\r\n        self.is_hovered = False\r\n        self.update()\r\n        super().hoverLeaveEvent(event)\r\n    \r\n    def advance_animation(self):\r\n        \"\"\"Calculates one frame of growth/retreat logic.\"\"\"\r\n        \r\n        # 1. Handle Dying (Retreating)\r\n        if self.is_dying:\r\n            self.growth -= self.growth_speed * 1.5\r\n            self.opacity -= 0.05\r\n            if self.opacity < 0: self.opacity = 0\r\n            return self.opacity <= 0\r\n\r\n        # 2. Handle Living (Growing)\r\n        if self.growth < 1.0:\r\n            self.growth += self.growth_speed\r\n        if self.growth > 1.0: self.growth = 1.0\r\n        \r\n        # Fade in\r\n        if self.growth > 0 and self.opacity < 1.0:\r\n            self.opacity += 0.05\r\n            if self.opacity > 1.0: self.opacity = 1.0\r\n            \r\n        # 3. Pulse Logic\r\n        if self.growth > 0.8:\r\n            current_speed = 0.02 + (abs(self.weight) * 0.06)\r\n            self.pulse_phase += current_speed\r\n            if self.pulse_phase > 1.0: self.pulse_phase = 0.0\r\n\r\n        self.update()\r\n        return False # Still alive\r\n\r\n    def paint(self, painter, option, widget):\r\n        painter.save()\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n\r\n        # Draw hover glow (subtle blue when hovered but not selected)\r\n        if self.is_hovered and not self.is_selected:\r\n            pen_width = 12\r\n            glow_color = QColor(100, 200, 255, 100)\r\n            painter.setPen(QPen(glow_color, pen_width, Qt.SolidLine, Qt.RoundCap))\r\n            painter.drawLine(self.source_pos, self.target_pos)\r\n\r\n        # Draw the actual connection line\r\n        # Determine color based on weight (Green=Excitatory, Red=Inhibitory)\r\n        base_color = QColor(50, 205, 50) if self.weight >= 0 else QColor(220, 50, 50)\r\n        \r\n        # Adjust alpha by opacity (growth animation)\r\n        base_color.setAlpha(int(255 * self.opacity))\r\n        \r\n        # Determine thickness (magnitude of weight)\r\n        thickness = 2 + abs(self.weight) * 6\r\n        \r\n        # Pulse effect for thickness\r\n        pulse = math.sin(self.pulse_phase * math.pi * 2) * 0.5 + 0.5\r\n        thickness += pulse * 1.5\r\n\r\n        # Draw selection highlight (yellow glow when selected)\r\n        if self.is_selected:\r\n            painter.setPen(QPen(QColor(255, 215, 0), thickness + 4, Qt.SolidLine, Qt.RoundCap))\r\n            painter.drawLine(self.source_pos, self.target_pos)\r\n\r\n        painter.setPen(QPen(base_color, thickness, Qt.SolidLine, Qt.RoundCap))\r\n        painter.drawLine(self.source_pos, self.target_pos)\r\n        \r\n        painter.restore()\r\n\r\n\r\nclass ConnectorNeuronItem(QGraphicsItem):\r\n    \"\"\"\r\n    Connector Neuron: Black hexagon with a white 'c' inside.\r\n    Matches brain_widget.py's draw_hexagon_neuron style.\r\n    \"\"\"\r\n    \r\n    def __init__(self, x, y, radius, name, parent=None):\r\n        super().__init__(parent)\r\n        self.name = name\r\n        self.radius = radius\r\n        self.is_selected = False\r\n        self.is_hovered = False\r\n        self.neuron_type = NeuronType.CONNECTOR\r\n        \r\n        self.setPos(x, y)\r\n        self.setAcceptHoverEvents(True)\r\n        self.setData(0, ('neuron', name))\r\n        self.setZValue(2)\r\n    \r\n    def boundingRect(self):\r\n        extra = 15\r\n        return QRectF(-self.radius - extra, -self.radius - extra,\r\n                      (self.radius + extra) * 2, (self.radius + extra) * 2)\r\n    \r\n    def shape(self):\r\n        \"\"\"Custom hitbox for Hexagon.\"\"\"\r\n        path = QPainterPath()\r\n        sides = 6\r\n        polygon = QPolygonF()\r\n        angle_step = 360.0 / sides\r\n        for i in range(sides):\r\n            angle = math.radians(i * angle_step - 90)\r\n            polygon.append(QPointF(self.radius * math.cos(angle), \r\n                                   self.radius * math.sin(angle)))\r\n        path.addPolygon(polygon)\r\n        path.closeSubpath()\r\n        return path\r\n    \r\n    def hoverEnterEvent(self, event):\r\n        self.is_hovered = True\r\n        self.update()\r\n        super().hoverEnterEvent(event)\r\n    \r\n    def hoverLeaveEvent(self, event):\r\n        self.is_hovered = False\r\n        self.update()\r\n        super().hoverLeaveEvent(event)\r\n    \r\n    def paint(self, painter, option, widget):\r\n        painter.save()\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        # Draw hover glow\r\n        if self.is_hovered and not self.is_selected:\r\n            grad = QRadialGradient(0, 0, self.radius + 12)\r\n            grad.setColorAt(0.5, QColor(100, 200, 255, 100))\r\n            grad.setColorAt(1.0, QColor(100, 200, 255, 0))\r\n            painter.setBrush(QBrush(grad))\r\n            painter.setPen(Qt.NoPen)\r\n            painter.drawEllipse(QPointF(0, 0), self.radius + 12, self.radius + 12)\r\n        \r\n        # Draw selection ring\r\n        if self.is_selected:\r\n            painter.setPen(QPen(QColor(255, 255, 255), 4))\r\n            painter.setBrush(Qt.NoBrush)\r\n            painter.drawEllipse(QPointF(0, 0), self.radius + 5, self.radius + 5)\r\n        \r\n        # Draw BLACK hexagon body\r\n        sides = 6\r\n        polygon = QPolygonF()\r\n        angle_step = 360.0 / sides\r\n        for i in range(sides):\r\n            angle = math.radians(i * angle_step - 90)\r\n            polygon.append(QPointF(self.radius * math.cos(angle), \r\n                                   self.radius * math.sin(angle)))\r\n        \r\n        painter.setBrush(QBrush(QColor(0, 0, 0)))  # BLACK fill\r\n        painter.setPen(QPen(QColor(50, 50, 50), 2))\r\n        painter.drawPolygon(polygon)\r\n        \r\n        # Draw WHITE 'c' inside\r\n        font = QFont(\"Arial\", int(self.radius * 0.7))\r\n        font.setBold(True)\r\n        painter.setFont(font)\r\n        painter.setPen(QColor(255, 255, 255))  # WHITE text\r\n        \r\n        rect = QRectF(-self.radius, -self.radius, self.radius * 2, self.radius * 2)\r\n        painter.drawText(rect, Qt.AlignCenter, \"c\")\r\n        \r\n        painter.restore()\r\n\r\n\r\nclass NeuronItem(QGraphicsEllipseItem):\r\n    \"\"\"\r\n    Neuron body. \r\n    Acts as the Parent item for the Ring and Label.\r\n    \"\"\"\r\n    \r\n    def __init__(self, x, y, radius, name, neuron_type=None, parent=None):\r\n        # Define geometry centered at (0,0) locally\r\n        super().__init__(-radius, -radius, radius * 2, radius * 2, parent)\r\n        self.name = name\r\n        self.radius = radius\r\n        self.is_selected = False\r\n        self.is_hovered = False\r\n        self.neuron_type = neuron_type\r\n        # Set absolute position in the scene\r\n        self.setPos(x, y)\r\n        \r\n        self.setAcceptHoverEvents(True)\r\n        self.setData(0, ('neuron', name))\r\n        self.setZValue(2)\r\n\r\n    def event(self, event):\r\n        if self.scene() is None:\r\n            return False\r\n        return super().event(event)\r\n    \r\n    def hoverEnterEvent(self, event):\r\n        self.is_hovered = True\r\n        self.update()\r\n        super().hoverEnterEvent(event)\r\n    \r\n    def hoverLeaveEvent(self, event):\r\n        self.is_hovered = False\r\n        self.update()\r\n        super().hoverLeaveEvent(event)\r\n    \r\n    def paint(self, painter, option, widget):\r\n        # Draw hover glow\r\n        if self.is_hovered and not self.is_selected:\r\n            painter.setRenderHint(QPainter.Antialiasing)\r\n            # Glow is drawn relative to local center (0,0)\r\n            grad = QRadialGradient(0, 0, self.radius + 12)\r\n            grad.setColorAt(0.5, QColor(100, 200, 255, 100))\r\n            grad.setColorAt(1.0, QColor(100, 200, 255, 0))\r\n            painter.setBrush(QBrush(grad))\r\n            painter.setPen(Qt.NoPen)\r\n            painter.drawEllipse(-self.radius - 12, -self.radius - 12, (self.radius + 12)*2, (self.radius + 12)*2)\r\n        \r\n        super().paint(painter, option, widget)\r\n\r\n\r\nclass DesignerConfig:\r\n    \"\"\"Manages designer preferences.\"\"\"\r\n    def __init__(self, config_path=None):\r\n        if config_path is None:\r\n            config_dir = os.path.expanduser(\"~/.dosidicus\")\r\n            os.makedirs(config_dir, exist_ok=True)\r\n            config_path = os.path.join(config_dir, \"designer_config.json\")\r\n        self.config_path = config_path\r\n        self.config = self._load_config()\r\n    \r\n    def _load_config(self):\r\n        if os.path.exists(self.config_path):\r\n            try:\r\n                with open(self.config_path, 'r') as f:\r\n                    return json.load(f)\r\n            except: pass\r\n        return {'confirm_connection_delete': True}\r\n    \r\n    def save(self):\r\n        try:\r\n            with open(self.config_path, 'w') as f:\r\n                json.dump(self.config, f, indent=2)\r\n        except: pass\r\n    \r\n    def get(self, key, default=None):\r\n        return self.config.get(key, default)\r\n    \r\n    def set(self, key, value):\r\n        self.config[key] = value\r\n        self.save()\r\n\r\n\r\nclass ConfirmDeleteDialog(QDialog):\r\n    \"\"\"Confirmation dialog.\"\"\"\r\n    def __init__(self, source, target, parent=None):\r\n        super().__init__(parent)\r\n        self.setWindowTitle(\"Delete Connection\")\r\n        self.dont_ask_again = False\r\n        layout = QVBoxLayout()\r\n        msg = QLabel(f\"Are you sure you want to delete the connection:\\n{source} → {target}?\")\r\n        msg.setWordWrap(True)\r\n        layout.addWidget(msg)\r\n        self.checkbox = QCheckBox(\"Don't ask again\")\r\n        layout.addWidget(self.checkbox)\r\n        button_layout = QHBoxLayout()\r\n        self.yes_button = QPushButton(\"Yes, Delete\")\r\n        self.yes_button.clicked.connect(self.accept)\r\n        self.no_button = QPushButton(\"Cancel\")\r\n        self.no_button.clicked.connect(self.reject)\r\n        button_layout.addWidget(self.no_button)\r\n        button_layout.addWidget(self.yes_button)\r\n        layout.addLayout(button_layout)\r\n        self.setLayout(layout)\r\n        self.yes_button.setDefault(True)\r\n    \r\n    def accept(self):\r\n        self.dont_ask_again = self.checkbox.isChecked()\r\n        super().accept()\r\n\r\n\r\nclass ConnectionWeightDialog(QDialog):\r\n    \"\"\"Dialog for editing connection weight.\"\"\"\r\n    def __init__(self, source, target, current_weight, config, parent=None):\r\n        super().__init__(parent)\r\n        self.source = source\r\n        self.target = target\r\n        self.config = config\r\n        self.delete_requested = False\r\n        self.setWindowTitle(\"Edit Connection\")\r\n        self.setModal(True)\r\n        layout = QVBoxLayout()\r\n        info_label = QLabel(f\"Connection: {source} → {target}\")\r\n        info_label.setStyleSheet(\"font-weight: bold;\")\r\n        layout.addWidget(info_label)\r\n        weight_layout = QHBoxLayout()\r\n        weight_layout.addWidget(QLabel(\"Weight:\"))\r\n        self.weight_spin = QDoubleSpinBox()\r\n        self.weight_spin.setRange(-1.0, 1.0)\r\n        self.weight_spin.setSingleStep(0.05)\r\n        self.weight_spin.setDecimals(3)\r\n        self.weight_spin.setValue(current_weight)\r\n        self.weight_spin.setMinimumWidth(100)\r\n        weight_layout.addWidget(self.weight_spin)\r\n        layout.addLayout(weight_layout)\r\n        info_text = QLabel(\"Positive = Excitatory (green), Negative = Inhibitory (red)\")\r\n        info_text.setStyleSheet(\"color: gray; font-size: 9pt;\")\r\n        layout.addWidget(info_text)\r\n        layout.addSpacing(10)\r\n        button_layout = QHBoxLayout()\r\n        self.delete_button = QPushButton(\"Delete Connection\")\r\n        self.delete_button.setStyleSheet(\"background-color: #d32f2f; color: white;\")\r\n        self.delete_button.clicked.connect(self.on_delete_clicked)\r\n        button_layout.addWidget(self.delete_button)\r\n        button_layout.addStretch()\r\n        self.cancel_button = QPushButton(\"Cancel\")\r\n        self.cancel_button.clicked.connect(self.reject)\r\n        button_layout.addWidget(self.cancel_button)\r\n        self.ok_button = QPushButton(\"OK\")\r\n        self.ok_button.clicked.connect(self.accept)\r\n        self.ok_button.setDefault(True)\r\n        button_layout.addWidget(self.ok_button)\r\n        layout.addLayout(button_layout)\r\n        self.setLayout(layout)\r\n        self.weight_spin.setFocus()\r\n        self.weight_spin.selectAll()\r\n    \r\n    def on_delete_clicked(self):\r\n        should_confirm = self.config.get('confirm_connection_delete', True)\r\n        if should_confirm:\r\n            confirm_dlg = ConfirmDeleteDialog(self.source, self.target, self)\r\n            result = confirm_dlg.exec_()\r\n            if result == QDialog.Accepted:\r\n                if confirm_dlg.dont_ask_again:\r\n                    self.config.set('confirm_connection_delete', False)\r\n                self.delete_requested = True\r\n                self.reject()\r\n        else:\r\n            self.delete_requested = True\r\n            self.reject()\r\n    \r\n    def get_weight(self):\r\n        return self.weight_spin.value()\r\n\r\n\r\nclass BrainCanvas(QGraphicsView):\r\n    \"\"\"Main canvas for visualizing and editing brain designs.\"\"\"\r\n    \r\n    # Signals\r\n    neuronSelected = pyqtSignal(str) \r\n    neuronMoved = pyqtSignal(str, float, float)\r\n    connectionCreated = pyqtSignal(str, str)\r\n    connectionSelected = pyqtSignal(str, str)\r\n    connectionDeleted = pyqtSignal(str, str)\r\n    canvasClicked = pyqtSignal(float, float)\r\n    weightChanged = pyqtSignal(str, str, float)\r\n    connectionReversed = pyqtSignal(str, str)\r\n    \r\n    NEURON_RADIUS = 25\r\n    WEIGHT_STEP = 0.05\r\n    WEIGHT_STEP_LARGE = 0.25\r\n    \r\n    GRID_SIZE_MINOR = 25\r\n    GRID_SIZE_MAJOR = 100\r\n    GRID_COLOR_MINOR = QColor(220, 220, 230, 100)\r\n    GRID_COLOR_MAJOR = QColor(200, 200, 215, 150)\r\n    \r\n    def __init__(self, design: BrainDesign, parent=None):\r\n        super().__init__(parent)\r\n        self.design = design\r\n        self.scene = QGraphicsScene(self)\r\n        self.setScene(self.scene)\r\n        self.config = DesignerConfig()\r\n        \r\n        self.selected_neuron = None\r\n        self.selected_connection_key = None\r\n        \r\n        self.pan_active = False\r\n        self.pan_start_pos = QPointF()\r\n        self.drag_line = None\r\n        self.drag_source_id = None\r\n        self.drag_start_pos = None\r\n        self.zoom_level = 1.0\r\n        \r\n        self.dragging_neuron = None\r\n        self.neuron_drag_start_pos = None\r\n        \r\n        self.neuron_items = {}\r\n        self.connection_items = {}\r\n        \r\n        self.setRenderHint(QPainter.Antialiasing)\r\n        self.setRenderHint(QPainter.SmoothPixmapTransform)\r\n        self.setDragMode(QGraphicsView.NoDrag)\r\n        self.setMouseTracking(True)\r\n        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)\r\n        self.scene.setSceneRect(-500, -200, 1500, 800)\r\n        self.setFocusPolicy(Qt.StrongFocus)\r\n        \r\n        self.rebuild()\r\n        \r\n        self.anim_timer = QTimer(self)\r\n        self.anim_timer.timeout.connect(self.animate_network)\r\n        self.anim_timer.start(33)\r\n\r\n    def drawBackground(self, painter, rect):\r\n        painter.fillRect(rect, QColor(245, 245, 250))\r\n        left = int(rect.left()) - (int(rect.left()) % self.GRID_SIZE_MINOR) - self.GRID_SIZE_MINOR\r\n        top = int(rect.top()) - (int(rect.top()) % self.GRID_SIZE_MINOR) - self.GRID_SIZE_MINOR\r\n        right = int(rect.right()) + self.GRID_SIZE_MINOR\r\n        bottom = int(rect.bottom()) + self.GRID_SIZE_MINOR\r\n        \r\n        painter.setRenderHint(QPainter.Antialiasing, False)\r\n        painter.setPen(QPen(self.GRID_COLOR_MINOR, 1))\r\n        \r\n        x = left\r\n        while x <= right:\r\n            if x % self.GRID_SIZE_MAJOR != 0:\r\n                painter.drawLine(x, top, x, bottom)\r\n            x += self.GRID_SIZE_MINOR\r\n        \r\n        y = top\r\n        while y <= bottom:\r\n            if y % self.GRID_SIZE_MAJOR != 0:\r\n                painter.drawLine(left, y, right, y)\r\n            y += self.GRID_SIZE_MINOR\r\n        \r\n        painter.setPen(QPen(self.GRID_COLOR_MAJOR, 1.5))\r\n        major_left = left - (left % self.GRID_SIZE_MAJOR)\r\n        x = major_left\r\n        while x <= right:\r\n            painter.drawLine(x, top, x, bottom)\r\n            x += self.GRID_SIZE_MAJOR\r\n        \r\n        major_top = top - (top % self.GRID_SIZE_MAJOR)\r\n        y = major_top\r\n        while y <= bottom:\r\n            painter.drawLine(left, y, right, y)\r\n            y += self.GRID_SIZE_MAJOR\r\n        \r\n        if left <= 0 <= right:\r\n            painter.setPen(QPen(QColor(180, 180, 200, 180), 2))\r\n            painter.drawLine(0, top, 0, bottom)\r\n        if top <= 0 <= bottom:\r\n            painter.setPen(QPen(QColor(180, 180, 200, 180), 2))\r\n            painter.drawLine(left, 0, right, 0)\r\n\r\n    def animate_network(self):\r\n        for item in self.connection_items.values():\r\n            item.advance_animation()\r\n        if hasattr(self, 'dying_connections'):\r\n            for i in range(len(self.dying_connections) - 1, -1, -1):\r\n                item = self.dying_connections[i]\r\n                is_dead = item.advance_animation()\r\n                if is_dead:\r\n                    self.scene.removeItem(item)\r\n                    self.dying_connections.pop(i)\r\n\r\n    def rebuild(self):\r\n        \"\"\"Synchronizes the visual scene with the design data.\"\"\"\r\n        self.scene.blockSignals(True)\r\n        \r\n        # 1. Update Layers\r\n        for item in self.scene.items():\r\n            if not isinstance(item, (NeuronItem, ConnectorNeuronItem, SmartConnectionItem)) and item.zValue() < 0:\r\n                if isinstance(item, (QGraphicsRectItem, QGraphicsTextItem)): \r\n                     if item.zValue() < -5: self.scene.removeItem(item)\r\n        self.draw_layers()\r\n\r\n        # 2. Update Neurons\r\n        existing_neurons = set(self.neuron_items.keys())\r\n        design_neurons = set(self.design.neurons.keys())\r\n        \r\n        for name in existing_neurons - design_neurons:\r\n            self.scene.removeItem(self.neuron_items[name])\r\n            del self.neuron_items[name]\r\n            \r\n        for name in design_neurons:\r\n            neuron = self.design.neurons[name]\r\n            if neuron.position is None: continue\r\n            \r\n            x, y = neuron.position\r\n            \r\n            if name in self.neuron_items:\r\n                # Update existing position\r\n                self.neuron_items[name].setPos(x, y) \r\n            else:\r\n                self.draw_single_neuron(name, neuron)\r\n\r\n        # 3. Connection Update\r\n        existing_keys = set(self.connection_items.keys())\r\n        design_keys = set()\r\n        for conn in self.design.connections:\r\n            design_keys.add((conn.source, conn.target))\r\n        \r\n        for key in existing_keys - design_keys:\r\n            item = self.connection_items[key]\r\n            item.is_dying = True\r\n            del self.connection_items[key]\r\n            if not hasattr(self, 'dying_connections'):\r\n                self.dying_connections = []\r\n            self.dying_connections.append(item)\r\n\r\n        for conn in self.design.connections:\r\n            key = (conn.source, conn.target)\r\n            src_neuron = self.design.get_neuron(conn.source)\r\n            tgt_neuron = self.design.get_neuron(conn.target)\r\n            \r\n            if not (src_neuron and tgt_neuron and src_neuron.position and tgt_neuron.position):\r\n                continue\r\n\r\n            src_pos = QPointF(*src_neuron.position)\r\n            tgt_pos = QPointF(*tgt_neuron.position)\r\n\r\n            if key not in existing_keys:\r\n                item = SmartConnectionItem(src_pos, tgt_pos, conn.weight, conn.source, conn.target)\r\n                item.setData(0, conn)\r\n                if self.selected_connection_key == key: item.is_selected = True\r\n                self.scene.addItem(item)\r\n                self.connection_items[key] = item\r\n            else:\r\n                item = self.connection_items[key]\r\n                item.is_dying = False \r\n                \r\n                # Check for weight change to ensure visual thickness updates\r\n                if item.weight != conn.weight:\r\n                    item.weight = conn.weight\r\n                    item.update()\r\n                \r\n                if self.selected_connection_key == key: item.is_selected = True\r\n                else: item.is_selected = False\r\n                \r\n                if src_pos != item.source_pos or tgt_pos != item.target_pos:\r\n                    item.prepareGeometryChange()\r\n                    item.source_pos = src_pos\r\n                    item.target_pos = tgt_pos\r\n                    item.update()\r\n\r\n        self.center_on_neurons()\r\n        self.scene.blockSignals(False)\r\n        \r\n    def draw_layers(self):\r\n        for layer in self.design.layers:\r\n            y_pos = layer.y_position\r\n            rect_height = DEFAULT_LAYER_HEIGHT\r\n            rect_top = y_pos - rect_height / 2\r\n            \r\n            lt = layer.layer_type.name.lower()\r\n            if lt == 'input': fill, border = (180, 235, 180, 100), (120, 180, 120, 150)\r\n            elif lt == 'output': fill, border = (235, 180, 180, 100), (180, 120, 120, 150)\r\n            else: fill, border = (200, 200, 235, 100), (160, 160, 200, 150)\r\n\r\n            rect = QGraphicsRectItem(-400, rect_top, 1300, rect_height)\r\n            rect.setBrush(QBrush(QColor(*fill)))\r\n            rect.setPen(QPen(QColor(*border), 2, Qt.DashLine))\r\n            rect.setZValue(-10)\r\n            self.scene.addItem(rect)\r\n\r\n            label = QGraphicsTextItem(layer.name)\r\n            label.setDefaultTextColor(QColor(*border[:3]))\r\n            label.setFont(QFont(\"Arial\", 11, QFont.Bold))\r\n            label.setPos(-380, rect_top + 5)\r\n            label.setZValue(-9)\r\n            self.scene.addItem(label)\r\n\r\n    def draw_single_neuron(self, name, neuron):\r\n        \"\"\"Draw a single neuron item (Body) and attach its children (Ring, Label).\"\"\"\r\n        if neuron.position is None: return\r\n\r\n        try:\r\n            x, y = neuron.position\r\n        except (TypeError, ValueError): return\r\n\r\n        # Determine Shape\r\n        shape = getattr(neuron, 'shape', 'circle')\r\n\r\n        # --- CASE 1: CONNECTOR (Hexagon) ---\r\n        is_connector = (neuron.neuron_type == NeuronType.CONNECTOR or \r\n                        name.startswith('connector_') or\r\n                        shape == 'hexagon')\r\n        \r\n        if is_connector:\r\n            # Use the special ConnectorNeuronItem (black hexagon with 'c')\r\n            body = ConnectorNeuronItem(x, y, self.NEURON_RADIUS, name)\r\n            body.is_selected = (name == self.selected_neuron)\r\n            self.scene.addItem(body)\r\n            self.neuron_items[name] = body\r\n            \r\n            # Create Label (Child of Body) - simplified for connectors\r\n            display = name.replace('_', ' ').title()\r\n            label = QGraphicsTextItem(display)\r\n            label.setDefaultTextColor(QColor(40, 40, 50))\r\n            label.setFont(QFont(\"Arial\", 8, QFont.Bold))\r\n            \r\n            # Center text horizontally relative to body center (0,0)\r\n            w = label.boundingRect().width()\r\n            label.setPos(-w / 2, self.NEURON_RADIUS + 5)\r\n            label.setZValue(3)\r\n            label.setParentItem(body)\r\n            return\r\n\r\n        # --- CASE 2: POLYGONS (Diamond, Square, Triangle) ---\r\n        if shape in ['diamond', 'square', 'triangle']:\r\n            sides = 4\r\n            rotation = 0\r\n            if shape == 'diamond':\r\n                sides = 4\r\n                rotation = 0\r\n            elif shape == 'square':\r\n                sides = 4\r\n                rotation = 45\r\n            elif shape == 'triangle':\r\n                sides = 3\r\n                rotation = 0\r\n            \r\n            # Create Polygon Body\r\n            body = PolygonNeuronItem(x, y, self.NEURON_RADIUS, name, \r\n                                     sides=sides, rotation=rotation, \r\n                                     color=neuron.color)\r\n            body.is_selected = (name == self.selected_neuron)\r\n            self.scene.addItem(body)\r\n            self.neuron_items[name] = body\r\n\r\n            # Create Label (Child of Body)\r\n            display = name.replace('_', ' ').title()\r\n            label = QGraphicsTextItem(display)\r\n            label.setDefaultTextColor(QColor(40, 40, 50))\r\n            label.setFont(QFont(\"Arial\", 9, QFont.Bold))\r\n            \r\n            w = label.boundingRect().width()\r\n            label.setPos(-w / 2, self.NEURON_RADIUS + 5)\r\n            label.setZValue(3)\r\n            label.setParentItem(body)\r\n            return\r\n\r\n        # --- CASE 3: CIRCLE (Default) ---\r\n        # 1. Create Neuron Body (The Parent Item) - standard ellipse\r\n        body = NeuronItem(x, y, self.NEURON_RADIUS, name, neuron.neuron_type)\r\n        body.setBrush(QBrush(QColor(*neuron.color)))\r\n        body.setPen(QPen(QColor(50, 50, 50), 2))\r\n        body.is_selected = (name == self.selected_neuron)\r\n        \r\n        self.scene.addItem(body)\r\n        self.neuron_items[name] = body\r\n\r\n        # 2. Determine Ring Style (Only for circles)\r\n        if neuron.is_core:\r\n            ring_color = QColor(*CORE_NEURON_RING_COLOR)\r\n            ring_width = PROTECTED_RING_WIDTH\r\n        elif neuron.is_required:\r\n            ring_color = QColor(*INPUT_SENSOR_RING_COLOR)\r\n            ring_width = PROTECTED_RING_WIDTH\r\n        elif neuron.is_sensor:\r\n            ring_color = QColor(*INPUT_SENSOR_RING_COLOR)\r\n            ring_width = NORMAL_RING_WIDTH\r\n        else:\r\n            ring_color = QColor(*CUSTOM_NEURON_RING_COLOR)\r\n            ring_width = NORMAL_RING_WIDTH\r\n\r\n        if name == self.selected_neuron:\r\n            ring_color = QColor(255, 255, 255)\r\n            ring_width = 4\r\n\r\n        # 3. Create Ring (Child of Body)\r\n        r_outer = self.NEURON_RADIUS + 3\r\n        outer = QGraphicsEllipseItem(-r_outer, -r_outer, r_outer * 2, r_outer * 2)\r\n        outer.setPen(QPen(ring_color, ring_width))\r\n        outer.setZValue(1) \r\n        outer.setData(0, ('ring', name))\r\n        outer.setParentItem(body)\r\n        outer.setFlag(QGraphicsItem.ItemStacksBehindParent)\r\n\r\n        # 4. Create Label (Child of Body)\r\n        display = name.replace('_', ' ').title()\r\n        # Add icons for specific types\r\n        if neuron.is_core: display = f\"🟡 {display}\"\r\n        elif name == 'can_see_food': display = f\"👁 {display}\"\r\n        elif neuron.is_sensor: display = f\"📡 {display}\"\r\n        \r\n        label = QGraphicsTextItem(display)\r\n        label.setDefaultTextColor(QColor(40, 40, 50))\r\n        label.setFont(QFont(\"Arial\", 9, QFont.Bold))\r\n        \r\n        # Center text horizontally relative to body center (0,0)\r\n        w = label.boundingRect().width()\r\n        label.setPos(-w / 2, self.NEURON_RADIUS + 5)\r\n        label.setZValue(3)\r\n        label.setParentItem(body)\r\n\r\n\r\n    def draw_neurons(self):\r\n        for name, neuron in self.design.neurons.items():\r\n            self.draw_single_neuron(name, neuron)\r\n\r\n    def center_on_neurons(self):\r\n        if not self.design.neurons or self.pan_active: return\r\n        valid_positions = []\r\n        for n in self.design.neurons.values():\r\n            if n.position is not None:\r\n                try:\r\n                    x, y = n.position\r\n                    if x is not None and y is not None:\r\n                        valid_positions.append((x, y))\r\n                except: continue\r\n        \r\n        if not valid_positions: return\r\n        xs = [p[0] for p in valid_positions]\r\n        ys = [p[1] for p in valid_positions]\r\n        self.scene.setSceneRect(QRectF(\r\n            min(xs) - 100, min(ys) - 100, \r\n            max(xs) - min(xs) + 200, max(ys) - min(ys) + 200\r\n        ))\r\n\r\n    def get_neuron_at(self, pos):\r\n        for name, neuron in self.design.neurons.items():\r\n            if neuron.position is None: continue\r\n            x, y = neuron.position\r\n            if math.hypot(pos.x() - x, pos.y() - y) <= self.NEURON_RADIUS + 5:\r\n                return name\r\n        return None\r\n    \r\n    def get_connection_at(self, pos):\r\n        items = self.scene.items(pos)\r\n        for item in items:\r\n            if isinstance(item, SmartConnectionItem):\r\n                return item\r\n        return None\r\n\r\n    # MOUSE EVENTS\r\n    def mousePressEvent(self, event):\r\n        self.setFocus()\r\n        scene_pos = self.mapToScene(event.pos())\r\n        \r\n        if event.button() == Qt.RightButton or event.button() == Qt.MiddleButton:\r\n            self.pan_active = True\r\n            self.pan_start_pos = event.pos()\r\n            self.setCursor(Qt.ClosedHandCursor)\r\n            event.accept()\r\n            return\r\n\r\n        if event.button() == Qt.LeftButton:\r\n            clicked_neuron = self.get_neuron_at(scene_pos)\r\n            if clicked_neuron:\r\n                self.select_neuron(clicked_neuron)\r\n                if event.modifiers() & Qt.ControlModifier:\r\n                    self.dragging_neuron = clicked_neuron\r\n                    self.neuron_drag_start_pos = scene_pos\r\n                    event.accept()\r\n                    return\r\n                \r\n                n_obj = self.design.get_neuron(clicked_neuron)\r\n                if n_obj.neuron_type != NeuronType.OUTPUT:\r\n                    self.start_connection_drag(clicked_neuron, scene_pos)\r\n                event.accept()\r\n                return\r\n\r\n            conn_item = self.get_connection_at(scene_pos)\r\n            if conn_item:\r\n                conn_data = conn_item.data(0)\r\n                self.select_connection(conn_data.source, conn_data.target)\r\n                event.accept()\r\n                return\r\n            \r\n            self.clear_selection()\r\n        super().mousePressEvent(event)\r\n\r\n    def mouseDoubleClickEvent(self, event):\r\n        if event.button() == Qt.LeftButton:\r\n            scene_pos = self.mapToScene(event.pos())\r\n            conn_item = self.get_connection_at(scene_pos)\r\n            if conn_item:\r\n                conn_data = conn_item.data(0)\r\n                self.open_weight_dialog(conn_data.source, conn_data.target)\r\n                event.accept()\r\n                return\r\n        super().mouseDoubleClickEvent(event)\r\n\r\n    def mouseMoveEvent(self, event):\r\n        scene_pos = self.mapToScene(event.pos())\r\n        \r\n        if self.pan_active:\r\n            delta = event.pos() - self.pan_start_pos\r\n            self.pan_start_pos = event.pos()\r\n            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())\r\n            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())\r\n            event.accept()\r\n            return\r\n        \r\n        if self.dragging_neuron:\r\n            delta = scene_pos - self.neuron_drag_start_pos\r\n            neuron = self.design.get_neuron(self.dragging_neuron)\r\n            if neuron:\r\n                old_pos = neuron.position\r\n                new_pos = (old_pos[0] + delta.x(), old_pos[1] + delta.y())\r\n                neuron.position = new_pos\r\n                self.neuron_drag_start_pos = scene_pos\r\n                self.rebuild()\r\n            event.accept()\r\n            return\r\n        \r\n        if self.drag_line:\r\n            target_id = self.get_neuron_at(scene_pos)\r\n            if target_id and target_id != self.drag_source_id:\r\n                end = QPointF(*self.design.get_neuron(target_id).position)\r\n                valid = self.is_valid_connection(self.drag_source_id, target_id)\r\n                color = QColor(50, 205, 50) if valid else QColor(220, 20, 60)\r\n                style = Qt.SolidLine if valid else Qt.DashLine\r\n            else:\r\n                end = scene_pos\r\n                color = QColor(255, 215, 0)\r\n                style = Qt.DashLine\r\n            self.drag_line.setLine(QLineF(self.drag_start_pos, end))\r\n            self.drag_line.setPen(QPen(color, 3, style))\r\n            event.accept()\r\n            return\r\n        super().mouseMoveEvent(event)\r\n\r\n    def mouseReleaseEvent(self, event):\r\n        if event.button() == Qt.RightButton or event.button() == Qt.MiddleButton:\r\n            self.pan_active = False\r\n            self.setCursor(Qt.ArrowCursor)\r\n            event.accept()\r\n            return\r\n        \r\n        if self.dragging_neuron:\r\n            self.dragging_neuron = None\r\n            self.neuron_drag_start_pos = None\r\n            event.accept()\r\n            return\r\n        \r\n        if self.drag_line:\r\n            scene_pos = self.mapToScene(event.pos())\r\n            target_id = self.get_neuron_at(scene_pos)\r\n            self.scene.removeItem(self.drag_line)\r\n            self.drag_line = None\r\n            if target_id and target_id != self.drag_source_id:\r\n                if self.is_valid_connection(self.drag_source_id, target_id):\r\n                    self.design.add_connection(self.drag_source_id, target_id, 0.5)\r\n                    self.connectionCreated.emit(self.drag_source_id, target_id)\r\n                    self.select_connection(self.drag_source_id, target_id)\r\n                    self.rebuild()\r\n                else:\r\n                    QToolTip.showText(QCursor.pos(), \"Invalid connection\", self)\r\n            self.drag_source_id = None\r\n            event.accept()\r\n            return\r\n        super().mouseReleaseEvent(event)\r\n\r\n    # KEY EVENTS\r\n    def keyPressEvent(self, event):\r\n        if event.key() == Qt.Key_Delete:\r\n            if self.selected_connection_key:\r\n                self.design.remove_connection(*self.selected_connection_key)\r\n                self.selected_connection_key = None\r\n                self.rebuild()\r\n                event.accept()\r\n                return\r\n        if event.key() == Qt.Key_Space:\r\n            if self.selected_connection_key:\r\n                self.reverse_selected_connection()\r\n                event.accept()\r\n                return\r\n        if event.key() in [Qt.Key_Plus, Qt.Key_Equal, Qt.Key_Minus, Qt.Key_Underscore]:\r\n            self.adjust_connection_weight_by_key(event)\r\n            event.accept()\r\n            return\r\n        if event.key() in [Qt.Key_PageUp, Qt.Key_PageDown]:\r\n            self.adjust_connection_weight_page(event)\r\n            event.accept()\r\n            return\r\n        super().keyPressEvent(event)\r\n\r\n    def wheelEvent(self, event):\r\n        \"\"\"\r\n        Mouse wheel behavior:\r\n        - If a connection is SELECTED (clicked), scroll changes its weight\r\n        - Otherwise, scroll zooms the canvas\r\n        \"\"\"\r\n        # Only adjust weight if a connection is explicitly selected (clicked)\r\n        if self.selected_connection_key:\r\n            conn = self.design.get_connection(*self.selected_connection_key)\r\n            if conn:\r\n                delta = event.angleDelta().y()\r\n                step = self.WEIGHT_STEP_LARGE if event.modifiers() & Qt.ShiftModifier else self.WEIGHT_STEP\r\n                change = step if delta > 0 else -step\r\n                new_weight = max(-1.0, min(1.0, conn.weight + change))\r\n                if abs(new_weight) < 0.03: new_weight = 0.0\r\n                conn.weight = new_weight\r\n                self.rebuild()\r\n                self.weightChanged.emit(conn.source, conn.target, new_weight)\r\n                QToolTip.showText(QCursor.pos(), f\"Weight: {new_weight:+.2f}\", self)\r\n                event.accept()\r\n                return\r\n        \r\n        # Default: zoom canvas\r\n        factor = 1.15 if event.angleDelta().y() > 0 else 1 / 1.15\r\n        self.zoom_level = max(0.2, min(3.0, self.zoom_level * factor))\r\n        self.setTransform(QTransform().scale(self.zoom_level, self.zoom_level))\r\n        event.accept()\r\n\r\n    # SELECTION HELPERS\r\n    def select_neuron(self, name):\r\n        self.selected_neuron = name\r\n        self.selected_connection_key = None\r\n        self.neuronSelected.emit(name)\r\n        self.rebuild()\r\n    \r\n    def select_connection(self, source, target):\r\n        self.selected_connection_key = (source, target)\r\n        self.selected_neuron = None\r\n        self.connectionSelected.emit(source, target)\r\n        self.rebuild()\r\n    \r\n    def clear_selection(self):\r\n        self.selected_neuron = None\r\n        self.selected_connection_key = None\r\n        self.rebuild()\r\n\r\n    def start_connection_drag(self, neuron_name, scene_pos):\r\n        n_obj = self.design.get_neuron(neuron_name)\r\n        self.drag_source_id = neuron_name\r\n        self.drag_start_pos = QPointF(*n_obj.position)\r\n        self.drag_line = QGraphicsLineItem(QLineF(self.drag_start_pos, scene_pos))\r\n        self.drag_line.setPen(QPen(QColor(255, 215, 0), 3, Qt.DashLine))\r\n        self.drag_line.setZValue(10)\r\n        self.scene.addItem(self.drag_line)\r\n\r\n    def is_valid_connection(self, src_name, tgt_name):\r\n        src = self.design.get_neuron(src_name)\r\n        tgt = self.design.get_neuron(tgt_name)\r\n        if not src or not tgt: return False\r\n        if tgt.neuron_type in [NeuronType.INPUT, NeuronType.SENSOR]: return False\r\n        if src.neuron_type == NeuronType.OUTPUT: return False\r\n        if self.design.get_connection(src_name, tgt_name): return False\r\n        return True\r\n\r\n    def reverse_selected_connection(self):\r\n        if not self.selected_connection_key: return\r\n        source, target = self.selected_connection_key\r\n        conn = self.design.get_connection(source, target)\r\n        if not conn: return\r\n        src_neuron = self.design.get_neuron(source)\r\n        tgt_neuron = self.design.get_neuron(target)\r\n        if self.design.get_connection(target, source): return\r\n        if src_neuron and src_neuron.neuron_type in [NeuronType.SENSOR, NeuronType.INPUT]: return\r\n        if tgt_neuron and tgt_neuron.neuron_type == NeuronType.OUTPUT: return\r\n        weight = conn.weight\r\n        self.design.remove_connection(source, target)\r\n        self.design.add_connection(target, source, weight)\r\n        self.selected_connection_key = (target, source)\r\n        self.connectionReversed.emit(source, target)\r\n        self.rebuild()\r\n\r\n    def open_weight_dialog(self, source, target):\r\n        conn = self.design.get_connection(source, target)\r\n        if not conn: return\r\n        dialog = ConnectionWeightDialog(source, target, conn.weight, self.config, self)\r\n        result = dialog.exec_()\r\n        if dialog.delete_requested:\r\n            self.design.remove_connection(source, target)\r\n            self.selected_connection_key = None\r\n            self.connectionDeleted.emit(source, target)\r\n            self.rebuild()\r\n            return\r\n        if result == QDialog.Accepted:\r\n            new_weight = dialog.get_weight()\r\n            if new_weight != conn.weight:\r\n                conn.weight = new_weight\r\n                self.weightChanged.emit(source, target, new_weight)\r\n                self.rebuild()\r\n\r\n    def adjust_connection_weight_by_key(self, event):\r\n        pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))\r\n        conn_item = self.get_connection_at(pos)\r\n        if conn_item: conn = conn_item.data(0)\r\n        elif self.selected_connection_key: conn = self.design.get_connection(*self.selected_connection_key)\r\n        else: return\r\n        if not conn: return\r\n        step = self.WEIGHT_STEP_LARGE if event.modifiers() & Qt.ShiftModifier else self.WEIGHT_STEP\r\n        if event.key() in [Qt.Key_Minus, Qt.Key_Underscore]: step = -step\r\n        conn.weight = max(-1.0, min(1.0, conn.weight + step))\r\n        self.rebuild()\r\n        self.weightChanged.emit(conn.source, conn.target, conn.weight)\r\n        QToolTip.showText(QCursor.pos(), f\"Weight: {conn.weight:+.2f}\", self)\r\n\r\n    def adjust_connection_weight_page(self, event):\r\n        pos = self.mapToScene(self.mapFromGlobal(QCursor.pos()))\r\n        conn_item = self.get_connection_at(pos)\r\n        if conn_item: conn = conn_item.data(0)\r\n        elif self.selected_connection_key: conn = self.design.get_connection(*self.selected_connection_key)\r\n        else: return\r\n        if not conn: return\r\n        step = self.WEIGHT_STEP_LARGE\r\n        if event.key() == Qt.Key_PageDown: step = -step\r\n        conn.weight = max(-1.0, min(1.0, conn.weight + step))\r\n        self.rebuild()\r\n        self.weightChanged.emit(conn.source, conn.target, conn.weight)\r\n        QToolTip.showText(QCursor.pos(), f\"Weight: {conn.weight:+.2f}\", self)\r\n\r\n\r\nclass PolygonNeuronItem(QGraphicsItem):\r\n    \"\"\"\r\n    Polygonal Neuron Body (Diamond, Square, Triangle).\r\n    Acts as Parent item for Label (Ring is usually not used for these shapes in Designer).\r\n    \"\"\"\r\n    \r\n    def __init__(self, x, y, radius, name, sides=4, rotation=0, color=(150, 150, 220), parent=None):\r\n        super().__init__(parent)\r\n        self.name = name\r\n        self.radius = radius\r\n        self.sides = sides\r\n        self.rotation_deg = rotation\r\n        self.color = color\r\n        self.is_selected = False\r\n        self.is_hovered = False\r\n        \r\n        self.setPos(x, y)\r\n        self.setAcceptHoverEvents(True)\r\n        self.setData(0, ('neuron', name))\r\n        self.setZValue(2)\r\n        \r\n    def boundingRect(self):\r\n        extra = 5\r\n        return QRectF(-self.radius - extra, -self.radius - extra,\r\n                      (self.radius + extra) * 2, (self.radius + extra) * 2)\r\n    \r\n    def shape(self):\r\n        path = QPainterPath()\r\n        polygon = QPolygonF()\r\n        angle_step = 360.0 / self.sides\r\n        # Apply rotation offset\r\n        offset_rad = math.radians(self.rotation_deg - 90)\r\n        \r\n        for i in range(self.sides):\r\n            angle = math.radians(i * angle_step) + offset_rad\r\n            polygon.append(QPointF(self.radius * math.cos(angle), \r\n                                   self.radius * math.sin(angle)))\r\n        path.addPolygon(polygon)\r\n        path.closeSubpath()\r\n        return path\r\n\r\n    def paint(self, painter, option, widget):\r\n        painter.save()\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        # Draw hover glow\r\n        if self.is_hovered and not self.is_selected:\r\n            painter.setBrush(Qt.NoBrush)\r\n            painter.setPen(QPen(QColor(100, 200, 255, 150), 6))\r\n            # Re-calculate polygon for glow path\r\n            path = self.shape()\r\n            painter.drawPath(path)\r\n\r\n        # Draw Selection Highlight\r\n        if self.is_selected:\r\n            painter.setPen(QPen(QColor(255, 255, 255), 4))\r\n            painter.setBrush(Qt.NoBrush)\r\n            painter.drawPath(self.shape())\r\n\r\n        # Draw Polygon Body\r\n        painter.setBrush(QBrush(QColor(*self.color)))\r\n        painter.setPen(QPen(QColor(50, 50, 50), 2))\r\n        \r\n        polygon = QPolygonF()\r\n        angle_step = 360.0 / self.sides\r\n        offset_rad = math.radians(self.rotation_deg - 90)\r\n        \r\n        for i in range(self.sides):\r\n            angle = math.radians(i * angle_step) + offset_rad\r\n            polygon.append(QPointF(self.radius * math.cos(angle), \r\n                                   self.radius * math.sin(angle)))\r\n        \r\n        painter.drawPolygon(polygon)\r\n        painter.restore()\r\n        \r\n    def hoverEnterEvent(self, event):\r\n        self.is_hovered = True\r\n        self.update()\r\n        super().hoverEnterEvent(event)\r\n    \r\n    def hoverLeaveEvent(self, event):\r\n        self.is_hovered = False\r\n        self.update()\r\n        super().hoverLeaveEvent(event)"
  },
  {
    "path": "src/designer_canvas_utils.py",
    "content": "\"\"\"\r\nEnhanced Canvas Utilities for Brain Designer\r\n\r\nAdditional visual feedback for wiring operations and activation display.\r\n\"\"\"\r\n\r\nfrom PyQt5.QtWidgets import (\r\n    QGraphicsItem, QGraphicsEllipseItem, QGraphicsTextItem, \r\n    QGraphicsRectItem, QGraphicsPathItem, QToolTip\r\n)\r\nfrom PyQt5.QtCore import Qt, QPointF, QRectF, QTimer\r\nfrom PyQt5.QtGui import (\r\n    QPainter, QPen, QBrush, QColor, QFont, QFontMetrics,\r\n    QPainterPath, QRadialGradient, QLinearGradient\r\n)\r\n\r\nimport math\r\n\r\n\r\nclass WiringPreviewItem(QGraphicsItem):\r\n    \"\"\"\r\n    Enhanced preview of a connection being dragged.\r\n    Shows validity feedback, snapping, and weight preview.\r\n    \"\"\"\r\n    \r\n    def __init__(self, start_pos: QPointF, parent=None):\r\n        super().__init__(parent)\r\n        self.start_pos = start_pos\r\n        self.end_pos = start_pos\r\n        self.is_valid = False\r\n        self.target_name = \"\"\r\n        self.preview_weight = 0.5\r\n        self.is_snapped = False\r\n        \r\n        # Animation\r\n        self.pulse_phase = 0.0\r\n        \r\n        self.setZValue(100)  # Above everything\r\n    \r\n    def set_end(self, pos: QPointF, is_valid: bool = False, \r\n                target_name: str = \"\", snapped: bool = False):\r\n        \"\"\"Update the end position and validity state.\"\"\"\r\n        self.end_pos = pos\r\n        self.is_valid = is_valid\r\n        self.target_name = target_name\r\n        self.is_snapped = snapped\r\n        self.update()\r\n    \r\n    def boundingRect(self) -> QRectF:\r\n        return QRectF(self.start_pos, self.end_pos).normalized().adjusted(-50, -50, 50, 50)\r\n    \r\n    def paint(self, painter, option, widget):\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        # Determine colors\r\n        if self.is_valid and self.is_snapped:\r\n            # Valid and snapped to target\r\n            color = QColor(50, 205, 50)  # Green\r\n            glow_color = QColor(50, 205, 50, 80)\r\n            style = Qt.SolidLine\r\n        elif self.is_valid:\r\n            # Valid but not snapped\r\n            color = QColor(100, 180, 100)\r\n            glow_color = QColor(100, 180, 100, 60)\r\n            style = Qt.DashLine\r\n        else:\r\n            # Invalid or dragging in open space\r\n            color = QColor(255, 215, 0)  # Gold\r\n            glow_color = QColor(255, 215, 0, 60)\r\n            style = Qt.DashLine\r\n        \r\n        # Draw glow\r\n        painter.setPen(QPen(glow_color, 12, Qt.SolidLine, Qt.RoundCap))\r\n        painter.drawLine(self.start_pos, self.end_pos)\r\n        \r\n        # Draw main line\r\n        painter.setPen(QPen(color, 3, style, Qt.RoundCap))\r\n        painter.drawLine(self.start_pos, self.end_pos)\r\n        \r\n        # Draw arrowhead at end\r\n        line_vec = self.end_pos - self.start_pos\r\n        length = math.sqrt(line_vec.x()**2 + line_vec.y()**2)\r\n        if length > 10:\r\n            angle = math.atan2(line_vec.y(), line_vec.x())\r\n            arrow_size = 15\r\n            \r\n            p1 = self.end_pos - QPointF(\r\n                math.cos(angle + 0.5) * arrow_size,\r\n                math.sin(angle + 0.5) * arrow_size\r\n            )\r\n            p2 = self.end_pos - QPointF(\r\n                math.cos(angle - 0.5) * arrow_size,\r\n                math.sin(angle - 0.5) * arrow_size\r\n            )\r\n            \r\n            painter.setBrush(QBrush(color))\r\n            painter.setPen(Qt.NoPen)\r\n            path = QPainterPath()\r\n            path.moveTo(self.end_pos)\r\n            path.lineTo(p1)\r\n            path.lineTo(p2)\r\n            path.closeSubpath()\r\n            painter.drawPath(path)\r\n        \r\n        # Draw target label if snapped\r\n        if self.is_snapped and self.target_name:\r\n            mid = (self.start_pos + self.end_pos) / 2\r\n            \r\n            # Background\r\n            font = QFont(\"Arial\", 10, QFont.Bold)\r\n            painter.setFont(font)\r\n            fm = QFontMetrics(font)\r\n            \r\n            label = f\"→ {self.target_name.replace('_', ' ').title()}\"\r\n            text_width = fm.horizontalAdvance(label)\r\n            text_height = fm.height()\r\n            \r\n            rect = QRectF(mid.x() - text_width/2 - 8, mid.y() - text_height/2 - 4,\r\n                         text_width + 16, text_height + 8)\r\n            \r\n            bg_color = QColor(50, 50, 50, 200) if not self.is_valid else QColor(30, 100, 30, 200)\r\n            painter.setBrush(QBrush(bg_color))\r\n            painter.setPen(Qt.NoPen)\r\n            painter.drawRoundedRect(rect, 5, 5)\r\n            \r\n            # Text\r\n            painter.setPen(Qt.white)\r\n            painter.drawText(rect, Qt.AlignCenter, label)\r\n\r\n\r\nclass ActivationBadge(QGraphicsItem):\r\n    \"\"\"\r\n    Badge showing a neuron's current activation value.\r\n    \"\"\"\r\n    \r\n    def __init__(self, center: QPointF, value: float, \r\n                 is_binary: bool = False, parent=None):\r\n        super().__init__(parent)\r\n        self.center = center\r\n        self.value = value\r\n        self.is_binary = is_binary\r\n        \r\n        # Position below the neuron\r\n        self.setPos(center.x() - 20, center.y() + 35)\r\n        self.setZValue(5)\r\n    \r\n    def boundingRect(self) -> QRectF:\r\n        return QRectF(0, 0, 40, 18)\r\n    \r\n    def paint(self, painter, option, widget):\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        rect = self.boundingRect()\r\n        \r\n        # Color based on value\r\n        if self.is_binary:\r\n            if self.value > 50:\r\n                color = QColor(50, 205, 50)  # Green = ON\r\n                text = \"ON\"\r\n            else:\r\n                color = QColor(150, 150, 150)  # Gray = OFF\r\n                text = \"OFF\"\r\n        else:\r\n            # Gradient from blue (low) to red (high)\r\n            ratio = self.value / 100.0\r\n            if ratio < 0.5:\r\n                # Blue to yellow\r\n                r = int(50 + ratio * 2 * 205)\r\n                g = int(50 + ratio * 2 * 155)\r\n                b = int(200 - ratio * 2 * 150)\r\n            else:\r\n                # Yellow to red\r\n                r = 255\r\n                g = int(205 - (ratio - 0.5) * 2 * 155)\r\n                b = int(50 - (ratio - 0.5) * 2 * 50)\r\n            color = QColor(r, g, b)\r\n            text = f\"{self.value:.0f}\"\r\n        \r\n        # Background\r\n        painter.setBrush(QBrush(color))\r\n        painter.setPen(QPen(color.darker(120), 1))\r\n        painter.drawRoundedRect(rect, 4, 4)\r\n        \r\n        # Text\r\n        painter.setPen(Qt.white if color.lightness() < 150 else Qt.black)\r\n        painter.setFont(QFont(\"Arial\", 8, QFont.Bold))\r\n        painter.drawText(rect, Qt.AlignCenter, text)\r\n\r\n\r\nclass ConnectionStrengthIndicator(QGraphicsItem):\r\n    \"\"\"\r\n    Visual indicator of connection strength shown during weight editing.\r\n    \"\"\"\r\n    \r\n    def __init__(self, pos: QPointF, weight: float, parent=None):\r\n        super().__init__(parent)\r\n        self.weight = weight\r\n        self.setPos(pos)\r\n        self.setZValue(50)\r\n    \r\n    def boundingRect(self) -> QRectF:\r\n        return QRectF(-60, -20, 120, 40)\r\n    \r\n    def paint(self, painter, option, widget):\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        rect = QRectF(-55, -15, 110, 30)\r\n        \r\n        # Background\r\n        painter.setBrush(QBrush(QColor(40, 40, 40, 230)))\r\n        painter.setPen(Qt.NoPen)\r\n        painter.drawRoundedRect(rect, 6, 6)\r\n        \r\n        # Weight bar background\r\n        bar_rect = QRectF(-45, 2, 90, 8)\r\n        painter.setBrush(QBrush(QColor(80, 80, 80)))\r\n        painter.drawRoundedRect(bar_rect, 3, 3)\r\n        \r\n        # Weight bar fill\r\n        bar_width = abs(self.weight) * 45\r\n        if self.weight >= 0:\r\n            fill_rect = QRectF(0, 2, bar_width, 8)\r\n            color = QColor(50, 205, 50)\r\n        else:\r\n            fill_rect = QRectF(-bar_width, 2, bar_width, 8)\r\n            color = QColor(220, 50, 50)\r\n        \r\n        painter.setBrush(QBrush(color))\r\n        painter.drawRoundedRect(fill_rect, 3, 3)\r\n        \r\n        # Center line\r\n        painter.setPen(QPen(QColor(150, 150, 150), 1))\r\n        painter.drawLine(QPointF(0, 0), QPointF(0, 12))\r\n        \r\n        # Weight text\r\n        painter.setPen(Qt.white)\r\n        painter.setFont(QFont(\"Arial\", 9, QFont.Bold))\r\n        text = f\"{self.weight:+.2f}\"\r\n        painter.drawText(QRectF(-50, -14, 100, 14), Qt.AlignCenter, text)\r\n\r\n\r\nclass NeuronHighlightRing(QGraphicsEllipseItem):\r\n    \"\"\"\r\n    Animated highlight ring for neuron selection/hover states.\r\n    \"\"\"\r\n    \r\n    def __init__(self, center: QPointF, radius: float, \r\n                 color: QColor, parent=None):\r\n        super().__init__(\r\n            center.x() - radius - 5,\r\n            center.y() - radius - 5,\r\n            (radius + 5) * 2,\r\n            (radius + 5) * 2,\r\n            parent\r\n        )\r\n        self.center = center\r\n        self.base_radius = radius\r\n        self.highlight_color = color\r\n        self.pulse_phase = 0.0\r\n        \r\n        self.setPen(Qt.NoPen)\r\n        self.setBrush(Qt.NoBrush)\r\n        self.setZValue(0)\r\n    \r\n    def advance_pulse(self):\r\n        self.pulse_phase += 0.1\r\n        if self.pulse_phase > math.pi * 2:\r\n            self.pulse_phase = 0\r\n        self.update()\r\n    \r\n    def paint(self, painter, option, widget):\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        # Pulsing glow\r\n        pulse = math.sin(self.pulse_phase) * 0.3 + 0.7\r\n        alpha = int(100 * pulse)\r\n        \r\n        glow_color = QColor(\r\n            self.highlight_color.red(),\r\n            self.highlight_color.green(),\r\n            self.highlight_color.blue(),\r\n            alpha\r\n        )\r\n        \r\n        # Outer glow\r\n        gradient = QRadialGradient(self.center, self.base_radius + 15)\r\n        gradient.setColorAt(0.6, glow_color)\r\n        gradient.setColorAt(1.0, QColor(glow_color.red(), glow_color.green(), \r\n                                        glow_color.blue(), 0))\r\n        \r\n        painter.setBrush(QBrush(gradient))\r\n        painter.setPen(Qt.NoPen)\r\n        painter.drawEllipse(self.center, self.base_radius + 15, self.base_radius + 15)\r\n        \r\n        # Ring\r\n        ring_width = 2 + pulse\r\n        painter.setPen(QPen(self.highlight_color, ring_width))\r\n        painter.setBrush(Qt.NoBrush)\r\n        painter.drawEllipse(self.center, self.base_radius + 3, self.base_radius + 3)\r\n\r\n\r\ndef get_weight_color(weight: float) -> QColor:\r\n    \"\"\"Get color for a connection weight.\"\"\"\r\n    if weight >= 0:\r\n        # Green gradient for excitatory\r\n        intensity = min(1.0, weight)\r\n        return QColor(\r\n            int(50 + intensity * 0),\r\n            int(150 + intensity * 55),\r\n            int(50 + intensity * 0)\r\n        )\r\n    else:\r\n        # Red gradient for inhibitory\r\n        intensity = min(1.0, abs(weight))\r\n        return QColor(\r\n            int(150 + intensity * 70),\r\n            int(50 + intensity * 0),\r\n            int(50 + intensity * 10)\r\n        )\r\n\r\n\r\ndef format_neuron_name(name: str) -> str:\r\n    \"\"\"Format a neuron name for display.\"\"\"\r\n    return name.replace('_', ' ').title()\r\n"
  },
  {
    "path": "src/designer_constants.py",
    "content": "from enum import Enum, auto\r\nimport random\r\n\r\n# --- Enums ---\r\nclass NeuronType(Enum):\r\n    INPUT = auto()\r\n    OUTPUT = auto()\r\n    HIDDEN = auto()\r\n    CORE = auto()\r\n    SENSOR = auto()\r\n    CONNECTOR = auto()\r\n\r\n# --- Visual Constants ---\r\nCORE_NEURON_RING_COLOR = (255, 215, 0)\r\nINPUT_SENSOR_RING_COLOR = (100, 149, 237)\r\nCUSTOM_NEURON_RING_COLOR = (180, 180, 180)\r\nCONNECTOR_COLOR = (0, 0, 000)\r\nPROTECTED_RING_WIDTH = 3\r\nNORMAL_RING_WIDTH = 2\r\nDEFAULT_LAYER_HEIGHT = 120\r\nDEFAULT_LAYER_SPACING = 150\r\n\r\nDEFAULT_COLORS = {\r\n    'core': (150, 150, 220),\r\n    'required': (100, 180, 100),\r\n    'input': (100, 200, 150),\r\n    'output': (220, 150, 150),\r\n    'hidden': (180, 180, 200),\r\n    'sensor': (150, 200, 220),\r\n    'connector': (000, 0, 0)\r\n    \r\n}\r\n\r\nLAYER_COLORS = {\r\n    'input': {'fill': (200, 255, 200, 80), 'border': (150, 220, 150, 120)},\r\n    'output': {'fill': (255, 200, 200, 80), 'border': (220, 150, 150, 120)},\r\n    'hidden': {'fill': (230, 230, 255, 80), 'border': (200, 200, 240, 120)}\r\n}\r\n\r\n# --- Logic Constants ---\r\nCORE_NEURONS = {\r\n    \"hunger\": (127, 81), \"happiness\": (361, 81), \"cleanliness\": (627, 81),\r\n    \"sleepiness\": (840, 81), \"satisfaction\": (271, 380), \"anxiety\": (491, 389),\r\n    \"curiosity\": (701, 386),\r\n}\r\nMANDATORY_SENSOR = {\"can_see_food\": (50, 200)}\r\nREQUIRED_NEURONS = {**CORE_NEURONS, **MANDATORY_SENSOR}\r\nCORE_NEURON_NAMES = list(CORE_NEURONS.keys())\r\nREQUIRED_NEURON_NAMES = CORE_NEURON_NAMES + [\"can_see_food\"]\r\n\r\nINPUT_SENSORS = {\r\n    \"external_stimulus\": (50, 50), \"plant_proximity\": (50, 250),\r\n    \"threat_level\": (50, 350), \"pursuing_food\": (150, 50),\r\n    \"is_sick\": (150, 150), \"is_fleeing\": (150, 250),\r\n    \"is_eating\": (150, 350), \"is_sleeping\": (250, 50),\r\n    \"is_startled\": (250, 150),\r\n}\r\n\r\nBINARY_NEURONS = {\r\n    'can_see_food', 'is_eating', 'is_sleeping', 'is_sick',\r\n    'pursuing_food', 'is_fleeing', 'is_startled', 'external_stimulus'\r\n}\r\n\r\ndef is_core_neuron(name): return name in CORE_NEURONS\r\ndef is_required_neuron(name): return name in REQUIRED_NEURONS\r\ndef is_input_sensor(name): return name in INPUT_SENSORS\r\ndef is_binary_neuron(name): return name in BINARY_NEURONS\r\ndef is_protected_neuron(name): return is_required_neuron(name)\r\ndef get_neuron_category(name):\r\n    if is_core_neuron(name): return 'core'\r\n    elif name == 'can_see_food': return 'required'\r\n    elif is_input_sensor(name): return 'sensor'\r\n    return 'custom'\r\ndef get_missing_required(existing): return [n for n in REQUIRED_NEURONS if n not in existing]\r\n\r\n# --- Default Connections ---\r\nDEFAULT_SENSOR_CONNECTIONS = {\r\n    'can_see_food': [('hunger', 0.4, 1.0), ('happiness', 0.2, 0.5), ('satisfaction', 0.15, 0.2)],\r\n    'external_stimulus': [('curiosity', 0.35, 1.0), ('anxiety', 0.15, 0.3)],\r\n    'threat_level': [('anxiety', 0.6, 1.0), ('happiness', -0.3, 0.7), ('curiosity', -0.2, 0.4)],\r\n    'plant_proximity': [('happiness', 0.2, 1.0), ('curiosity', 0.15, 0.5)],\r\n    'is_sick': [('happiness', -0.5, 1.0), ('anxiety', 0.4, 0.8), ('sleepiness', 0.3, 0.6)],\r\n    'is_fleeing': [('anxiety', 0.4, 1.0), ('curiosity', -0.3, 0.7)],\r\n    'is_eating': [('satisfaction', 0.5, 1.0), ('happiness', 0.3, 0.8), ('hunger', -0.4, 1.0)],\r\n    'is_sleeping': [('sleepiness', -0.5, 1.0), ('anxiety', -0.2, 0.6)],\r\n    'is_startled': [('anxiety', 0.5, 1.0), ('curiosity', 0.2, 0.4)],\r\n    'pursuing_food': [('hunger', 0.2, 1.0), ('curiosity', 0.25, 0.5)],\r\n}\r\n\r\ndef get_default_connections_for_sensor(sensor_name: str) -> list:\r\n    if sensor_name not in DEFAULT_SENSOR_CONNECTIONS:\r\n        return []\r\n    connections = []\r\n    for target, weight, probability in DEFAULT_SENSOR_CONNECTIONS[sensor_name]:\r\n        if random.random() < probability:\r\n            connections.append((target, weight))\r\n    return connections"
  },
  {
    "path": "src/designer_core.py",
    "content": "import json\r\nimport random\r\nimport time\r\nimport math\r\nfrom typing import Dict, List, Optional, Tuple, Set\r\nfrom dataclasses import dataclass\r\n\r\nfrom .designer_constants import (\r\n    NeuronType, DEFAULT_COLORS, REQUIRED_NEURONS, CORE_NEURONS,\r\n    INPUT_SENSORS, is_core_neuron, is_required_neuron, is_input_sensor,\r\n    is_binary_neuron, get_neuron_category, get_default_connections_for_sensor\r\n)\r\n\r\n@dataclass\r\nclass DesignerLayer:\r\n    name: str\r\n    layer_type: NeuronType\r\n    y_position: float\r\n    color: Tuple[int, int, int, int] = (200, 200, 220, 80)\r\n    \r\n    def to_dict(self) -> dict:\r\n        return {\r\n            'name': self.name,\r\n            'layer_type': self.layer_type.name.lower(),\r\n            'y_position': self.y_position,\r\n            'color': self.color\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: dict) -> 'DesignerLayer':\r\n        layer_type_str = data.get('layer_type', 'hidden').upper()\r\n        try:\r\n            layer_type = NeuronType[layer_type_str]\r\n        except KeyError:\r\n            layer_type = NeuronType.HIDDEN\r\n        return cls(\r\n            name=data['name'],\r\n            layer_type=layer_type,\r\n            y_position=data['y_position'],\r\n            color=tuple(data.get('color', (200, 200, 220, 80)))\r\n        )\r\n\r\n@dataclass \r\nclass DesignerNeuron:\r\n    name: str\r\n    neuron_type: NeuronType\r\n    position: Tuple[float, float]\r\n    layer_index: int = 0\r\n    color: Tuple[int, int, int] = (150, 150, 220)\r\n    description: str = \"\"\r\n    is_binary: bool = False\r\n    shape: str = 'circle'\r\n    \r\n    def __post_init__(self):\r\n        # 1. CORE neurons (Circle)\r\n        if is_core_neuron(self.name):\r\n            self.neuron_type = NeuronType.CORE\r\n            self.color = DEFAULT_COLORS['core']\r\n            self.shape = 'circle'\r\n            \r\n        # 2. REQUIRED SENSOR (Square)\r\n        elif self.name == 'can_see_food':\r\n            self.neuron_type = NeuronType.SENSOR\r\n            self.color = DEFAULT_COLORS.get('required', (100, 180, 100))\r\n            self.is_binary = True\r\n            self.shape = 'square'\r\n            \r\n        # 3. CONNECTORS (Hexagon)\r\n        elif self.name.startswith('connector_') or self.neuron_type == NeuronType.CONNECTOR:\r\n            self.neuron_type = NeuronType.CONNECTOR\r\n            self.color = DEFAULT_COLORS['connector']\r\n            self.shape = 'hexagon'\r\n            \r\n        # 4. NEUROGENESIS TYPES (Shapes)\r\n        elif self.name.startswith('novelty_'):\r\n            self.shape = 'diamond'\r\n            # Typically yellowish\r\n        elif self.name.startswith('stress_'):\r\n            self.shape = 'square'\r\n            # Typically reddish\r\n        elif self.name.startswith('reward_'):\r\n            self.shape = 'triangle'\r\n            # Typically greenish or blueish\r\n            \r\n        # 5. INPUT SENSORS (Square)\r\n        elif is_input_sensor(self.name):\r\n            self.neuron_type = NeuronType.SENSOR\r\n            self.color = DEFAULT_COLORS['sensor']\r\n            self.is_binary = is_binary_neuron(self.name)\r\n            self.shape = 'square'\r\n\r\n    @property\r\n    def is_core(self) -> bool: return is_core_neuron(self.name)\r\n    @property\r\n    def is_required(self) -> bool: return is_required_neuron(self.name)\r\n    @property\r\n    def is_sensor(self) -> bool: \r\n        return self.neuron_type == NeuronType.SENSOR or self.name == 'can_see_food'\r\n    @property\r\n    def is_protected(self) -> bool: return self.is_required\r\n    @property\r\n    def category(self) -> str: return get_neuron_category(self.name)\r\n    \r\n    def to_dict(self) -> dict:\r\n        return {\r\n            'name': self.name,\r\n            'neuron_type': self.neuron_type.name.lower(),\r\n            'position': list(self.position),\r\n            'layer_index': self.layer_index,\r\n            'color': list(self.color),\r\n            'description': self.description,\r\n            'is_binary': self.is_binary,\r\n            'category': self.category,\r\n            'shape': self.shape\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: dict) -> 'DesignerNeuron':\r\n        type_str = data.get('neuron_type', 'hidden').upper()\r\n        try:\r\n            neuron_type = NeuronType[type_str]\r\n        except KeyError:\r\n            neuron_type = NeuronType.HIDDEN\r\n        \r\n        pos = data.get('position', (0, 0))\r\n        try:\r\n            if isinstance(pos, (list, tuple)) and len(pos) == 2:\r\n                x, y = pos\r\n                if isinstance(x, (int, float)) and isinstance(y, (int, float)):\r\n                    position = tuple(pos)\r\n                else:\r\n                    position = (0, 0) \r\n            else:\r\n                position = (0, 0)\r\n        except (TypeError, ValueError):\r\n            position = (0, 0)\r\n        \r\n        return cls(\r\n            name=data['name'],\r\n            neuron_type=neuron_type,\r\n            position=position,\r\n            layer_index=data.get('layer_index', 0),\r\n            color=tuple(data.get('color', (150, 150, 220))),\r\n            description=data.get('description', ''),\r\n            is_binary=data.get('is_binary', False),\r\n            shape=data.get('shape', 'circle')\r\n        )\r\n\r\n# ... (rest of designer_core.py is compatible as provided in prompt) ...\r\n@dataclass\r\nclass DesignerConnection:\r\n    source: str\r\n    target: str\r\n    weight: float = 0.5\r\n    \r\n    def to_dict(self) -> dict:\r\n        return {'source': self.source, 'target': self.target, 'weight': self.weight}\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: dict) -> 'DesignerConnection':\r\n        return cls(source=data['source'], target=data['target'], weight=data.get('weight', 0.5))\r\n\r\nclass BrainDesign:\r\n    def __init__(self):\r\n        self.neurons: Dict[str, DesignerNeuron] = {}\r\n        self.connections: List[DesignerConnection] = []\r\n        self.layers: List[DesignerLayer] = []\r\n        self.output_bindings: List[Dict] = []\r\n        self.metadata: Dict = {\r\n            'name': 'Untitled', 'description': '', 'author': '',\r\n            'version': '1.0', 'created': '', 'modified': ''\r\n        }\r\n        self._next_custom_neuron_x = 400\r\n        self._next_custom_neuron_y = 200\r\n\r\n    def remove_optional_sensors(self) -> int:\r\n        sensors_to_remove = []\r\n        for name, neuron in self.neurons.items():\r\n            if neuron.is_sensor and name != 'can_see_food':\r\n                sensors_to_remove.append(name)\r\n        \r\n        removed_count = 0\r\n        for name in sensors_to_remove:\r\n            if self.remove_neuron(name)[0]:\r\n                removed_count += 1\r\n        return removed_count\r\n    \r\n    def _validate_and_fix_position(self, position: any) -> Tuple[float, float]:\r\n        if position is None:\r\n            return self._generate_custom_position()\r\n        try:\r\n            if not isinstance(position, (tuple, list)) or len(position) != 2:\r\n                return self._generate_custom_position()\r\n            x, y = position\r\n            if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):\r\n                return self._generate_custom_position()\r\n            if x is None or y is None:\r\n                return self._generate_custom_position()\r\n            return (float(x), float(y))\r\n        except (TypeError, ValueError):\r\n            return self._generate_custom_position()\r\n    \r\n    def _generate_custom_position(self) -> Tuple[float, float]:\r\n        pos = (self._next_custom_neuron_x, self._next_custom_neuron_y)\r\n        self._next_custom_neuron_x += 120\r\n        if self._next_custom_neuron_x > 600:\r\n            self._next_custom_neuron_x = 400\r\n            self._next_custom_neuron_y += 100\r\n        return pos\r\n    \r\n    def add_neuron(self, neuron: DesignerNeuron) -> bool:\r\n        if neuron.name in self.neurons: return False\r\n        neuron.position = self._validate_and_fix_position(neuron.position)\r\n        self.neurons[neuron.name] = neuron\r\n        return True\r\n    \r\n    def remove_neuron(self, name: str) -> Tuple[bool, str]:\r\n        if name not in self.neurons: return False, f\"Neuron '{name}' not found\"\r\n        if is_required_neuron(name):\r\n            return False, f\"Cannot delete required neuron '{name}'\"\r\n        del self.neurons[name]\r\n        self.connections = [c for c in self.connections if c.source != name and c.target != name]\r\n        return True, f\"Removed neuron '{name}'\"\r\n    \r\n    def rename_neuron(self, old_name: str, new_name: str) -> Tuple[bool, str]:\r\n        if old_name not in self.neurons: return False, f\"Neuron '{old_name}' not found\"\r\n        if is_required_neuron(old_name): return False, f\"Cannot rename required neuron '{old_name}'\"\r\n        if new_name in self.neurons: return False, f\"Neuron '{new_name}' already exists\"\r\n        neuron = self.neurons.pop(old_name)\r\n        neuron.name = new_name\r\n        self.neurons[new_name] = neuron\r\n        for conn in self.connections:\r\n            if conn.source == old_name: conn.source = new_name\r\n            if conn.target == old_name: conn.target = new_name\r\n        return True, f\"Renamed '{old_name}' to '{new_name}'\"\r\n    \r\n    def get_neuron(self, name: str) -> Optional[DesignerNeuron]:\r\n        return self.neurons.get(name)\r\n    \r\n    def add_connection(self, source: str, target: str, weight: float = 0.5) -> bool:\r\n        if source not in self.neurons or target not in self.neurons: return False\r\n        for conn in self.connections:\r\n            if conn.source == source and conn.target == target:\r\n                conn.weight = weight\r\n                return True\r\n        self.connections.append(DesignerConnection(source, target, weight))\r\n        return True\r\n    \r\n    def remove_connection(self, source: str, target: str) -> bool:\r\n        for i, conn in enumerate(self.connections):\r\n            if conn.source == source and conn.target == target:\r\n                self.connections.pop(i)\r\n                return True\r\n        return False\r\n    \r\n    def get_connection(self, source: str, target: str) -> Optional[DesignerConnection]:\r\n        for conn in self.connections:\r\n            if conn.source == source and conn.target == target: return conn\r\n        return None\r\n    \r\n    def add_layer(self, layer: DesignerLayer) -> bool:\r\n        self.layers.append(layer)\r\n        self.layers.sort(key=lambda l: l.y_position)\r\n        return True\r\n    \r\n    def remove_layer(self, index: int) -> bool:\r\n        if 0 <= index < len(self.layers):\r\n            self.layers.pop(index)\r\n            return True\r\n        return False\r\n\r\n    def get_missing_required_neurons(self) -> List[str]:\r\n        return [name for name in REQUIRED_NEURONS if name not in self.neurons]\r\n    \r\n    def has_all_required_neurons(self) -> bool:\r\n        return len(self.get_missing_required_neurons()) == 0\r\n    \r\n    def add_missing_required_neurons(self) -> int:\r\n        added = 0\r\n        for name, pos in REQUIRED_NEURONS.items():\r\n            if name not in self.neurons:\r\n                ntype = NeuronType.SENSOR if name == 'can_see_food' else NeuronType.CORE\r\n                desc = \"Required neuron\"\r\n                self.add_neuron(DesignerNeuron(name=name, neuron_type=ntype, position=pos, description=desc))\r\n                added += 1\r\n        return added\r\n\r\n    def add_missing_core_neurons(self) -> int:\r\n        added = 0\r\n        for name, pos in CORE_NEURONS.items():\r\n            if name not in self.neurons:\r\n                self.add_neuron(DesignerNeuron(\r\n                    name=name, neuron_type=NeuronType.CORE, position=pos, description=\"Core neuron\"))\r\n                added += 1\r\n        return added\r\n    \r\n    def get_orphan_neurons(self) -> List[str]:\r\n        if not self.connections:\r\n            return [n for n in self.neurons if not is_required_neuron(n)]\r\n        connected = set()\r\n        for conn in self.connections:\r\n            connected.add(conn.source)\r\n            connected.add(conn.target)\r\n        return [name for name in self.neurons if name not in connected and not is_required_neuron(name)]\r\n    \r\n    def get_island_neurons(self) -> List[Set[str]]:\r\n        if not self.neurons: return []\r\n        adj = {name: set() for name in self.neurons}\r\n        for conn in self.connections:\r\n            if conn.source in adj and conn.target in adj:\r\n                adj[conn.source].add(conn.target)\r\n                adj[conn.target].add(conn.source)\r\n        \r\n        visited = set()\r\n        components = []\r\n        for start in self.neurons:\r\n            if start in visited: continue\r\n            component = set()\r\n            queue = [start]\r\n            while queue:\r\n                node = queue.pop(0)\r\n                if node in visited: continue\r\n                visited.add(node)\r\n                component.add(node)\r\n                for neighbor in adj[node]:\r\n                    if neighbor not in visited: queue.append(neighbor)\r\n            components.append(component)\r\n            \r\n        main_component = None\r\n        for comp in components:\r\n            if any(is_required_neuron(n) for n in comp):\r\n                main_component = comp\r\n                break\r\n        return [comp for comp in components if comp != main_component and len(comp) > 0]\r\n    \r\n    def auto_fix_connectivity(self) -> Tuple[int, List[str]]:\r\n        actions = []\r\n        connections_created = 0\r\n        \r\n        def get_suggested_weight(source_cat: str, target_cat: str, rng: random.Random) -> float:\r\n            tendencies = {\r\n                ('sensor', 'core'): (0.3, 0.6), ('sensor', 'sensor'): (-0.2, 0.4),\r\n                ('custom', 'core'): (-0.4, 0.8), ('custom', 'custom'): (-0.5, 0.5),\r\n                ('core', 'core'): (-0.3, 0.7),\r\n            }\r\n            key = (source_cat, target_cat)\r\n            if key not in tendencies: key = (target_cat, source_cat)\r\n            if key not in tendencies: key = ('custom', 'custom')\r\n            min_w, max_w = tendencies[key]\r\n            return rng.uniform(min_w, max_w)\r\n            \r\n        def find_nearest_core(neuron_pos: Tuple[float, float]) -> Optional[str]:\r\n            core_neurons = [n for n in self.neurons.values() if n.is_core]\r\n            if not core_neurons: return None\r\n            return min(core_neurons, key=lambda n: math.hypot(neuron_pos[0] - n.position[0], neuron_pos[1] - n.position[1])).name\r\n\r\n        fix_rng = random.Random()\r\n        fix_rng.seed(time.time() + hash(tuple(sorted(self.neurons.keys()))) % 10000)\r\n        \r\n        for orphan_name in self.get_orphan_neurons():\r\n            orphan = self.neurons[orphan_name]\r\n            target_name = find_nearest_core(orphan.position)\r\n            if target_name:\r\n                w1 = get_suggested_weight(orphan.category, self.neurons[target_name].category, fix_rng)\r\n                w2 = get_suggested_weight(self.neurons[target_name].category, orphan.category, fix_rng)\r\n                self.add_connection(orphan_name, target_name, w1)\r\n                self.add_connection(target_name, orphan_name, w2)\r\n                connections_created += 2\r\n                actions.append(f\"Connected orphan '{orphan_name}' ↔ '{target_name}'\")\r\n\r\n        main_network = set()\r\n        for conn in self.connections:\r\n            if is_core_neuron(conn.source) or is_core_neuron(conn.target):\r\n                main_network.add(conn.source)\r\n                main_network.add(conn.target)\r\n        if not main_network:\r\n             main_network = set(self.neurons.keys()) \r\n\r\n        for island in self.get_island_neurons():\r\n            island_list = list(island)\r\n            island_center = (\r\n                sum(self.neurons[n].position[0] for n in island_list) / len(island_list),\r\n                sum(self.neurons[n].position[1] for n in island_list) / len(island_list)\r\n            )\r\n            nearest_main = find_nearest_core(island_center)\r\n            if nearest_main:\r\n                connect_count = min(2, len(island_list))\r\n                for i in range(connect_count):\r\n                    n = island_list[i % len(island_list)]\r\n                    w = get_suggested_weight(self.neurons[n].category, self.neurons[nearest_main].category, fix_rng)\r\n                    self.add_connection(n, nearest_main, w)\r\n                    connections_created += 1\r\n                    actions.append(f\"Connected island node '{n}' → '{nearest_main}'\")\r\n        \r\n        return connections_created, actions\r\n\r\n    def add_sensor(self, name: str, create_default_connections: bool = True) -> Tuple[bool, str]:\r\n        if name not in INPUT_SENSORS and name != 'can_see_food':\r\n            return False, f\"'{name}' is not a valid input sensor\"\r\n        if name in self.neurons:\r\n            return False, f\"Sensor '{name}' already exists\"\r\n        \r\n        pos = REQUIRED_NEURONS[name] if name == 'can_see_food' else INPUT_SENSORS[name]\r\n        self.add_neuron(DesignerNeuron(name=name, neuron_type=NeuronType.SENSOR, position=pos, is_binary=is_binary_neuron(name)))\r\n        \r\n        conns_made = []\r\n        if create_default_connections:\r\n            for target, weight in get_default_connections_for_sensor(name):\r\n                if target in self.neurons:\r\n                    self.add_connection(name, target, weight)\r\n                    conns_made.append(f\"{name}→{target}\")\r\n        return True, f\"Added sensor '{name}' with connections: {', '.join(conns_made)}\"\r\n\r\n    def add_all_sensors(self) -> int:\r\n        added = 0\r\n        for name in INPUT_SENSORS:\r\n            if self.add_sensor(name)[0]: added += 1\r\n        return added\r\n    \r\n    def get_sensors_in_design(self) -> List[str]:\r\n        return [name for name, neuron in self.neurons.items() if neuron.is_sensor]\r\n    \r\n    def validate(self, auto_fix: bool = True) -> Tuple[bool, List[str], int]:\r\n        issues = []\r\n        auto_added = 0\r\n        missing_required = self.get_missing_required_neurons()\r\n        \r\n        if missing_required:\r\n            if auto_fix:\r\n                auto_added = self.add_missing_required_neurons()\r\n                issues.append(f\"Auto-added missing required: {', '.join(missing_required)}\")\r\n            else:\r\n                issues.append(f\"BLOCKING: Missing required: {', '.join(missing_required)}\")\r\n        \r\n        orphans = self.get_orphan_neurons()\r\n        if orphans:\r\n            if auto_fix:\r\n                conns, actions = self.auto_fix_connectivity()\r\n                issues.append(f\"Auto-fixed orphans with {conns} new connections\")\r\n                issues.extend(actions)\r\n                auto_added += conns\r\n            else:\r\n                issues.append(f\"WARNING: Orphan neurons: {', '.join(orphans)}\")\r\n        \r\n        if 'can_see_food' not in self.neurons:\r\n            issues.append(\"WARNING: Missing 'can_see_food' sensor!\")\r\n            \r\n        return not any(\"BLOCKING\" in i for i in issues), issues, auto_added\r\n    \r\n    def get_stats(self) -> Dict:\r\n        return {\r\n            'total_neurons': len(self.neurons),\r\n            'connections': len(self.connections),\r\n            'has_all_required': self.has_all_required_neurons(),\r\n            'missing_required': self.get_missing_required_neurons(),\r\n            'orphan_neurons': self.get_orphan_neurons(),\r\n            'island_groups': len(self.get_island_neurons()),\r\n            'sensors_used': self.get_sensors_in_design(),\r\n            'required_neurons': sum(1 for n in self.neurons.values() if n.is_required),\r\n            'sensor_neurons': sum(1 for n in self.neurons.values() if n.is_sensor and not n.is_required),\r\n            'custom_neurons': sum(1 for n in self.neurons.values() if not n.is_required and not n.is_sensor),\r\n            'layers': len(self.layers)\r\n        }\r\n\r\n    def to_dosidicus_format(self) -> dict:\r\n        neurons = {}\r\n        for name, obj in self.neurons.items():\r\n            neurons[name] = {\r\n                'position': list(obj.position),\r\n                'type': obj.neuron_type.name.lower(),\r\n                'is_binary': obj.is_binary,\r\n                'is_core': obj.is_core,\r\n                'is_sensor': obj.is_sensor,\r\n                'activation': 0.0 if obj.is_binary else (50.0 if obj.is_core else 0.0),\r\n            }\r\n        \r\n        connections = {}\r\n        for c in self.connections:\r\n            key = f\"{c.source}->{c.target}\"\r\n            connections[key] = c.weight\r\n        \r\n        state = {}\r\n        for name, obj in self.neurons.items():\r\n            if obj.is_binary:\r\n                state[name] = False\r\n            elif obj.is_core:\r\n                state[name] = 50.0\r\n            else:\r\n                state[name] = 0.0\r\n        \r\n        return {\r\n            'version': '2.0',\r\n            'format': 'dosidicus',\r\n            'metadata': self.metadata.copy(),\r\n            'neurons': neurons,\r\n            'connections': connections,\r\n            'state': state,\r\n            'neuron_shapes': {n: obj.shape for n, obj in self.neurons.items()}, \r\n            'excluded_neurons': [],\r\n            'output_bindings': self.output_bindings,\r\n            'neuron_positions': {n: tuple(obj.position) for n, obj in self.neurons.items()},\r\n            'weights': {f\"{c.source}|{c.target}\": c.weight for c in self.connections},\r\n            'layer_structure': [l.to_dict() for l in self.layers],\r\n            'neuron_details': {n: obj.to_dict() for n, obj in self.neurons.items()},\r\n            'sensors_used': self.get_sensors_in_design(),\r\n            'required_complete': self.has_all_required_neurons()\r\n        }\r\n    \r\n    def to_designer_format(self) -> dict:\r\n        neurons = {}\r\n        for n in self.neurons.values():\r\n            neurons[n.name] = {\r\n                'name': n.name,\r\n                'position': list(n.position),\r\n                'neuron_type': n.neuron_type.name.lower(),\r\n                'layer_index': n.layer_index,\r\n                'color': list(n.color),\r\n                'description': n.description,\r\n                'is_binary': n.is_binary,\r\n                'category': n.category,\r\n                'activation': 0.0 if n.is_binary else (50.0 if n.is_core else 0.0),\r\n                'shape': n.shape\r\n            }\r\n        \r\n        connections = [c.to_dict() for c in self.connections]\r\n        \r\n        return {\r\n            'version': '2.0',\r\n            'format': 'brain_designer',\r\n            'metadata': self.metadata.copy(),\r\n            'neurons': neurons,\r\n            'connections': connections,\r\n            'layers': [l.to_dict() for l in self.layers],\r\n            'excluded_neurons': [],\r\n            'output_bindings': self.output_bindings,\r\n        }\r\n    \r\n    @classmethod\r\n    def from_designer_format(cls, data: dict) -> 'BrainDesign':\r\n        design = cls()\r\n        design.metadata = data.get('metadata', {})\r\n        for l in data.get('layers', []):\r\n            design.layers.append(DesignerLayer.from_dict(l))\r\n        \r\n        neurons_data = data.get('neurons', [])\r\n        if isinstance(neurons_data, dict):\r\n            for name, n in neurons_data.items():\r\n                if 'name' not in n:\r\n                    n['name'] = name\r\n                neuron = DesignerNeuron.from_dict(n)\r\n                design.add_neuron(neuron)\r\n        else:\r\n            for n in neurons_data:\r\n                neuron = DesignerNeuron.from_dict(n)\r\n                design.add_neuron(neuron)\r\n        \r\n        for c in data.get('connections', []):\r\n            design.connections.append(DesignerConnection.from_dict(c))\r\n        \r\n        design.output_bindings = data.get('output_bindings', [])\r\n        return design\r\n    \r\n    @classmethod\r\n    def from_dosidicus_format(cls, data: dict) -> 'BrainDesign':\r\n        design = cls()\r\n        design.metadata = data.get('metadata', {'name': 'Imported Brain'})\r\n        for l in data.get('layer_structure', []):\r\n            design.layers.append(DesignerLayer.from_dict(l))\r\n        \r\n        neurons_data = data.get('neurons', {})\r\n        shapes_data = data.get('neuron_shapes', {})\r\n        \r\n        if neurons_data and isinstance(neurons_data, dict):\r\n            for name, nd in neurons_data.items():\r\n                pos = nd.get('position', [0, 0])\r\n                ntype_str = nd.get('type', 'hidden').upper()\r\n                try:\r\n                    ntype = NeuronType[ntype_str]\r\n                except KeyError:\r\n                    ntype = NeuronType.CORE if is_core_neuron(name) else (\r\n                        NeuronType.SENSOR if is_input_sensor(name) else NeuronType.HIDDEN\r\n                    )\r\n                \r\n                shape = shapes_data.get(name, 'circle')\r\n                \r\n                neuron = DesignerNeuron(\r\n                    name=name,\r\n                    neuron_type=ntype,\r\n                    position=tuple(pos) if isinstance(pos, list) else pos,\r\n                    is_binary=nd.get('is_binary', False),\r\n                    shape=shape\r\n                )\r\n                design.add_neuron(neuron)\r\n        else:\r\n            details = data.get('neuron_details', {})\r\n            for name, pos in data.get('neuron_positions', {}).items():\r\n                if name in details:\r\n                    neuron = DesignerNeuron.from_dict(details[name])\r\n                    design.add_neuron(neuron)\r\n                else:\r\n                    ntype = NeuronType.CORE if is_core_neuron(name) else (\r\n                        NeuronType.SENSOR if is_input_sensor(name) else NeuronType.HIDDEN\r\n                    )\r\n                    shape = shapes_data.get(name, 'circle')\r\n                    neuron = DesignerNeuron(name=name, neuron_type=ntype, position=tuple(pos), shape=shape)\r\n                    design.add_neuron(neuron)\r\n        \r\n        connections_data = data.get('connections', {})\r\n        if isinstance(connections_data, dict):\r\n            for k, w in connections_data.items():\r\n                if '->' in k:\r\n                    s, t = k.split('->')\r\n                elif '|' in k:\r\n                    s, t = k.split('|')\r\n                else:\r\n                    continue\r\n                design.connections.append(DesignerConnection(s.strip(), t.strip(), w))\r\n        elif isinstance(connections_data, list):\r\n            for c in connections_data:\r\n                if c.get('source') and c.get('target'):\r\n                    design.connections.append(DesignerConnection(\r\n                        c['source'], c['target'], c.get('weight', 0.1)\r\n                    ))\r\n        \r\n        if not design.connections:\r\n            for k, w in data.get('weights', {}).items():\r\n                if '|' in k:\r\n                    s, t = k.split('|')\r\n                    design.connections.append(DesignerConnection(s, t, w))\r\n        \r\n        design.output_bindings = data.get('output_bindings', [])\r\n        return design\r\n        \r\n    def save(self, filepath: str, format: str = 'designer') -> Tuple[bool, str]:\r\n        is_valid, issues, auto_added = self.validate(auto_fix=True)\r\n        if not is_valid: \r\n            return False, \"Save blocked by validation issues.\"\r\n        \r\n        try:\r\n            data = self.to_dosidicus_format() if format == 'dosidicus' else self.to_designer_format()\r\n            with open(filepath, 'w') as f: json.dump(data, f, indent=2)\r\n            \r\n            msg = f\"Saved successfully ({len(self.neurons)} neurons)\"\r\n            if auto_added > 0: msg += f\"\\n(Applied {auto_added} auto-fixes)\"\r\n            return True, msg\r\n        except Exception as e:\r\n            return False, f\"Error saving: {e}\"\r\n    \r\n    @classmethod\r\n    def load(cls, filepath: str) -> 'BrainDesign':\r\n        with open(filepath, 'r') as f: data = json.load(f)\r\n        return cls.from_dosidicus_format(data) if data.get('format') == 'dosidicus' else cls.from_designer_format(data)\r\n\r\n    def export_dosidicus(self, filepath):\r\n        return self.save(filepath, format='dosidicus')"
  },
  {
    "path": "src/designer_dialogs.py",
    "content": "\"\"\"\r\nSparse Network Generation Dialog\r\n\r\nProvides UI for generating random sparse networks with various presets.\r\n\"\"\"\r\n\r\nfrom PyQt5.QtWidgets import (\r\n    QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QPushButton,\r\n    QComboBox, QDoubleSpinBox, QCheckBox, QTextEdit, QFrame, QProgressBar,\r\n    QSizePolicy\r\n)\r\nfrom PyQt5.QtCore import Qt, QTimer\r\nfrom PyQt5.QtGui import QFont\r\n\r\nfrom .designer_network_generator import SparseNetworkGenerator\r\n\r\n\r\nclass SparseNetworkDialog(QDialog):\r\n    \"\"\"Dialog for generating sparse neural networks.\"\"\"\r\n    \r\n    def __init__(self, design, parent=None):\r\n        super().__init__(parent)\r\n        self.design = design\r\n        self.generator = SparseNetworkGenerator()\r\n        self.result_connections = []\r\n        self.result_actions = []\r\n        \r\n        self.setWindowTitle(\"🎲 Generate Sparse Network\")\r\n        self.setMinimumWidth(500)\r\n        self.setMinimumHeight(550)\r\n        \r\n        self.setup_ui()\r\n        self.load_preset('balanced')\r\n    \r\n    def setup_ui(self):\r\n        layout = QVBoxLayout(self)\r\n        \r\n        # Header\r\n        header = QLabel(\"Generate Random Neural Connections\")\r\n        header.setFont(QFont(\"Arial\", 12, QFont.Bold))\r\n        header.setAlignment(Qt.AlignCenter)\r\n        layout.addWidget(header)\r\n        \r\n        desc = QLabel(\r\n            \"Creates biologically-inspired connections between the 8 required neurons.\\n\"\r\n            \"Each generation is unique due to random noise.\"\r\n        )\r\n        desc.setStyleSheet(\"color: #666; font-size: 10pt;\")\r\n        desc.setAlignment(Qt.AlignCenter)\r\n        desc.setWordWrap(True)\r\n        layout.addWidget(desc)\r\n        \r\n        layout.addSpacing(10)\r\n        \r\n        # Preset selection\r\n        preset_group = QGroupBox(\"Style Preset\")\r\n        preset_layout = QHBoxLayout(preset_group)\r\n        \r\n        self.preset_combo = QComboBox()\r\n        presets = self.generator.get_preset_styles()\r\n        for key, info in presets.items():\r\n            self.preset_combo.addItem(info['name'], key)\r\n        self.preset_combo.currentIndexChanged.connect(self.on_preset_changed)\r\n        preset_layout.addWidget(self.preset_combo)\r\n        \r\n        self.preset_desc = QLabel()\r\n        self.preset_desc.setStyleSheet(\"color: #888; font-style: italic;\")\r\n        preset_layout.addWidget(self.preset_desc, stretch=1)\r\n        \r\n        layout.addWidget(preset_group)\r\n        \r\n        # Advanced options\r\n        advanced_group = QGroupBox(\"Fine Tuning\")\r\n        advanced_layout = QVBoxLayout(advanced_group)\r\n        \r\n        # Density & Noise\r\n        row1 = QHBoxLayout()\r\n        row1.addWidget(QLabel(\"Density:\"))\r\n        self.density_spin = QDoubleSpinBox()\r\n        self.density_spin.setRange(0.2, 2.0)\r\n        self.density_spin.setSingleStep(0.1)\r\n        self.density_spin.setDecimals(1)\r\n        self.density_spin.setValue(1.0)\r\n        self.density_spin.setToolTip(\"Lower = fewer connections, Higher = more connections\")\r\n        row1.addWidget(self.density_spin)\r\n        \r\n        row1.addSpacing(20)\r\n        \r\n        row1.addWidget(QLabel(\"Weight Noise:\"))\r\n        self.noise_spin = QDoubleSpinBox()\r\n        self.noise_spin.setRange(0.1, 3.0)\r\n        self.noise_spin.setSingleStep(0.1)\r\n        self.noise_spin.setDecimals(1)\r\n        self.noise_spin.setValue(1.0)\r\n        self.noise_spin.setToolTip(\"How much randomness in connection weights\")\r\n        row1.addWidget(self.noise_spin)\r\n        advanced_layout.addLayout(row1)\r\n\r\n        # Variance & Sensors (NEW)\r\n        row2 = QHBoxLayout()\r\n        row2.addWidget(QLabel(\"Pos Variance:\"))\r\n        self.variance_spin = QDoubleSpinBox()\r\n        self.variance_spin.setRange(0.0, 1.0)\r\n        self.variance_spin.setSingleStep(0.1)\r\n        self.variance_spin.setDecimals(2)\r\n        self.variance_spin.setValue(0.0)\r\n        self.variance_spin.setToolTip(\"Jitter neuron positions (0.0 = fixed, 0.5 = chaotic)\")\r\n        row2.addWidget(self.variance_spin)\r\n        \r\n        row2.addSpacing(20)\r\n        \r\n        row2.addWidget(QLabel(\"Sensor Prob:\"))\r\n        self.sensor_prob_spin = QDoubleSpinBox()\r\n        self.sensor_prob_spin.setRange(0.0, 1.0)\r\n        self.sensor_prob_spin.setSingleStep(0.1)\r\n        self.sensor_prob_spin.setDecimals(2)\r\n        self.sensor_prob_spin.setValue(0.0)\r\n        self.sensor_prob_spin.setToolTip(\"Probability of adding random extra sensors\")\r\n        row2.addWidget(self.sensor_prob_spin)\r\n        advanced_layout.addLayout(row2)\r\n        \r\n        # Options row\r\n        options_row = QHBoxLayout()\r\n        \r\n        self.feedback_check = QCheckBox(\"Include feedback loops\")\r\n        self.feedback_check.setChecked(True)\r\n        self.feedback_check.setToolTip(\"Allow bidirectional connections where biologically plausible\")\r\n        options_row.addWidget(self.feedback_check)\r\n        \r\n        self.clear_check = QCheckBox(\"Clear existing connections\")\r\n        self.clear_check.setChecked(True)\r\n        self.clear_check.setToolTip(\"Remove all existing connections before generating\")\r\n        options_row.addWidget(self.clear_check)\r\n        \r\n        options_row.addStretch()\r\n        advanced_layout.addLayout(options_row)\r\n        \r\n        layout.addWidget(advanced_group)\r\n        \r\n        # Preview area\r\n        preview_group = QGroupBox(\"Preview\")\r\n        preview_layout = QVBoxLayout(preview_group)\r\n        \r\n        self.preview_text = QTextEdit()\r\n        self.preview_text.setReadOnly(True)\r\n        self.preview_text.setMaximumHeight(150)\r\n        self.preview_text.setStyleSheet(\"\"\"\r\n            QTextEdit {\r\n                font-family: monospace;\r\n                font-size: 9pt;\r\n                background-color: #f5f5f5;\r\n            }\r\n        \"\"\")\r\n        preview_layout.addWidget(self.preview_text)\r\n        \r\n        preview_btn_row = QHBoxLayout()\r\n        self.preview_btn = QPushButton(\"🔄 Preview Generation\")\r\n        self.preview_btn.clicked.connect(self.generate_preview)\r\n        preview_btn_row.addWidget(self.preview_btn)\r\n        preview_btn_row.addStretch()\r\n        \r\n        self.count_label = QLabel()\r\n        self.count_label.setStyleSheet(\"color: #666;\")\r\n        preview_btn_row.addWidget(self.count_label)\r\n        \r\n        preview_layout.addLayout(preview_btn_row)\r\n        layout.addWidget(preview_group)\r\n        \r\n        # Separator\r\n        line = QFrame()\r\n        line.setFrameShape(QFrame.HLine)\r\n        line.setFrameShadow(QFrame.Sunken)\r\n        layout.addWidget(line)\r\n        \r\n        # Buttons\r\n        btn_layout = QHBoxLayout()\r\n        \r\n        self.cancel_btn = QPushButton(\"Cancel\")\r\n        self.cancel_btn.clicked.connect(self.reject)\r\n        btn_layout.addWidget(self.cancel_btn)\r\n        \r\n        btn_layout.addStretch()\r\n        \r\n        self.apply_btn = QPushButton(\"✨ Generate && Apply\")\r\n        self.apply_btn.setStyleSheet(\"\"\"\r\n            QPushButton {\r\n                background-color: #4CAF50;\r\n                color: white;\r\n                font-weight: bold;\r\n                padding: 8px 20px;\r\n            }\r\n            QPushButton:hover {\r\n                background-color: #45a049;\r\n            }\r\n        \"\"\")\r\n        self.apply_btn.clicked.connect(self.apply_generation)\r\n        btn_layout.addWidget(self.apply_btn)\r\n        \r\n        layout.addLayout(btn_layout)\r\n        \r\n        # Initial preview\r\n        QTimer.singleShot(100, self.generate_preview)\r\n    \r\n    def on_preset_changed(self, index):\r\n        key = self.preset_combo.currentData()\r\n        self.load_preset(key)\r\n    \r\n    def load_preset(self, key):\r\n        presets = self.generator.get_preset_styles()\r\n        if key not in presets:\r\n            return\r\n        \r\n        preset = presets[key]\r\n        self.preset_desc.setText(preset['description'])\r\n        \r\n        # Block signals during update\r\n        self.density_spin.blockSignals(True)\r\n        self.noise_spin.blockSignals(True)\r\n        self.variance_spin.blockSignals(True)\r\n        self.sensor_prob_spin.blockSignals(True)\r\n        self.feedback_check.blockSignals(True)\r\n        \r\n        self.density_spin.setValue(preset['density'])\r\n        self.noise_spin.setValue(preset.get('weight_noise', 1.0))\r\n        self.variance_spin.setValue(preset.get('position_variance', 0.0))\r\n        self.sensor_prob_spin.setValue(preset.get('sensor_probability', 0.0))\r\n        self.feedback_check.setChecked(preset.get('include_feedback', True))\r\n        \r\n        self.density_spin.blockSignals(False)\r\n        self.noise_spin.blockSignals(False)\r\n        self.variance_spin.blockSignals(False)\r\n        self.sensor_prob_spin.blockSignals(False)\r\n        self.feedback_check.blockSignals(False)\r\n        \r\n        self.generate_preview()\r\n    \r\n    def generate_preview(self):\r\n        \"\"\"Generate a preview without applying.\"\"\"\r\n        # Create fresh generator (no seed for variety)\r\n        gen = SparseNetworkGenerator()\r\n        \r\n        # Pass the weight_noise parameter\r\n        connections = gen.generate_connections(\r\n            density=self.density_spin.value(),\r\n            include_feedback_loops=self.feedback_check.isChecked(),\r\n            weight_noise=self.noise_spin.value()\r\n        )\r\n        \r\n        self.result_connections = connections\r\n        \r\n        # Format preview\r\n        lines = []\r\n        excitatory = 0\r\n        inhibitory = 0\r\n        \r\n        for source, target, weight in connections:\r\n            sign = \"+\" if weight > 0 else \"\"\r\n            arrow = \"→\" if weight > 0 else \"⊣\"\r\n            lines.append(f\"  {source:15} {arrow} {target:15} ({sign}{weight:.3f})\")\r\n            if weight > 0:\r\n                excitatory += 1\r\n            else:\r\n                inhibitory += 1\r\n        \r\n        # Add note about sensors/variance if active\r\n        if self.sensor_prob_spin.value() > 0:\r\n            lines.insert(0, f\"NOTE: Will attempt to add random sensors (Prob: {self.sensor_prob_spin.value()})\")\r\n        if self.variance_spin.value() > 0:\r\n            lines.insert(0, f\"NOTE: Will randomly perturb positions (Var: {self.variance_spin.value()})\")\r\n        \r\n        if lines:\r\n            self.preview_text.setPlainText(\"\\n\".join(lines))\r\n        else:\r\n            self.preview_text.setPlainText(\"No connections would be created with these settings.\")\r\n        \r\n        # Update count\r\n        total = len(connections)\r\n        self.count_label.setText(\r\n            f\"{total} connections ({excitatory} excitatory, {inhibitory} inhibitory)\"\r\n        )\r\n    \r\n    def apply_generation(self):\r\n        \"\"\"Apply the generated network to the design.\"\"\"\r\n        gen = SparseNetworkGenerator()\r\n        \r\n        # Now passing all new arguments to fix the TypeError\r\n        count, actions = gen.generate_for_design(\r\n            self.design,\r\n            clear_existing=self.clear_check.isChecked(),\r\n            density=self.density_spin.value(),\r\n            include_feedback=self.feedback_check.isChecked(),\r\n            weight_noise=self.noise_spin.value(),\r\n            position_variance=self.variance_spin.value(),\r\n            sensor_probability=self.sensor_prob_spin.value()\r\n        )\r\n        \r\n        self.result_actions = actions\r\n        self.accept()\r\n    \r\n    def get_result_summary(self) -> str:\r\n        \"\"\"Get a summary of what was generated.\"\"\"\r\n        return f\"Created {len(self.result_connections)} connections\"\r\n\r\n\r\nclass ActivationEditorDialog(QDialog):\r\n    \"\"\"Dialog for viewing and editing neuron activation values.\"\"\"\r\n    \r\n    def __init__(self, neuron_name: str, current_activation: float, \r\n                 is_binary: bool = False, parent=None):\r\n        super().__init__(parent)\r\n        self.neuron_name = neuron_name\r\n        self.is_binary = is_binary\r\n        self.result_value = current_activation\r\n        \r\n        self.setWindowTitle(f\"Activation: {neuron_name}\")\r\n        self.setMinimumWidth(300)\r\n        \r\n        self.setup_ui(current_activation)\r\n    \r\n    def setup_ui(self, current_value):\r\n        layout = QVBoxLayout(self)\r\n        \r\n        # Header\r\n        name_label = QLabel(self.neuron_name.replace('_', ' ').title())\r\n        name_label.setFont(QFont(\"Arial\", 11, QFont.Bold))\r\n        name_label.setAlignment(Qt.AlignCenter)\r\n        layout.addWidget(name_label)\r\n        \r\n        if self.is_binary:\r\n            # Binary neuron: on/off toggle\r\n            info = QLabel(\"Binary neuron (On/Off)\")\r\n            info.setStyleSheet(\"color: #666;\")\r\n            info.setAlignment(Qt.AlignCenter)\r\n            layout.addWidget(info)\r\n            \r\n            btn_row = QHBoxLayout()\r\n            \r\n            self.off_btn = QPushButton(\"OFF (0)\")\r\n            self.off_btn.setCheckable(True)\r\n            self.off_btn.setChecked(current_value < 50)\r\n            self.off_btn.clicked.connect(lambda: self.set_binary(False))\r\n            btn_row.addWidget(self.off_btn)\r\n            \r\n            self.on_btn = QPushButton(\"ON (100)\")\r\n            self.on_btn.setCheckable(True)\r\n            self.on_btn.setChecked(current_value >= 50)\r\n            self.on_btn.clicked.connect(lambda: self.set_binary(True))\r\n            btn_row.addWidget(self.on_btn)\r\n            \r\n            layout.addLayout(btn_row)\r\n            \r\n        else:\r\n            # Continuous neuron: spinner\r\n            info = QLabel(\"Activation (0-100)\")\r\n            info.setStyleSheet(\"color: #666;\")\r\n            info.setAlignment(Qt.AlignCenter)\r\n            layout.addWidget(info)\r\n            \r\n            spin_row = QHBoxLayout()\r\n            spin_row.addStretch()\r\n            \r\n            self.value_spin = QDoubleSpinBox()\r\n            self.value_spin.setRange(0, 100)\r\n            self.value_spin.setSingleStep(5)\r\n            self.value_spin.setDecimals(1)\r\n            self.value_spin.setValue(current_value)\r\n            self.value_spin.setMinimumWidth(100)\r\n            spin_row.addWidget(self.value_spin)\r\n            \r\n            spin_row.addStretch()\r\n            layout.addLayout(spin_row)\r\n            \r\n            # Quick buttons\r\n            quick_row = QHBoxLayout()\r\n            for val, label in [(0, \"Min\"), (25, \"Low\"), (50, \"Mid\"), (75, \"High\"), (100, \"Max\")]:\r\n                btn = QPushButton(label)\r\n                btn.setMaximumWidth(50)\r\n                btn.clicked.connect(lambda checked, v=val: self.value_spin.setValue(v))\r\n                quick_row.addWidget(btn)\r\n            layout.addLayout(quick_row)\r\n        \r\n        layout.addSpacing(15)\r\n        \r\n        # Buttons\r\n        btn_layout = QHBoxLayout()\r\n        \r\n        cancel_btn = QPushButton(\"Cancel\")\r\n        cancel_btn.clicked.connect(self.reject)\r\n        btn_layout.addWidget(cancel_btn)\r\n        \r\n        btn_layout.addStretch()\r\n        \r\n        ok_btn = QPushButton(\"OK\")\r\n        ok_btn.clicked.connect(self.accept_value)\r\n        ok_btn.setDefault(True)\r\n        btn_layout.addWidget(ok_btn)\r\n        \r\n        layout.addLayout(btn_layout)\r\n    \r\n    def set_binary(self, is_on: bool):\r\n        self.off_btn.setChecked(not is_on)\r\n        self.on_btn.setChecked(is_on)\r\n        self.result_value = 100.0 if is_on else 0.0\r\n    \r\n    def accept_value(self):\r\n        if self.is_binary:\r\n            self.result_value = 100.0 if self.on_btn.isChecked() else 0.0\r\n        else:\r\n            self.result_value = self.value_spin.value()\r\n        self.accept()\r\n    \r\n    def get_value(self) -> float:\r\n        return self.result_value"
  },
  {
    "path": "src/designer_logging.py",
    "content": "\"\"\"\r\nDesigner Logging - Centralized logging for Brain Designer\r\n\r\nProvides logging utilities that can be enabled/disabled based on debug mode.\r\n\"\"\"\r\n\r\nimport logging\r\nimport os\r\nimport sys\r\nimport traceback\r\nfrom datetime import datetime\r\nfrom contextlib import contextmanager\r\n\r\n\r\n# Global logger instance\r\n_logger = None\r\n_logging_enabled = False\r\n\r\n\r\nclass NullHandler(logging.Handler):\r\n    \"\"\"A handler that does nothing - used when logging is disabled.\"\"\"\r\n    def emit(self, record):\r\n        pass\r\n\r\n\r\nclass CrashReporter:\r\n    \"\"\"Handles crash reporting and error dialogs.\"\"\"\r\n    \r\n    def __init__(self, enable_logging: bool = False):\r\n        self._error_dialog_callback = None\r\n        self._logging_enabled = enable_logging\r\n    \r\n    def set_error_dialog_callback(self, callback):\r\n        \"\"\"Set the callback function for showing error dialogs.\"\"\"\r\n        self._error_dialog_callback = callback\r\n    \r\n    def report_crash(self, exc_type, exc_value, exc_tb):\r\n        \"\"\"Report a crash - log it and optionally show dialog.\"\"\"\r\n        error_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))\r\n        \r\n        if self._logging_enabled:\r\n            logger = get_logger()\r\n            logger.critical(f\"Crash reported:\\n{error_msg}\")\r\n        \r\n        if self._error_dialog_callback:\r\n            self._error_dialog_callback(\r\n                \"Critical Error\",\r\n                f\"An unexpected error occurred:\\n\\n{exc_value}\"\r\n            )\r\n\r\n\r\ndef get_log_directory() -> str:\r\n    \"\"\"Get the directory for log files.\"\"\"\r\n    # Logs go in the root folder's logs/ directory (one level up from src/)\r\n    script_dir = os.path.dirname(os.path.abspath(__file__))\r\n    parent_dir = os.path.dirname(script_dir)  # Go up one level from src/\r\n    log_dir = os.path.join(parent_dir, 'logs')\r\n    return log_dir\r\n\r\n\r\ndef initialize_error_handling(enable_logging: bool = False) -> CrashReporter:\r\n    \"\"\"\r\n    Initialize error handling and optionally set up logging.\r\n    \r\n    Args:\r\n        enable_logging: If True, creates log files and enables logging.\r\n                       If False, logging calls are no-ops.\r\n    \r\n    Returns:\r\n        CrashReporter instance\r\n    \"\"\"\r\n    global _logger, _logging_enabled\r\n    _logging_enabled = enable_logging\r\n    \r\n    # Create logger\r\n    _logger = logging.getLogger('brain_designer')\r\n    _logger.handlers.clear()  # Remove any existing handlers\r\n    \r\n    if enable_logging:\r\n        # Set up actual logging\r\n        _logger.setLevel(logging.DEBUG)\r\n        \r\n        # Create logs directory\r\n        log_dir = get_log_directory()\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        \r\n        # Create file handler with timestamp\r\n        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')\r\n        log_file = os.path.join(log_dir, f'designer_{timestamp}.log')\r\n        \r\n        file_handler = logging.FileHandler(log_file, encoding='utf-8')\r\n        file_handler.setLevel(logging.DEBUG)\r\n        \r\n        # Create console handler\r\n        console_handler = logging.StreamHandler(sys.stdout)\r\n        console_handler.setLevel(logging.INFO)\r\n        \r\n        # Create formatter\r\n        formatter = logging.Formatter(\r\n            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'\r\n        )\r\n        file_handler.setFormatter(formatter)\r\n        console_handler.setFormatter(formatter)\r\n        \r\n        _logger.addHandler(file_handler)\r\n        _logger.addHandler(console_handler)\r\n        \r\n        _logger.info(f\"Logging initialized. Log file: {log_file}\")\r\n    else:\r\n        # Disable logging - use null handler\r\n        _logger.setLevel(logging.CRITICAL + 1)  # Effectively disable all logging\r\n        _logger.addHandler(NullHandler())\r\n    \r\n    # Create crash reporter\r\n    crash_reporter = CrashReporter(enable_logging)\r\n    \r\n    # Install global exception handler\r\n    def exception_handler(exc_type, exc_value, exc_tb):\r\n        if exc_type != KeyboardInterrupt:\r\n            crash_reporter.report_crash(exc_type, exc_value, exc_tb)\r\n        sys.__excepthook__(exc_type, exc_value, exc_tb)\r\n    \r\n    sys.excepthook = exception_handler\r\n    \r\n    return crash_reporter\r\n\r\n\r\ndef get_logger(name: str = None) -> logging.Logger:\r\n    \"\"\"\r\n    Get a logger instance.\r\n    \r\n    Args:\r\n        name: Optional sub-logger name. If None, returns the main designer logger.\r\n    \r\n    Returns:\r\n        Logger instance\r\n    \"\"\"\r\n    global _logger\r\n    \r\n    if _logger is None:\r\n        # Initialize with logging disabled by default\r\n        initialize_error_handling(enable_logging=False)\r\n    \r\n    if name:\r\n        return _logger.getChild(name)\r\n    return _logger\r\n\r\n\r\n@contextmanager\r\ndef OperationLogger(operation_name: str, logger: logging.Logger = None):\r\n    \"\"\"\r\n    Context manager for logging operations with timing.\r\n    \r\n    Usage:\r\n        with OperationLogger(\"Loading data\"):\r\n            load_data()\r\n    \r\n    Args:\r\n        operation_name: Name of the operation being performed\r\n        logger: Optional specific logger to use\r\n    \"\"\"\r\n    if logger is None:\r\n        logger = get_logger()\r\n    \r\n    if not _logging_enabled:\r\n        # Just execute the block without logging\r\n        yield\r\n        return\r\n    \r\n    start_time = datetime.now()\r\n    logger.debug(f\"Starting: {operation_name}\")\r\n    \r\n    try:\r\n        yield\r\n        elapsed = (datetime.now() - start_time).total_seconds()\r\n        logger.debug(f\"Completed: {operation_name} ({elapsed:.3f}s)\")\r\n    except Exception as e:\r\n        elapsed = (datetime.now() - start_time).total_seconds()\r\n        logger.error(f\"Failed: {operation_name} ({elapsed:.3f}s) - {e}\")\r\n        raise\r\n\r\n\r\ndef log_exceptions(func):\r\n    \"\"\"\r\n    Decorator to log exceptions from a function.\r\n    \r\n    Usage:\r\n        @log_exceptions\r\n        def my_function():\r\n            ...\r\n    \"\"\"\r\n    def wrapper(*args, **kwargs):\r\n        try:\r\n            return func(*args, **kwargs)\r\n        except Exception as e:\r\n            if _logging_enabled:\r\n                logger = get_logger()\r\n                logger.error(f\"Exception in {func.__name__}: {e}\", exc_info=True)\r\n            raise\r\n    return wrapper\r\n\r\n\r\ndef safe_call(func, *args, default=None, **kwargs):\r\n    \"\"\"\r\n    Safely call a function, returning default on exception.\r\n    \r\n    Args:\r\n        func: Function to call\r\n        *args: Positional arguments\r\n        default: Value to return on exception\r\n        **kwargs: Keyword arguments\r\n    \r\n    Returns:\r\n        Function result or default value\r\n    \"\"\"\r\n    try:\r\n        return func(*args, **kwargs)\r\n    except Exception as e:\r\n        if _logging_enabled:\r\n            logger = get_logger()\r\n            logger.warning(f\"safe_call caught exception in {func.__name__}: {e}\")\r\n        return default\r\n"
  },
  {
    "path": "src/designer_network_generator.py",
    "content": "\"\"\"\r\nSparse Neural Network Generator\r\n\r\nGenerates realistic, biologically-inspired connections between the core neurons\r\nwith random noise so no two generations are identical.\r\n\"\"\"\r\n\r\nimport random\r\nimport math\r\nfrom typing import List, Tuple, Dict, Optional\r\nfrom dataclasses import dataclass\r\n\r\nfrom .designer_constants import CORE_NEURONS, REQUIRED_NEURON_NAMES\r\n\r\n\r\n@dataclass\r\nclass ConnectionTemplate:\r\n    \"\"\"Template for a potential connection with probability and weight range.\"\"\"\r\n    source: str\r\n    target: str\r\n    base_weight: float  # Central weight value\r\n    weight_variance: float  # +/- variance\r\n    probability: float  # Chance this connection is created (0.0 - 1.0)\r\n    description: str = \"\"\r\n\r\n\r\n# ============================================================================\r\n# BIOLOGICALLY-INSPIRED CONNECTION TEMPLATES\r\n# These define the \"tendencies\" of the neural network - realistic relationships\r\n# between hunger, emotions, and sensory input\r\n# ============================================================================\r\n\r\nCORE_CONNECTION_TEMPLATES = [\r\n    # === HUNGER dynamics ===\r\n    # Hunger creates dissatisfaction and negative mood\r\n    ConnectionTemplate(\"hunger\", \"satisfaction\", -0.35, 0.15, 0.85,\r\n                      \"Being hungry reduces satisfaction\"),\r\n    ConnectionTemplate(\"hunger\", \"happiness\", -0.25, 0.12, 0.70,\r\n                      \"Hunger negatively affects mood\"),\r\n    ConnectionTemplate(\"hunger\", \"anxiety\", 0.20, 0.10, 0.60,\r\n                      \"Hunger can cause anxiety\"),\r\n    ConnectionTemplate(\"hunger\", \"curiosity\", 0.15, 0.10, 0.45,\r\n                      \"Hunger may drive food-seeking curiosity\"),\r\n    \r\n    # === HAPPINESS dynamics ===\r\n    # Happiness is calming and promotes exploration\r\n    ConnectionTemplate(\"happiness\", \"anxiety\", -0.30, 0.12, 0.80,\r\n                      \"Being happy reduces anxiety\"),\r\n    ConnectionTemplate(\"happiness\", \"curiosity\", 0.25, 0.10, 0.65,\r\n                      \"Happy creatures are more curious\"),\r\n    ConnectionTemplate(\"happiness\", \"satisfaction\", 0.20, 0.08, 0.55,\r\n                      \"Happiness contributes to satisfaction\"),\r\n    \r\n    # === CLEANLINESS dynamics ===\r\n    # Being clean improves mood and reduces stress\r\n    ConnectionTemplate(\"cleanliness\", \"happiness\", 0.25, 0.10, 0.75,\r\n                      \"Being clean improves mood\"),\r\n    ConnectionTemplate(\"cleanliness\", \"anxiety\", -0.20, 0.08, 0.60,\r\n                      \"Cleanliness reduces stress\"),\r\n    ConnectionTemplate(\"cleanliness\", \"satisfaction\", 0.15, 0.08, 0.50,\r\n                      \"Cleanliness contributes to overall satisfaction\"),\r\n    \r\n    # === SLEEPINESS dynamics ===\r\n    # Tiredness affects cognition and mood negatively\r\n    ConnectionTemplate(\"sleepiness\", \"curiosity\", -0.30, 0.12, 0.70,\r\n                      \"Tiredness suppresses curiosity\"),\r\n    ConnectionTemplate(\"sleepiness\", \"happiness\", -0.20, 0.10, 0.65,\r\n                      \"Being tired affects mood\"),\r\n    ConnectionTemplate(\"sleepiness\", \"anxiety\", 0.15, 0.08, 0.55,\r\n                      \"Sleep deprivation increases anxiety\"),\r\n    ConnectionTemplate(\"sleepiness\", \"satisfaction\", -0.15, 0.08, 0.45,\r\n                      \"Tiredness reduces satisfaction\"),\r\n    \r\n    # === SATISFACTION dynamics ===\r\n    # Satisfaction is calming and mood-boosting\r\n    ConnectionTemplate(\"satisfaction\", \"happiness\", 0.35, 0.12, 0.85,\r\n                      \"Satisfaction promotes happiness\"),\r\n    ConnectionTemplate(\"satisfaction\", \"anxiety\", -0.25, 0.10, 0.75,\r\n                      \"Being satisfied reduces anxiety\"),\r\n    ConnectionTemplate(\"satisfaction\", \"curiosity\", 0.10, 0.08, 0.40,\r\n                      \"Satisfied creatures may explore more\"),\r\n    \r\n    # === ANXIETY dynamics ===\r\n    # Anxiety suppresses positive states and exploration\r\n    ConnectionTemplate(\"anxiety\", \"curiosity\", -0.35, 0.12, 0.80,\r\n                      \"Anxiety suppresses exploration\"),\r\n    ConnectionTemplate(\"anxiety\", \"happiness\", -0.25, 0.10, 0.70,\r\n                      \"Anxiety reduces happiness\"),\r\n    ConnectionTemplate(\"anxiety\", \"satisfaction\", -0.15, 0.08, 0.50,\r\n                      \"Anxiety reduces overall satisfaction\"),\r\n    ConnectionTemplate(\"anxiety\", \"sleepiness\", 0.10, 0.08, 0.35,\r\n                      \"Anxiety can cause fatigue\"),\r\n    \r\n    # === CURIOSITY dynamics ===\r\n    # Curiosity promotes positive mood and engagement\r\n    ConnectionTemplate(\"curiosity\", \"happiness\", 0.20, 0.10, 0.60,\r\n                      \"Curiosity brings joy\"),\r\n    ConnectionTemplate(\"curiosity\", \"satisfaction\", 0.15, 0.08, 0.45,\r\n                      \"Exploration satisfies\"),\r\n    ConnectionTemplate(\"curiosity\", \"anxiety\", -0.10, 0.08, 0.35,\r\n                      \"Curiosity can reduce anxiety through engagement\"),\r\n    \r\n    # === CAN_SEE_FOOD dynamics (vision input) ===\r\n    # Seeing food triggers hunger awareness and emotional responses\r\n    ConnectionTemplate(\"can_see_food\", \"hunger\", 0.30, 0.12, 0.90,\r\n                      \"Seeing food activates hunger awareness\"),\r\n    ConnectionTemplate(\"can_see_food\", \"happiness\", 0.25, 0.10, 0.75,\r\n                      \"Food sighting is exciting\"),\r\n    ConnectionTemplate(\"can_see_food\", \"curiosity\", 0.20, 0.10, 0.65,\r\n                      \"Food triggers investigative behavior\"),\r\n    ConnectionTemplate(\"can_see_food\", \"satisfaction\", 0.15, 0.08, 0.50,\r\n                      \"Food sight brings anticipatory satisfaction\"),\r\n    ConnectionTemplate(\"can_see_food\", \"anxiety\", -0.10, 0.08, 0.40,\r\n                      \"Food sighting may reduce food-seeking anxiety\"),\r\n]\r\n\r\n\r\nclass SparseNetworkGenerator:\r\n    \"\"\"\r\n    Generates sparse neural networks with biologically-inspired connections.\r\n    \"\"\"\r\n    \r\n    def __init__(self, seed: Optional[int] = None):\r\n        \"\"\"\r\n        Initialize generator with optional seed for reproducibility.\r\n        \r\n        Args:\r\n            seed: Random seed. If None, uses system entropy.\r\n        \"\"\"\r\n        self.seed = seed\r\n        self.rng = random.Random(seed)\r\n        self.templates = CORE_CONNECTION_TEMPLATES.copy()\r\n    \r\n    def set_seed(self, seed: int):\r\n        \"\"\"Set random seed for reproducible generation.\"\"\"\r\n        self.seed = seed\r\n        self.rng = random.Random(seed)\r\n    \r\n    def _generate_weight(self, template: ConnectionTemplate) -> float:\r\n        \"\"\"Generate a noisy weight from a template.\"\"\"\r\n        # Gaussian noise centered on base_weight\r\n        noise = self.rng.gauss(0, template.weight_variance)\r\n        weight = template.base_weight + noise\r\n        \r\n        # Clamp to valid range and round\r\n        weight = max(-1.0, min(1.0, weight))\r\n        return round(weight, 3)\r\n\r\n    def perturb_positions(self, design, variance: float = 0.3,\r\n                     bounds: Tuple[float, float, float, float] = (-400, -150, 900, 750)):\r\n        \"\"\"\r\n        Randomly perturb neuron positions with organic variance.\r\n        \r\n        Args:\r\n            design: BrainDesign to modify\r\n            variance: Variance multiplier (0.3 = ±30% from original position)\r\n            bounds: (min_x, min_y, max_x, max_y) to constrain neurons within view window\r\n        \"\"\"\r\n        if variance <= 0:\r\n            return\r\n            \r\n        min_x, min_y, max_x, max_y = bounds\r\n        rng = self.rng\r\n        \r\n        # Calculate center for organic spreading\r\n        if not design.neurons:\r\n            return\r\n            \r\n        for name, neuron in design.neurons.items():\r\n            # Skip required/core neurons to keep the brain structure recognizable\r\n            # but allow custom neurons (connectors, neurogenesis types) to move\r\n            if neuron.is_required or neuron.is_core:\r\n                continue\r\n            \r\n            # Ensure position is valid\r\n            if neuron.position is None:\r\n                continue\r\n                \r\n            x, y = neuron.position\r\n            \r\n            # Add Gaussian noise scaled by variance\r\n            # Using a larger base scale (100px) so variance=0.2 moves things visibly (~20px)\r\n            noise_scale = variance * 100 \r\n            new_x = x + rng.gauss(0, noise_scale)\r\n            new_y = y + rng.gauss(0, noise_scale)\r\n            \r\n            # Clamp to view bounds to keep them on canvas\r\n            new_x = max(min_x, min(max_x, new_x))\r\n            new_y = max(min_y, min(max_y, new_y))\r\n            \r\n            neuron.position = (new_x, new_y)\r\n\r\n    def add_random_sensors(self, design, probability: float = 0.3):\r\n        \"\"\"\r\n        Randomly add input sensors to the design based on specific game rules.\r\n        \r\n        Rules:\r\n        - 'can_see_food' is required (handled by design validation, but implicitly part of valid set)\r\n        - 'plant_proximity': 20% chance\r\n        - 'is_fleeing': 10% chance\r\n        - No other sensors are generated.\r\n        \r\n        Args:\r\n            design: BrainDesign to modify\r\n            probability: Ignored. Probabilities are hardcoded per requirements.\r\n        \r\n        Returns:\r\n            Number of sensors added\r\n        \"\"\"\r\n        # Specific probabilities requested\r\n        sensor_rules = {\r\n            'plant_proximity': 0.20,\r\n            'is_fleeing': 0.10\r\n        }\r\n        \r\n        added = 0\r\n        for sensor_name, chance in sensor_rules.items():\r\n            # Skip if already present\r\n            if sensor_name in design.neurons:\r\n                continue\r\n\r\n            # Roll the dice\r\n            if self.rng.random() < chance:\r\n                # add_sensor handles both creation and sensible wiring via defaults\r\n                success, _ = design.add_sensor(sensor_name, create_default_connections=True)\r\n                if success:\r\n                    added += 1\r\n                    \r\n        return added\r\n    \r\n    def _should_create_connection(self, template: ConnectionTemplate, \r\n                                   density_multiplier: float = 1.0) -> bool:\r\n        \"\"\"Decide if a connection should be created based on probability.\"\"\"\r\n        adjusted_prob = min(1.0, template.probability * density_multiplier)\r\n        return self.rng.random() < adjusted_prob\r\n    \r\n    def generate_connections(self, \r\n                              density: float = 1.0,\r\n                              include_feedback_loops: bool = True,\r\n                              weight_noise: float = 1.0\r\n                              ) -> List[Tuple[str, str, float]]:\r\n        \"\"\"\r\n        Generate sparse connections between core neurons.\r\n        \r\n        Args:\r\n            density: Multiplier for connection probability (0.5 = sparser, 1.5 = denser)\r\n            include_feedback_loops: If True, may add some bidirectional connections\r\n            weight_noise: Multiplier for weight variance (0.5 = less noise, 2.0 = more noise)\r\n        \r\n        Returns:\r\n            List of (source, target, weight) tuples\r\n        \"\"\"\r\n        connections = []\r\n        created_pairs = set()\r\n        \r\n        # Process each template\r\n        for template in self.templates:\r\n            if not self._should_create_connection(template, density):\r\n                continue\r\n            \r\n            # Generate noisy weight\r\n            adjusted_template = ConnectionTemplate(\r\n                template.source, template.target,\r\n                template.base_weight,\r\n                template.weight_variance * weight_noise,\r\n                template.probability,\r\n                template.description\r\n            )\r\n            weight = self._generate_weight(adjusted_template)\r\n            \r\n            # Skip very weak connections\r\n            if abs(weight) < 0.02:\r\n                continue\r\n            \r\n            connections.append((template.source, template.target, weight))\r\n            created_pairs.add((template.source, template.target))\r\n        \r\n        # Optionally add feedback loops (reverse connections)\r\n        if include_feedback_loops:\r\n            feedback_candidates = [\r\n                (\"satisfaction\", \"hunger\", -0.15, 0.4),  # Being satisfied reduces hunger drive\r\n                (\"happiness\", \"sleepiness\", -0.10, 0.3),  # Being happy reduces tiredness\r\n                (\"anxiety\", \"hunger\", 0.10, 0.25),  # Stress eating?\r\n                (\"curiosity\", \"anxiety\", 0.08, 0.2),  # Exploration can be slightly stressful\r\n            ]\r\n            \r\n            for source, target, base_w, prob in feedback_candidates:\r\n                # Don't create if forward connection doesn't exist or reverse already exists\r\n                if (target, source) not in created_pairs:\r\n                    continue\r\n                if (source, target) in created_pairs:\r\n                    continue\r\n                    \r\n                if self.rng.random() < prob * density:\r\n                    noise = self.rng.gauss(0, 0.05 * weight_noise)\r\n                    weight = round(max(-1.0, min(1.0, base_w + noise)), 3)\r\n                    if abs(weight) >= 0.02:\r\n                        connections.append((source, target, weight))\r\n                        created_pairs.add((source, target))\r\n        \r\n        # Shuffle to avoid predictable order\r\n        self.rng.shuffle(connections)\r\n        \r\n        return connections\r\n    \r\n    def generate_for_design(self, design, \r\n                            clear_existing: bool = True,\r\n                            density: float = 1.0,\r\n                            include_feedback: bool = True,\r\n                            weight_noise: float = 1.0,        # ADDED to match call sig\r\n                            position_variance: float = 0.0,   # ADDED to fix Error\r\n                            sensor_probability: float = 0.0,  # ADDED to match call sig\r\n                            bounds: Tuple[float, float, float, float] = (-400, -150, 900, 750),\r\n                            seed: Optional[int] = None,\r\n                            silent: bool = False) -> Tuple[int, List[str]]:\r\n        \"\"\"\r\n        Generate and apply sparse network to a BrainDesign.\r\n        \r\n        Args:\r\n            design: BrainDesign instance to modify\r\n            clear_existing: If True, removes existing connections first\r\n            density: Connection density multiplier\r\n            include_feedback: Include feedback loops\r\n            weight_noise: Multiplier for random weight variance\r\n            position_variance: If > 0, randomly moves neurons (perturbation)\r\n            sensor_probability: Chance to add random input sensors\r\n            bounds: (min_x, min_y, max_x, max_y) to constrain neurons within view window\r\n            seed: Optional seed for reproducible generation.\r\n            silent: If True, suppresses generation of action description strings\r\n        \r\n        Returns:\r\n            Tuple of (connections_created, list of action descriptions)\r\n        \"\"\"\r\n        actions = []\r\n        \r\n        # Apply seed if provided\r\n        if seed is not None:\r\n            self.set_seed(seed)\r\n            if not silent:\r\n                actions.append(f\"Using seed: {seed}\")\r\n        \r\n        # Ensure required neurons exist\r\n        missing = design.get_missing_required_neurons()\r\n        if missing:\r\n            design.add_missing_required_neurons()\r\n            if not silent:\r\n                actions.append(f\"Added missing required neurons: {', '.join(missing)}\")\r\n        \r\n        # 1. Handle Random Sensors (if requested)\r\n        if sensor_probability > 0:\r\n            added_sensors = self.add_random_sensors(design, sensor_probability)\r\n            if added_sensors > 0 and not silent:\r\n                actions.append(f\"Added {added_sensors} random input sensors\")\r\n\r\n        # 2. Handle Position Perturbation (if requested)\r\n        if position_variance > 0:\r\n            self.perturb_positions(design, variance=position_variance, bounds=bounds)\r\n            if not silent:\r\n                actions.append(f\"Perturbed neuron positions (variance: {position_variance})\")\r\n\r\n        # 3. Clear existing connections (if requested)\r\n        if clear_existing:\r\n            old_count = len(design.connections)\r\n            design.connections.clear()\r\n            if old_count > 0 and not silent:\r\n                actions.append(f\"Cleared {old_count} existing connections\")\r\n        \r\n        # 4. Generate new connections\r\n        connections = self.generate_connections(\r\n            density=density,\r\n            include_feedback_loops=include_feedback,\r\n            weight_noise=weight_noise\r\n        )\r\n        \r\n        # Apply to design\r\n        created = 0\r\n        for source, target, weight in connections:\r\n            # Verify both neurons exist in design\r\n            if source not in design.neurons or target not in design.neurons:\r\n                continue\r\n            \r\n            if design.add_connection(source, target, weight):\r\n                created += 1\r\n                if not silent:\r\n                    sign = \"+\" if weight > 0 else \"\"\r\n                    actions.append(f\"  {source} → {target} ({sign}{weight:.3f})\")\r\n        \r\n        return created, actions\r\n    \r\n    def get_preset_styles(self) -> Dict[str, Dict]:\r\n        \"\"\"Return preset generation styles with new variance and sensor options.\"\"\"\r\n        return {\r\n            'balanced': {\r\n                'name': '⚖️ Balanced',\r\n                'description': 'Standard density, moderate noise, slight position variance',\r\n                'density': 1.0,\r\n                'include_feedback': True,\r\n                'weight_noise': 1.0,\r\n                'position_variance': 0.2,\r\n                'sensor_probability': 0.15\r\n            },\r\n            'sparse': {\r\n                'name': '🔬 Minimal',\r\n                'description': 'Fewer connections, minimal position variance',\r\n                'density': 0.5,\r\n                'include_feedback': False,\r\n                'weight_noise': 0.7,\r\n                'position_variance': 0.1,\r\n                'sensor_probability': 0.0\r\n            },\r\n            'dense': {\r\n                'name': '🕸️ Dense',\r\n                'description': 'More connections, rich dynamics, moderate sensors',\r\n                'density': 1.4,\r\n                'include_feedback': True,\r\n                'weight_noise': 1.2,\r\n                'position_variance': 0.3,\r\n                'sensor_probability': 0.3\r\n            },\r\n            'chaotic': {\r\n                'name': '🌀 Chaotic',\r\n                'description': 'High noise, unpredictable, high position variance',\r\n                'density': 1.1,\r\n                'include_feedback': True,\r\n                'weight_noise': 2.5,\r\n                'position_variance': 0.5,\r\n                'sensor_probability': 0.4\r\n            },\r\n            'calm': {\r\n                'name': '🧘 Calm',\r\n                'description': 'Weaker connections, stable, low variance',\r\n                'density': 0.8,\r\n                'include_feedback': True,\r\n                'weight_noise': 0.5,\r\n                'position_variance': 0.1,\r\n                'sensor_probability': 0.0\r\n            },\r\n            'wild': {\r\n                'name': '🌿 Wild',\r\n                'description': 'Organic positions, many sensors, natural feel',\r\n                'density': 1.2,\r\n                'include_feedback': True,\r\n                'weight_noise': 1.5,\r\n                'position_variance': 0.4,\r\n                'sensor_probability': 0.5\r\n            }\r\n        }\r\n\r\n\r\n# ============================================================================\r\n# UTILITY FUNCTIONS\r\n# ============================================================================\r\n\r\ndef generate_sparse_core_network(density: float = 1.0, \r\n                                  seed: Optional[int] = None) -> List[Tuple[str, str, float]]:\r\n    \"\"\"\r\n    Convenience function to generate sparse network connections.\r\n    \"\"\"\r\n    generator = SparseNetworkGenerator(seed)\r\n    return generator.generate_connections(density=density)\r\n\r\n\r\ndef describe_connection(source: str, target: str, weight: float) -> str:\r\n    \"\"\"Generate a human-readable description of a connection.\"\"\"\r\n    effect = \"excites\" if weight > 0 else \"inhibits\"\r\n    strength = abs(weight)\r\n    \r\n    if strength < 0.15:\r\n        strength_word = \"weakly\"\r\n    elif strength < 0.35:\r\n        strength_word = \"moderately\"\r\n    elif strength < 0.6:\r\n        strength_word = \"strongly\"\r\n    else:\r\n        strength_word = \"powerfully\"\r\n    \r\n    return f\"{source} {strength_word} {effect} {target}\"\r\n"
  },
  {
    "path": "src/designer_outputs_panel.py",
    "content": "# designer_outputs_panel.py\r\n\"\"\"\r\nBrain Designer panel for configuring neuron output bindings.\r\n\r\nThis panel allows users to:\r\n1. Bind neurons to output hooks (actuators)\r\n2. Configure trigger thresholds and modes\r\n3. Manage cooldowns and parameters (including colour selection)\r\n\r\nWhen the squid runs this brain, neurons that exceed their threshold\r\nwill trigger the bound behaviors (flee, seek food, ink, etc.)\r\n\"\"\"\r\n\r\nfrom PyQt5.QtWidgets import (\r\n    QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,\r\n    QPushButton, QLabel, QComboBox, QDoubleSpinBox, QCheckBox,\r\n    QTableWidget, QTableWidgetItem, QHeaderView, QDialog,\r\n    QDialogButtonBox, QScrollArea, QFrame, QMessageBox, QSpinBox,\r\n    QColorDialog\r\n)\r\nfrom PyQt5.QtCore import Qt, pyqtSignal\r\nfrom PyQt5.QtGui import QColor\r\n\r\ntry:\r\n    from .brain_neuron_outputs import (\r\n        NeuronOutputBinding, OutputTriggerMode,\r\n        STANDARD_OUTPUT_HOOKS, get_output_hooks_by_category\r\n    )\r\nexcept ImportError:\r\n    from brain_neuron_outputs import (\r\n        NeuronOutputBinding, OutputTriggerMode,\r\n        STANDARD_OUTPUT_HOOKS, get_output_hooks_by_category\r\n    )\r\n\r\n\r\nclass OutputBindingDialog(QDialog):\r\n    \"\"\"Dialog for creating or editing a neuron output binding.\"\"\"\r\n    \r\n    def __init__(self, design, existing_binding=None, parent=None):\r\n        super().__init__(parent)\r\n        self.design = design\r\n        self.existing_binding = existing_binding\r\n        self.result_binding = None\r\n        \r\n        # Store dynamic parameters here\r\n        self.current_params = {}\r\n        if existing_binding and existing_binding.hook_params:\r\n            self.current_params = existing_binding.hook_params.copy()\r\n        \r\n        self.setWindowTitle(\"Configure Output Binding\" if existing_binding else \"Add Output Binding\")\r\n        self.setMinimumWidth(450)\r\n        self.setup_ui()\r\n        \r\n        if existing_binding:\r\n            self._populate_from_binding(existing_binding)\r\n    \r\n    def setup_ui(self):\r\n        layout = QVBoxLayout(self)\r\n        \r\n        # 1. Selection Group\r\n        type_group = QGroupBox(\"Select Neuron Type\")\r\n        type_layout = QFormLayout(type_group)\r\n        \r\n        self.neuron_combo = QComboBox()\r\n        self._populate_neurons()\r\n        type_layout.addRow(\"Neuron:\", self.neuron_combo)\r\n        \r\n        # Show current activation hint\r\n        self.activation_label = QLabel(\"Current: --\")\r\n        self.activation_label.setStyleSheet(\"color: #888;\")\r\n        type_layout.addRow(\"\", self.activation_label)\r\n        \r\n        layout.addWidget(type_group)\r\n        \r\n        # 2. Output hook selection\r\n        hook_group = QGroupBox(\"Output Behavior\")\r\n        hook_layout = QFormLayout(hook_group)\r\n        \r\n        self.hook_combo = QComboBox()\r\n        self._populate_hooks()\r\n        self.hook_combo.currentTextChanged.connect(self._on_hook_changed)\r\n        hook_layout.addRow(\"Trigger:\", self.hook_combo)\r\n        \r\n        self.hook_description = QLabel(\"\")\r\n        self.hook_description.setWordWrap(True)\r\n        self.hook_description.setStyleSheet(\"color: #666; font-style: italic;\")\r\n        hook_layout.addRow(\"\", self.hook_description)\r\n        \r\n        layout.addWidget(hook_group)\r\n\r\n        # === Dynamic Parameters Area ===\r\n        self.params_group = QGroupBox(\"Behavior Parameters\")\r\n        self.params_layout = QVBoxLayout(self.params_group)\r\n        \r\n        # Color Picker UI (Hidden by default)\r\n        self.color_widget = QWidget()\r\n        color_layout = QHBoxLayout(self.color_widget)\r\n        color_layout.setContentsMargins(0,0,0,0)\r\n        \r\n        self.color_preview = QLabel()\r\n        self.color_preview.setFixedSize(30, 30)\r\n        self.color_preview.setStyleSheet(\"background-color: #CCCCCC; border: 1px solid #888;\")\r\n        \r\n        self.pick_color_btn = QPushButton(\"Pick Colour...\")\r\n        self.pick_color_btn.clicked.connect(self._pick_color)\r\n        \r\n        self.reset_color_btn = QPushButton(\"Reset (Random)\")\r\n        self.reset_color_btn.clicked.connect(self._reset_color)\r\n        \r\n        color_layout.addWidget(QLabel(\"Tint Colour:\"))\r\n        color_layout.addWidget(self.color_preview)\r\n        color_layout.addWidget(self.pick_color_btn)\r\n        color_layout.addWidget(self.reset_color_btn)\r\n        color_layout.addStretch()\r\n        \r\n        self.params_layout.addWidget(self.color_widget)\r\n        self.color_widget.hide() # Hide initially\r\n        \r\n        layout.addWidget(self.params_group)\r\n        self.params_group.hide() # Hide group initially\r\n        \r\n        # Trigger configuration\r\n        trigger_group = QGroupBox(\"Trigger Settings\")\r\n        trigger_layout = QFormLayout(trigger_group)\r\n        \r\n        # Threshold\r\n        self.threshold_spin = QDoubleSpinBox()\r\n        self.threshold_spin.setRange(0, 100)\r\n        self.threshold_spin.setValue(70)\r\n        self.threshold_spin.setSuffix(\" %\")\r\n        self.threshold_spin.setToolTip(\"Activation level required to trigger the output\")\r\n        trigger_layout.addRow(\"Threshold:\", self.threshold_spin)\r\n        \r\n        # Trigger mode\r\n        self.mode_combo = QComboBox()\r\n        self.mode_combo.addItem(\"Rising Edge (cross threshold going up)\", OutputTriggerMode.THRESHOLD_RISING.value)\r\n        self.mode_combo.addItem(\"Falling Edge (cross threshold going down)\", OutputTriggerMode.THRESHOLD_FALLING.value)\r\n        self.mode_combo.addItem(\"While Above (continuous while > threshold)\", OutputTriggerMode.THRESHOLD_ABOVE.value)\r\n        self.mode_combo.addItem(\"While Below (continuous while < threshold)\", OutputTriggerMode.THRESHOLD_BELOW.value)\r\n        self.mode_combo.addItem(\"On Change (any significant change)\", OutputTriggerMode.ON_CHANGE.value)\r\n        trigger_layout.addRow(\"Mode:\", self.mode_combo)\r\n        \r\n        # Cooldown\r\n        self.cooldown_spin = QDoubleSpinBox()\r\n        self.cooldown_spin.setRange(0.1, 60)\r\n        self.cooldown_spin.setValue(1.0)\r\n        self.cooldown_spin.setSuffix(\" sec\")\r\n        self.cooldown_spin.setToolTip(\"Minimum time between triggers\")\r\n        trigger_layout.addRow(\"Cooldown:\", self.cooldown_spin)\r\n        \r\n        # Enabled\r\n        self.enabled_check = QCheckBox(\"Enabled\")\r\n        self.enabled_check.setChecked(True)\r\n        trigger_layout.addRow(\"\", self.enabled_check)\r\n        \r\n        layout.addWidget(trigger_group)\r\n        \r\n        # Buttons\r\n        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)\r\n        buttons.accepted.connect(self.accept)\r\n        buttons.rejected.connect(self.reject)\r\n        layout.addWidget(buttons)\r\n        \r\n        # Initialize hook description\r\n        self._on_hook_changed()\r\n    \r\n    def _populate_neurons(self):\r\n        \"\"\"Populate neuron combo with available neurons.\"\"\"\r\n        self.neuron_combo.clear()\r\n        \r\n        # Get all non-core neurons (outputs typically come from processing neurons)\r\n        for name, neuron in sorted(self.design.neurons.items()):\r\n            if not neuron.is_core:\r\n                display = name\r\n                \r\n                # Add icons for special types\r\n                if neuron.is_sensor:\r\n                    display = f\"📡 {name}\"\r\n                elif name.startswith('connector_'):\r\n                    display = f\"🔗 {name}\"\r\n                elif name.startswith('novelty_'):\r\n                    display = f\"✨ {name}\"\r\n                elif name.startswith('stress_'):\r\n                    display = f\"🔥 {name}\"\r\n                elif name.startswith('reward_'):\r\n                    display = f\"💎 {name}\"\r\n                    \r\n                self.neuron_combo.addItem(display, name)\r\n        \r\n        # Also add core neurons (they might want anxiety to trigger flee, etc.)\r\n        self.neuron_combo.insertSeparator(self.neuron_combo.count())\r\n        for name, neuron in sorted(self.design.neurons.items()):\r\n            if neuron.is_core:\r\n                self.neuron_combo.addItem(f\"⚡ {name}\", name)\r\n    \r\n    def _populate_hooks(self):\r\n        \"\"\"Populate hook combo with available output hooks.\"\"\"\r\n        self.hook_combo.clear()\r\n        \r\n        by_category = get_output_hooks_by_category()\r\n        \r\n        for category in sorted(by_category.keys()):\r\n            hooks = by_category[category]\r\n            \r\n            # Add category header\r\n            self.hook_combo.addItem(f\"── {category.title()} ──\", None)\r\n            \r\n            # Make header non-selectable\r\n            idx = self.hook_combo.count() - 1\r\n            self.hook_combo.model().item(idx).setEnabled(False)\r\n            \r\n            for hook_name, info in sorted(hooks.items()):\r\n                display_name = hook_name.replace('neuron_output_', '').replace('_', ' ').title()\r\n                self.hook_combo.addItem(f\"  {display_name}\", hook_name)\r\n    \r\n    def _on_hook_changed(self):\r\n        \"\"\"Update description and param UI when hook selection changes.\"\"\"\r\n        hook_name = self.hook_combo.currentData()\r\n        self._update_param_ui(hook_name)\r\n\r\n        if hook_name and hook_name in STANDARD_OUTPUT_HOOKS:\r\n            info = STANDARD_OUTPUT_HOOKS[hook_name]\r\n            self.hook_description.setText(info.get('description', ''))\r\n            \r\n            # Update default threshold\r\n            default_thresh = info.get('default_threshold', 70)\r\n            if not self.existing_binding:  # Only auto-set for new bindings\r\n                self.threshold_spin.setValue(default_thresh)\r\n        else:\r\n            self.hook_description.setText(\"\")\r\n    \r\n    def _update_param_ui(self, hook_name):\r\n        \"\"\"Show specific UI elements based on the selected hook.\"\"\"\r\n        self.params_group.hide()\r\n        self.color_widget.hide()\r\n        \r\n        if hook_name == 'neuron_output_change_color':\r\n            self.params_group.show()\r\n            self.color_widget.show()\r\n            self._update_color_preview()\r\n\r\n    def _update_color_preview(self):\r\n        \"\"\"Update the color preview box based on current params.\"\"\"\r\n        if 'red' in self.current_params:\r\n            r = self.current_params.get('red', 255)\r\n            g = self.current_params.get('green', 255)\r\n            b = self.current_params.get('blue', 255)\r\n            self.color_preview.setStyleSheet(f\"background-color: rgb({r},{g},{b}); border: 1px solid #000;\")\r\n            self.color_preview.setText(\"\")\r\n        else:\r\n            self.color_preview.setStyleSheet(\"background-color: #EEE; border: 1px dashed #888;\")\r\n            self.color_preview.setText(\"?\")\r\n\r\n    def _pick_color(self):\r\n        \"\"\"Open color picker dialog.\"\"\"\r\n        initial = QColor(255, 255, 255)\r\n        if 'red' in self.current_params:\r\n            initial = QColor(\r\n                self.current_params['red'], \r\n                self.current_params['green'], \r\n                self.current_params['blue']\r\n            )\r\n            \r\n        color = QColorDialog.getColor(initial, self, \"Select Output Tint\")\r\n        \r\n        if color.isValid():\r\n            self.current_params['red'] = color.red()\r\n            self.current_params['green'] = color.green()\r\n            self.current_params['blue'] = color.blue()\r\n            self._update_color_preview()\r\n\r\n    def _reset_color(self):\r\n        \"\"\"Clear color params to revert to random.\"\"\"\r\n        self.current_params.pop('red', None)\r\n        self.current_params.pop('green', None)\r\n        self.current_params.pop('blue', None)\r\n        self._update_color_preview()\r\n\r\n    def _populate_from_binding(self, binding: NeuronOutputBinding):\r\n        \"\"\"Fill dialog fields from existing binding.\"\"\"\r\n        # Find and select neuron\r\n        for i in range(self.neuron_combo.count()):\r\n            if self.neuron_combo.itemData(i) == binding.neuron_name:\r\n                self.neuron_combo.setCurrentIndex(i)\r\n                break\r\n        \r\n        # Find and select hook\r\n        for i in range(self.hook_combo.count()):\r\n            if self.hook_combo.itemData(i) == binding.output_hook:\r\n                self.hook_combo.setCurrentIndex(i)\r\n                break\r\n        \r\n        self.threshold_spin.setValue(binding.threshold)\r\n        self.cooldown_spin.setValue(binding.cooldown)\r\n        self.enabled_check.setChecked(binding.enabled)\r\n        \r\n        # Find and select mode\r\n        for i in range(self.mode_combo.count()):\r\n            if self.mode_combo.itemData(i) == binding.trigger_mode.value:\r\n                self.mode_combo.setCurrentIndex(i)\r\n                break\r\n        \r\n        # Load params\r\n        self.current_params = binding.hook_params.copy() if binding.hook_params else {}\r\n        self._on_hook_changed()\r\n\r\n    def accept(self):\r\n        \"\"\"Validate and create binding.\"\"\"\r\n        neuron_name = self.neuron_combo.currentData()\r\n        hook_name = self.hook_combo.currentData()\r\n        \r\n        if not neuron_name:\r\n            QMessageBox.warning(self, \"Error\", \"Please select a neuron\")\r\n            return\r\n        \r\n        if not hook_name:\r\n            QMessageBox.warning(self, \"Error\", \"Please select an output behavior\")\r\n            return\r\n        \r\n        mode_value = self.mode_combo.currentData()\r\n        trigger_mode = OutputTriggerMode(mode_value)\r\n        \r\n        self.result_binding = NeuronOutputBinding(\r\n            neuron_name=neuron_name,\r\n            output_hook=hook_name,\r\n            threshold=self.threshold_spin.value(),\r\n            trigger_mode=trigger_mode,\r\n            cooldown=self.cooldown_spin.value(),\r\n            enabled=self.enabled_check.isChecked(),\r\n            hook_params=self.current_params # Save collected params\r\n        )\r\n        \r\n        super().accept()\r\n\r\n\r\nclass NeuronOutputsPanel(QWidget):\r\n    \"\"\"\r\n    Panel for managing neuron output bindings in the brain designer.\r\n    \r\n    Shows a table of all output bindings and allows adding/editing/removing them.\r\n    \"\"\"\r\n    \r\n    outputsChanged = pyqtSignal()\r\n    \r\n    def __init__(self, design, parent=None):\r\n        super().__init__(parent)\r\n        self.design = design\r\n        \r\n        # Store bindings locally (will be exported with brain)\r\n        self.bindings: list = []\r\n        \r\n        self.setup_ui()\r\n    \r\n    def setup_ui(self):\r\n        layout = QVBoxLayout(self)\r\n        \r\n        # Header with description\r\n        header = QLabel(\r\n            \"<b>Output Bindings</b><br>\"\r\n            \"<small>Connect neurons to squid behaviors. When a neuron's activation \"\r\n            \"exceeds the threshold, it triggers the bound action.</small>\"\r\n        )\r\n        header.setWordWrap(True)\r\n        layout.addWidget(header)\r\n        \r\n        # Toolbar\r\n        toolbar = QHBoxLayout()\r\n        \r\n        add_btn = QPushButton(\"➕ Add Binding\")\r\n        add_btn.clicked.connect(self.add_binding)\r\n        toolbar.addWidget(add_btn)\r\n        \r\n        edit_btn = QPushButton(\"✏️ Edit\")\r\n        edit_btn.clicked.connect(self.edit_binding)\r\n        toolbar.addWidget(edit_btn)\r\n        \r\n        remove_btn = QPushButton(\"🗑️ Remove\")\r\n        remove_btn.clicked.connect(self.remove_binding)\r\n        toolbar.addWidget(remove_btn)\r\n        \r\n        toolbar.addStretch()\r\n        layout.addLayout(toolbar)\r\n        \r\n        # Bindings table\r\n        self.table = QTableWidget()\r\n        self.table.setColumnCount(5)\r\n        self.table.setHorizontalHeaderLabels([\r\n            \"Neuron\", \"→ Behavior\", \"Threshold\", \"Mode\", \"Enabled\"\r\n        ])\r\n        \r\n        # [CHANGED] Enable interactive column resizing and set default widths\r\n        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)\r\n        self.table.setColumnWidth(0, 150)  # Neuron\r\n        self.table.setColumnWidth(1, 200)  # Behavior (wider to show color)\r\n        self.table.setColumnWidth(2, 100)  # Threshold\r\n        self.table.setColumnWidth(3, 150)  # Mode\r\n        self.table.setColumnWidth(4, 80)   # Enabled\r\n        \r\n        self.table.setSelectionBehavior(QTableWidget.SelectRows)\r\n        self.table.setSelectionMode(QTableWidget.SingleSelection)\r\n        self.table.doubleClicked.connect(self.edit_binding)\r\n        layout.addWidget(self.table)\r\n        \r\n        # Quick info\r\n        self.info_label = QLabel(\"\")\r\n        self.info_label.setStyleSheet(\"color: #888;\")\r\n        layout.addWidget(self.info_label)\r\n        \r\n        self.refresh()\r\n    \r\n    def refresh(self):\r\n        \"\"\"Refresh the bindings table.\"\"\"\r\n        self.table.setRowCount(len(self.bindings))\r\n        \r\n        for row, binding in enumerate(self.bindings):\r\n            # Neuron name\r\n            neuron_item = QTableWidgetItem(binding.neuron_name)\r\n            if binding.neuron_name not in self.design.neurons:\r\n                neuron_item.setForeground(QColor(255, 100, 100))  # Red if missing\r\n                neuron_item.setToolTip(\"⚠️ Neuron not found in design\")\r\n            self.table.setItem(row, 0, neuron_item)\r\n            \r\n            # Output hook (formatted nicely)\r\n            hook_display = binding.output_hook.replace('neuron_output_', '').replace('_', ' ').title()\r\n            \r\n            # [CHANGED] For color bindings, show color as cell background instead of RGB text\r\n            hook_item = QTableWidgetItem(hook_display)\r\n            if binding.output_hook in STANDARD_OUTPUT_HOOKS:\r\n                hook_item.setToolTip(STANDARD_OUTPUT_HOOKS[binding.output_hook].get('description', ''))\r\n            \r\n            # If this is a color binding, set the background color\r\n            if binding.hook_params and 'red' in binding.hook_params:\r\n                r = binding.hook_params.get('red', 255)\r\n                g = binding.hook_params.get('green', 255)\r\n                b = binding.hook_params.get('blue', 255)\r\n                color = QColor(r, g, b)\r\n                hook_item.setBackground(color)\r\n                \r\n                # Adjust text color for contrast\r\n                brightness = (r * 299 + g * 587 + b * 114) / 1000\r\n                if brightness < 128:\r\n                    hook_item.setForeground(QColor(255, 255, 255))\r\n                else:\r\n                    hook_item.setForeground(QColor(0, 0, 0))\r\n            \r\n            self.table.setItem(row, 1, hook_item)\r\n            \r\n            # Threshold\r\n            thresh_item = QTableWidgetItem(f\"{binding.threshold:.0f}%\")\r\n            self.table.setItem(row, 2, thresh_item)\r\n            \r\n            # Mode\r\n            mode_display = binding.trigger_mode.value.replace('_', ' ').title()\r\n            mode_item = QTableWidgetItem(mode_display)\r\n            self.table.setItem(row, 3, mode_item)\r\n            \r\n            # Enabled\r\n            enabled_item = QTableWidgetItem(\"✓\" if binding.enabled else \"✗\")\r\n            enabled_item.setTextAlignment(Qt.AlignCenter)\r\n            if not binding.enabled:\r\n                enabled_item.setForeground(QColor(150, 150, 150))\r\n            self.table.setItem(row, 4, enabled_item)\r\n        \r\n        # Update info\r\n        enabled_count = sum(1 for b in self.bindings if b.enabled)\r\n        self.info_label.setText(f\"{len(self.bindings)} binding(s), {enabled_count} enabled\")\r\n    \r\n    def add_binding(self):\r\n        \"\"\"Show dialog to add a new binding.\"\"\"\r\n        dialog = OutputBindingDialog(self.design, parent=self)\r\n        if dialog.exec_() == QDialog.Accepted and dialog.result_binding:\r\n            # Duplicate check REMOVED to allow multiple bindings for same neuron/hook\r\n            # (e.g. one for Rising threshold, one for Falling)\r\n            \r\n            self.bindings.append(dialog.result_binding)\r\n            self.refresh()\r\n            self.outputsChanged.emit()\r\n    \r\n    def edit_binding(self):\r\n        \"\"\"Edit the selected binding.\"\"\"\r\n        row = self.table.currentRow()\r\n        if row < 0 or row >= len(self.bindings):\r\n            return\r\n        \r\n        binding = self.bindings[row]\r\n        dialog = OutputBindingDialog(self.design, existing_binding=binding, parent=self)\r\n        \r\n        if dialog.exec_() == QDialog.Accepted and dialog.result_binding:\r\n            self.bindings[row] = dialog.result_binding\r\n            self.refresh()\r\n            self.outputsChanged.emit()\r\n    \r\n    def remove_binding(self):\r\n        \"\"\"Remove the selected binding.\"\"\"\r\n        row = self.table.currentRow()\r\n        if row < 0 or row >= len(self.bindings):\r\n            return\r\n        \r\n        binding = self.bindings[row]\r\n        reply = QMessageBox.question(\r\n            self, \"Remove Binding\",\r\n            f\"Remove binding: {binding.neuron_name} → {binding.output_hook}?\",\r\n            QMessageBox.Yes | QMessageBox.No\r\n        )\r\n        \r\n        if reply == QMessageBox.Yes:\r\n            self.bindings.pop(row)\r\n            self.refresh()\r\n            self.outputsChanged.emit()\r\n    \r\n    def load_bindings(self, bindings_data: list):\r\n        \"\"\"Load bindings from saved data.\"\"\"\r\n        self.bindings.clear()\r\n        for data in bindings_data:\r\n            try:\r\n                binding = NeuronOutputBinding.from_dict(data)\r\n                self.bindings.append(binding)\r\n            except Exception as e:\r\n                print(f\"[OutputsPanel] Error loading binding: {e}\")\r\n        self.refresh()\r\n    \r\n    def export_bindings(self) -> list:\r\n        \"\"\"Export bindings for saving.\"\"\"\r\n        return [b.to_dict() for b in self.bindings]\r\n    \r\n    def validate_bindings(self) -> list:\r\n        \"\"\"\r\n        Validate bindings against current design.\r\n        \r\n        Returns list of warning messages.\r\n        \"\"\"\r\n        warnings = []\r\n        for binding in self.bindings:\r\n            if binding.neuron_name not in self.design.neurons:\r\n                warnings.append(f\"Binding references missing neuron: {binding.neuron_name}\")\r\n        return warnings"
  },
  {
    "path": "src/designer_panels.py",
    "content": "from PyQt5.QtWidgets import (\n    QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QListWidget, QListWidgetItem, \n    QPushButton, QLabel, QLineEdit, QDoubleSpinBox, QCheckBox, QComboBox, QTextEdit, \n    QTableWidget, QTableWidgetItem, QHeaderView, QScrollArea, QDialog, QMessageBox, QInputDialog\n)\nfrom PyQt5.QtCore import Qt, pyqtSignal\nfrom typing import Optional, Dict\n\nfrom .designer_core import BrainDesign, DesignerNeuron, DesignerLayer\nfrom .designer_constants import (\n    NeuronType, INPUT_SENSORS, REQUIRED_NEURONS, DEFAULT_SENSOR_CONNECTIONS,\n    is_required_neuron, is_input_sensor\n)\n\n# Optional: Import sensor discovery for plugin sensors\ntry:\n    from .designer_sensor_discovery import get_all_available_sensors, is_plugin_sensor\n    _HAS_SENSOR_DISCOVERY = True\nexcept ImportError:\n    _HAS_SENSOR_DISCOVERY = False\n    def get_all_available_sensors():\n        # Wrap raw (x,y) tuples from INPUT_SENSORS into proper info dicts\n        return {\n            name: {'description': name.replace('_', ' ').title(),\n                   'is_binary': False, 'category': 'Built-in', 'plugin': None,\n                   'default_connections': []}\n            for name in INPUT_SENSORS\n        }\n    def is_plugin_sensor(name):\n        return False\n\nclass AddNeuronDialog(QDialog):\n    def __init__(self, design: BrainDesign, position=None, parent=None):\n        super().__init__(parent)\n        self.design = design\n        self.position = position or (400, 200)\n        self.result_neuron = None\n        self.result_message = \"\"\n        self.setWindowTitle(\"Add Neuron\")\n        self.setMinimumWidth(400)\n        self.setup_ui()\n    \n    def setup_ui(self):\n        layout = QVBoxLayout(self)\n        \n        # 1. Selection Group\n        type_group = QGroupBox(\"Select Neuron Type\")\n        type_layout = QVBoxLayout(type_group)\n        \n        # Custom Neuron Button (The \"Magic\" one)\n        custom_btn = QPushButton(\"✨ Custom / Plugin Neuron\")\n        custom_btn.setToolTip(\"Create a neuron with a specific name to link with game plugins\")\n        custom_btn.clicked.connect(lambda: self.select_type('custom'))\n        type_layout.addWidget(custom_btn)\n        \n        # Sensor Button\n        sensor_btn = QPushButton(\"📡 Input Sensor\")\n        sensor_btn.clicked.connect(lambda: self.select_type('sensor'))\n        type_layout.addWidget(sensor_btn)\n        \n        layout.addWidget(type_group)\n        \n        # 2. Sensor Selection Group (Hidden by default)\n        self.sensor_group = QGroupBox(\"Select Sensor\")\n        sensor_layout = QVBoxLayout(self.sensor_group)\n        self.sensor_list = QListWidget()\n        self.sensor_list.itemDoubleClicked.connect(self.accept_sensor)\n        sensor_layout.addWidget(self.sensor_list)\n        layout.addWidget(self.sensor_group)\n        self.sensor_group.hide()\n        \n        # 3. Custom Neuron Entry Group (Hidden by default)\n        self.custom_group = QGroupBox(\"Define Custom Neuron\")\n        custom_layout = QFormLayout(self.custom_group)\n        \n        # Magic Link Instruction\n        info_label = QLabel(\n            \"<i>To affect the squid, the <b>Name</b> must match a plugin ID.<br>\"\n            \"Example: Name it <b>'jet_boost'</b> to activate a jetpack plugin.</i>\"\n        )\n        info_label.setWordWrap(True)\n        info_label.setStyleSheet(\"color: #666; margin-bottom: 5px;\")\n        custom_layout.addRow(info_label)\n        \n        self.name_edit = QLineEdit()\n        self.name_edit.setPlaceholderText(\"e.g. turbo_mode\")\n        custom_layout.addRow(\"Plugin ID / Name:\", self.name_edit)\n        \n        add_custom_btn = QPushButton(\"Create Link\")\n        add_custom_btn.setStyleSheet(\"font-weight: bold; background-color: #E0F7FA; color: #006064;\")\n        add_custom_btn.clicked.connect(self.accept_custom)\n        custom_layout.addRow(add_custom_btn)\n        \n        layout.addWidget(self.custom_group)\n        self.custom_group.hide()\n    \n    def select_type(self, t):\n        if t == 'sensor':\n            self.sensor_group.show()\n            self.custom_group.hide()\n            self.populate_sensor_list()\n        else:\n            self.custom_group.show()\n            self.sensor_group.hide()\n            self.name_edit.setFocus()\n\n    def populate_sensor_list(self):\n        self.sensor_list.clear()\n        existing = set(self.design.neurons.keys())\n        \n        # Get all available sensors (built-in + plugin)\n        all_sensors = get_all_available_sensors()\n        \n        # Also ensure can_see_food is included\n        if 'can_see_food' not in all_sensors and 'can_see_food' in REQUIRED_NEURONS:\n            all_sensors['can_see_food'] = {\n                'description': REQUIRED_NEURONS['can_see_food'].get('description', ''),\n                'is_binary': True,\n                'plugin': None\n            }\n        \n        # Filter to available sensors\n        available = {k: v for k, v in all_sensors.items() if k not in existing}\n        \n        if not available:\n            self.sensor_list.addItem(\"All sensors added\")\n            return\n\n        for name in sorted(available.keys()):\n            info = available[name]\n            display_name = name.replace('_', ' ').title()\n            \n            # Add plugin indicator\n            if info.get('plugin'):\n                display_name = f\"🔌 {display_name}\"\n            \n            item = QListWidgetItem(display_name)\n            item.setData(Qt.UserRole, name)\n            \n            # Add tooltip\n            tooltip = info.get('description', '')\n            if info.get('plugin'):\n                tooltip += f\"\\n[Plugin: {info['plugin']}]\"\n            if tooltip:\n                item.setToolTip(tooltip.strip())\n            \n            self.sensor_list.addItem(item)\n\n    def accept_sensor(self):\n        items = self.sensor_list.selectedItems()\n        if not items: return\n        name = items[0].data(Qt.UserRole)\n        if not name: return \n        \n        success, msg = self.design.add_sensor(name)\n        if success:\n            self.result_message = msg\n            self.accept()\n        else: QMessageBox.warning(self, \"Error\", msg)\n\n    def accept_custom(self):\n        name = self.name_edit.text().strip().lower().replace(' ', '_')\n        if not name: return\n        if name in self.design.neurons:\n            QMessageBox.warning(self, \"Error\", \"Exists\")\n            return\n        self.design.add_neuron(DesignerNeuron(name=name, neuron_type=NeuronType.HIDDEN, position=self.position))\n        self.result_message = f\"Created {name}\"\n        self.accept()\n\nclass NeuronPropertiesPanel(QWidget):\n    neuronChanged = pyqtSignal(str)\n    def __init__(self, design: BrainDesign, parent=None):\n        super().__init__(parent)\n        self.design = design\n        self.current_neuron = None\n        self.setup_ui()\n        \n    def setup_ui(self):\n        layout = QVBoxLayout(self)\n        self.header_label = QLabel(\"No neuron selected\")\n        layout.addWidget(self.header_label)\n        \n        form = QFormLayout()\n        self.name_edit = QLineEdit()\n        self.name_edit.editingFinished.connect(self.on_name_changed)\n        form.addRow(\"Name:\", self.name_edit)\n        \n        self.type_combo = QComboBox()\n        # [UPDATED] Added CONNECTOR to valid types\n        self.type_combo.addItems([\"CORE\", \"SENSOR\", \"INPUT\", \"OUTPUT\", \"HIDDEN\", \"CONNECTOR\"])\n        self.type_combo.currentTextChanged.connect(self.on_type_changed)\n        form.addRow(\"Type:\", self.type_combo)\n        \n        self.x_spin = QDoubleSpinBox()\n        self.x_spin.setRange(-1000, 2000)\n        self.x_spin.valueChanged.connect(self.on_pos_changed)\n        form.addRow(\"X:\", self.x_spin)\n        self.y_spin = QDoubleSpinBox()\n        self.y_spin.setRange(-500, 1000)\n        self.y_spin.valueChanged.connect(self.on_pos_changed)\n        form.addRow(\"Y:\", self.y_spin)\n        layout.addLayout(form)\n        \n        self.delete_btn = QPushButton(\"Delete Neuron\")\n        self.delete_btn.clicked.connect(self.on_delete)\n        layout.addWidget(self.delete_btn)\n        layout.addStretch()\n        self.setEnabled(False)\n\n    def set_neuron(self, name):\n        self.current_neuron = name\n        if not name:\n            self.setEnabled(False)\n            self.header_label.setText(\"No Selection\")\n            return\n        \n        self.setEnabled(True)\n        neuron = self.design.get_neuron(name)\n        self.header_label.setText(name)\n        \n        self.name_edit.blockSignals(True)\n        self.type_combo.blockSignals(True)\n        self.x_spin.blockSignals(True)\n        self.y_spin.blockSignals(True)\n        \n        self.name_edit.setText(name)\n        self.name_edit.setEnabled(not neuron.is_protected)\n        self.type_combo.setCurrentText(neuron.neuron_type.name)\n        self.type_combo.setEnabled(not neuron.is_protected and not neuron.is_sensor)\n        self.x_spin.setValue(neuron.position[0])\n        self.y_spin.setValue(neuron.position[1])\n        self.delete_btn.setEnabled(not neuron.is_protected)\n        \n        self.name_edit.blockSignals(False)\n        self.type_combo.blockSignals(False)\n        self.x_spin.blockSignals(False)\n        self.y_spin.blockSignals(False)\n    \n    def on_name_changed(self):\n        if not self.current_neuron: return\n        new_name = self.name_edit.text().strip()\n        if new_name != self.current_neuron:\n            if self.design.rename_neuron(self.current_neuron, new_name)[0]:\n                self.current_neuron = new_name\n                self.neuronChanged.emit(new_name)\n\n    def on_type_changed(self, t):\n        if self.current_neuron:\n             self.design.get_neuron(self.current_neuron).neuron_type = NeuronType[t]\n             self.neuronChanged.emit(self.current_neuron)\n\n    def on_pos_changed(self):\n        if self.current_neuron:\n            self.design.get_neuron(self.current_neuron).position = (self.x_spin.value(), self.y_spin.value())\n            self.neuronChanged.emit(self.current_neuron)\n\n    def on_delete(self):\n        if self.design.remove_neuron(self.current_neuron)[0]:\n            self.current_neuron = None\n            self.neuronChanged.emit(\"\")\n\nclass LayersPanel(QWidget):\n    layersChanged = pyqtSignal()\n    def __init__(self, design, parent=None):\n        super().__init__(parent)\n        self.design = design\n        self.setup_ui()\n    \n    def setup_ui(self):\n        l = QVBoxLayout(self)\n        self.list = QListWidget()\n        l.addWidget(self.list)\n        btn = QPushButton(\"Add Layer\")\n        btn.clicked.connect(self.add_layer)\n        l.addWidget(btn)\n        self.refresh()\n        \n    def refresh(self):\n        self.list.clear()\n        for layer in self.design.layers:\n            self.list.addItem(f\"{layer.name} ({layer.layer_type.name})\")\n\n    def add_layer(self):\n        name, ok = QInputDialog.getText(self, \"New Layer\", \"Name:\")\n        if ok and name:\n            self.design.add_layer(DesignerLayer(name, NeuronType.HIDDEN, 200))\n            self.refresh()\n            self.layersChanged.emit()\n\nclass SensorsPanel(QWidget):\n    \"\"\"\n    Panel showing available input sensors.\n    \n    Supports both built-in sensors from INPUT_SENSORS and\n    custom sensors registered by plugins via the PluginManager.\n    \"\"\"\n    sensorsChanged = pyqtSignal()\n    \n    def __init__(self, design, parent=None):\n        super().__init__(parent)\n        self.design = design\n        self._scroll_widget = None  # Store reference for dynamic updates\n        self._scroll_layout = None\n        self.setup_ui()\n    \n    def setup_ui(self):\n        l = QVBoxLayout(self)\n        \n        # Header with refresh button for plugin sensors\n        header = QHBoxLayout()\n        header.addWidget(QLabel(\"Input Sensors:\"))\n        header.addStretch()\n        \n        refresh_btn = QPushButton(\"🔄\")\n        refresh_btn.setToolTip(\"Refresh sensor list (includes plugin-registered sensors)\")\n        refresh_btn.setMaximumWidth(30)\n        refresh_btn.clicked.connect(self.rebuild_sensor_list)\n        header.addWidget(refresh_btn)\n        l.addLayout(header)\n        \n        # Scrollable sensor list\n        scroll = QScrollArea()\n        self._scroll_widget = QWidget()\n        self._scroll_layout = QVBoxLayout(self._scroll_widget)\n        self.checks = {}\n        \n        # Build initial sensor list\n        self._populate_sensors()\n        \n        scroll.setWidget(self._scroll_widget)\n        scroll.setWidgetResizable(True)\n        l.addWidget(scroll)\n        self.refresh()\n    \n    def _populate_sensors(self):\n        \"\"\"Populate the sensor checkboxes from all available sources.\"\"\"\n        # Clear existing checkboxes\n        self.checks.clear()\n        while self._scroll_layout.count():\n            item = self._scroll_layout.takeAt(0)\n            if item.widget():\n                item.widget().deleteLater()\n        \n        # Get all available sensors (built-in + plugin)\n        all_sensors = get_all_available_sensors()\n        \n        # Group by category\n        categories = {}\n        for name, info in all_sensors.items():\n            cat = info.get('category', 'other')\n            if cat not in categories:\n                categories[cat] = {}\n            categories[cat][name] = info\n        \n        # Add sensors grouped by category\n        for cat_name in sorted(categories.keys()):\n            cat_sensors = categories[cat_name]\n            \n            # Add category label if there are multiple categories\n            if len(categories) > 1:\n                cat_label = QLabel(f\"── {cat_name.title()} ──\")\n                cat_label.setStyleSheet(\"color: #888; font-size: 10px;\")\n                self._scroll_layout.addWidget(cat_label)\n            \n            for name in sorted(cat_sensors.keys()):\n                info = cat_sensors[name]\n                \n                # Create checkbox with plugin indicator\n                display_name = name\n                if info.get('plugin'):\n                    display_name = f\"🔌 {name}\"  # Plugin indicator\n                \n                cb = QCheckBox(display_name)\n                cb.setProperty('n', name)\n                cb.stateChanged.connect(self.toggled)\n                \n                # Add tooltip with description\n                tooltip = info.get('description', '')\n                if info.get('plugin'):\n                    tooltip += f\"\\n[From plugin: {info['plugin']}]\"\n                if info.get('is_binary'):\n                    tooltip += \"\\n[Binary: 0 or 100]\"\n                if tooltip:\n                    cb.setToolTip(tooltip.strip())\n                \n                self.checks[name] = cb\n                self._scroll_layout.addWidget(cb)\n        \n        # Add stretch at end\n        self._scroll_layout.addStretch()\n    \n    def rebuild_sensor_list(self):\n        \"\"\"Rebuild the sensor list to pick up newly registered plugin sensors.\"\"\"\n        self._populate_sensors()\n        self.refresh()\n        \n    def refresh(self):\n        \"\"\"Update checkbox states based on current design.\"\"\"\n        current = set(self.design.get_sensors_in_design())\n        for name, cb in self.checks.items():\n            cb.blockSignals(True)\n            cb.setChecked(name in current)\n            cb.blockSignals(False)\n\n    def toggled(self, state):\n        name = self.sender().property('n')\n        if state: \n            self.design.add_sensor(name)\n        else: \n            self.design.remove_neuron(name)\n        self.sensorsChanged.emit()\n\nclass ConnectionsTable(QWidget):\n    connectionChanged = pyqtSignal()\n    def __init__(self, design, parent=None):\n        super().__init__(parent)\n        self.design = design\n        l = QVBoxLayout(self)\n        self.table = QTableWidget()\n        self.table.setColumnCount(3)\n        self.table.setHorizontalHeaderLabels([\"Source\", \"Target\", \"Weight\"])\n        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)\n        l.addWidget(self.table)\n        self.refresh()\n        \n    def refresh(self):\n        self.table.setRowCount(len(self.design.connections))\n        for i, c in enumerate(self.design.connections):\n            self.table.setItem(i, 0, QTableWidgetItem(c.source))\n            self.table.setItem(i, 1, QTableWidgetItem(c.target))\n            self.table.setItem(i, 2, QTableWidgetItem(str(c.weight)))"
  },
  {
    "path": "src/designer_sensor_discovery.py",
    "content": "# designer_sensor_discovery.py\r\n\"\"\"\r\nUtility module for discovering available input sensors.\r\n\r\nThis module provides functions to merge built-in sensors from designer_constants\r\nwith plugin-registered custom sensors from the plugin manager.\r\n\"\"\"\r\n\r\nfrom typing import Dict, Any, Optional\r\n\r\n# Try to import designer constants\r\ntry:\r\n    from .designer_constants import INPUT_SENSORS, REQUIRED_NEURONS, BINARY_NEURONS\r\nexcept ImportError:\r\n    # Fallbacks if constants file is missing or structure is different\r\n    INPUT_SENSORS = {}\r\n    REQUIRED_NEURONS = {}\r\n    BINARY_NEURONS = set()\r\n\r\n# Localization helper\r\ntry:\r\n    from localization import tr\r\nexcept ImportError:\r\n    def tr(key, **kwargs):\r\n        return key\r\n\r\n\r\ndef get_plugin_manager() -> Optional[Any]:\r\n    \"\"\"\r\n    Try to get the PluginManager singleton instance.\r\n    \r\n    Returns:\r\n        PluginManager instance or None if not available\r\n    \"\"\"\r\n    try:\r\n        from plugin_manager import PluginManager\r\n        pm = PluginManager()\r\n        if hasattr(pm, '_initialized') and pm._initialized:\r\n            return pm\r\n    except ImportError:\r\n        pass\r\n    except Exception:\r\n        pass\r\n    return None\r\n\r\n\r\ndef get_builtin_sensors() -> Dict[str, Dict]:\r\n    \"\"\"\r\n    Get all built-in sensors from designer_constants.\r\n    \r\n    Returns:\r\n        Dict mapping sensor names to their info dicts\r\n    \"\"\"\r\n    sensors = {}\r\n    \r\n    # Add INPUT_SENSORS\r\n    # NOTE: In brain_constants, values are (x, y) tuples, not dicts.\r\n    for name, info in INPUT_SENSORS.items():\r\n        # Default values\r\n        description = tr(\"desc_builtin_sensor\").format(name=name.replace('_', ' ').title())\r\n        is_binary = name in BINARY_NEURONS\r\n        category = tr(\"desc_builtin\")\r\n        default_connections = []\r\n        \r\n        # Handle case where info is a dict (future compatibility) vs tuple (current)\r\n        if isinstance(info, dict):\r\n            description = info.get('description', description)\r\n            is_binary = info.get('is_binary', is_binary)\r\n            category = info.get('category', category)\r\n            default_connections = info.get('default_connections', default_connections)\r\n        \r\n        sensors[name] = {\r\n            'description': description,\r\n            'is_binary': is_binary,\r\n            'category': category,\r\n            'plugin': None,  # Built-in, not from a plugin\r\n            'default_connections': default_connections\r\n        }\r\n    \r\n    # Add can_see_food from REQUIRED_NEURONS if not already present\r\n    if 'can_see_food' not in sensors and 'can_see_food' in REQUIRED_NEURONS:\r\n        # Check if REQUIRED_NEURONS uses dicts or tuples\r\n        info = REQUIRED_NEURONS['can_see_food']\r\n        \r\n        description = tr(\"desc_vision_food\")\r\n        default_connections = []\r\n        \r\n        if isinstance(info, dict):\r\n             description = info.get('description', description)\r\n             default_connections = info.get('default_connections', default_connections)\r\n\r\n        sensors['can_see_food'] = {\r\n            'description': description,\r\n            'is_binary': True,\r\n            'category': tr(\"desc_vision\"),\r\n            'plugin': None,\r\n            'default_connections': default_connections\r\n        }\r\n    \r\n    return sensors\r\n\r\n\r\ndef get_plugin_sensors() -> Dict[str, Dict]:\r\n    \"\"\"\r\n    Get all custom sensors registered by plugins.\r\n    \r\n    Returns:\r\n        Dict mapping sensor names to their info dicts\r\n    \"\"\"\r\n    pm = get_plugin_manager()\r\n    if not pm or not hasattr(pm, 'get_all_neuron_handler_info'):\r\n        return {}\r\n    \r\n    sensors = {}\r\n    try:\r\n        for name, data in pm.get_all_neuron_handler_info().items():\r\n            metadata = data.get('metadata', {})\r\n            plugin_name = data.get(\"plugin\", \"unknown\")\r\n            default_desc = tr(\"desc_custom_sensor\").format(plugin=plugin_name)\r\n            \r\n            sensors[name] = {\r\n                'description': metadata.get('description', default_desc),\r\n                'is_binary': metadata.get('is_binary', False),\r\n                'category': metadata.get('category', tr(\"desc_plugin\")),\r\n                'plugin': data.get('plugin'),\r\n                'default_connections': metadata.get('default_connections', [])\r\n            }\r\n    except Exception as e:\r\n        print(f\"[Designer] Error getting plugin sensors: {e}\")\r\n        return {}\r\n    \r\n    return sensors\r\n\r\n\r\ndef get_all_available_sensors() -> Dict[str, Dict]:\r\n    \"\"\"\r\n    Get all available sensors (built-in + plugin-registered).\r\n    \r\n    Returns:\r\n        Dict mapping sensor names to their info dicts.\r\n        Each info dict contains:\r\n        - description: Human-readable description\r\n        - is_binary: Whether the sensor outputs only 0 or 100\r\n        - category: Category for grouping\r\n        - plugin: Plugin name if custom, None if built-in\r\n        - default_connections: Suggested default connections\r\n    \"\"\"\r\n    sensors = get_builtin_sensors()\r\n    \r\n    # Merge plugin sensors (plugin sensors can override built-ins)\r\n    plugin_sensors = get_plugin_sensors()\r\n    for name, info in plugin_sensors.items():\r\n        if name in sensors:\r\n            # Plugin is overriding a built-in sensor\r\n            info['_overrides_builtin'] = True\r\n        sensors[name] = info\r\n    \r\n    return sensors\r\n\r\n\r\ndef get_sensors_by_category() -> Dict[str, Dict[str, Dict]]:\r\n    \"\"\"\r\n    Get all sensors organized by category.\r\n    \r\n    Returns:\r\n        Dict mapping category names to dicts of sensors in that category\r\n    \"\"\"\r\n    sensors = get_all_available_sensors()\r\n    by_category = {}\r\n    \r\n    for name, info in sensors.items():\r\n        category = info.get('category', tr(\"desc_other\"))\r\n        if category not in by_category:\r\n            by_category[category] = {}\r\n        by_category[category][name] = info\r\n    \r\n    return by_category\r\n\r\n\r\ndef refresh_plugin_sensors():\r\n    \"\"\"\r\n    Force refresh of plugin sensors.\r\n    \r\n    Call this after plugins are loaded/enabled to update the available sensors.\r\n    \"\"\"\r\n    # Currently a no-op since we query the plugin manager dynamically,\r\n    # but could be used for caching in the future\r\n    pass\r\n\r\n\r\n# Convenience function for checking if a sensor is from a plugin\r\ndef is_plugin_sensor(sensor_name: str) -> bool:\r\n    \"\"\"Check if a sensor is from a plugin rather than built-in.\"\"\"\r\n    sensors = get_all_available_sensors()\r\n    if sensor_name in sensors:\r\n        return sensors[sensor_name].get('plugin') is not None\r\n    return False"
  },
  {
    "path": "src/designer_templates.py",
    "content": "from .designer_core import BrainDesign, DesignerLayer, DesignerNeuron\r\nfrom .designer_constants import NeuronType, REQUIRED_NEURONS, CORE_NEURONS, INPUT_SENSORS, DEFAULT_LAYER_SPACING\r\nimport random\r\n\r\nclass TemplateManager:\r\n    @staticmethod\r\n    def get_templates() -> dict:\r\n        return {\r\n            'core_only': {'name': '🟡 Required Only', 'description': '8 required neurons'},\r\n            'dosidicus_default': {'name': '🟡 Dosidicus Default', 'description': 'Standard layout'},\r\n            'full_sensors': {'name': '🟡 Full Sensor Suite', 'description': 'All sensors'},\r\n            'insomniac': {'name': '🔴 The Insomniac', 'description': 'Anxiety & Curiosity block sleep'},\r\n            'hyperactive': {'name': '🔴 The Hyperactive', 'description': 'Noise neurons overwhelm sleepiness'},\r\n            'hangry': {'name': '🔴 The Hangry', 'description': 'Hunger causes extreme rage'},\r\n            'depressive': {'name': '🔴 The Depressive', 'description': 'Resistant to happiness'},\r\n            'obsessive': {'name': '🔴 The Obsessive', 'description': 'Anxiety/Curiosity feedback loop'},\r\n        }\r\n    \r\n    @staticmethod\r\n    def create_template(key: str) -> BrainDesign:\r\n        design = BrainDesign()\r\n        \r\n        # ==========================================\r\n        # STANDARD TEMPLATES\r\n        # ==========================================\r\n        if key == 'core_only':\r\n            design.layers = [DesignerLayer(\"Sensors\", NeuronType.INPUT, 100), DesignerLayer(\"Core\", NeuronType.HIDDEN, 250)]\r\n            design.add_missing_required_neurons()\r\n            \r\n        elif key == 'full_sensors':\r\n            design.layers = [DesignerLayer(\"Input\", NeuronType.INPUT, 50), DesignerLayer(\"Core\", NeuronType.HIDDEN, 200), DesignerLayer(\"Out\", NeuronType.OUTPUT, 350)]\r\n            design.add_missing_required_neurons()\r\n            design.add_all_sensors()\r\n\r\n        # ==========================================\r\n        # PATHOLOGICAL / SPECIFIC BEHAVIORS\r\n        # ==========================================\r\n        elif key == 'insomniac':\r\n            # A brain where active states aggressively inhibit sleep\r\n            design.layers = [DesignerLayer(\"Sensors\", NeuronType.INPUT, 150), DesignerLayer(\"Racing Mind\", NeuronType.HIDDEN, 200), DesignerLayer(\"State\", NeuronType.OUTPUT, 350)]\r\n            design.add_missing_required_neurons()\r\n            \r\n            # The Insomniac Logic:\r\n            # Any active emotion makes it impossible to sleep (Strong Inhibition)\r\n            design.add_connection(\"anxiety\", \"sleepiness\", -0.95)\r\n            design.add_connection(\"curiosity\", \"sleepiness\", -0.80)\r\n            design.add_connection(\"hunger\", \"sleepiness\", -0.50)\r\n            design.add_connection(\"can_see_food\", \"sleepiness\", -0.50)\r\n            \r\n            # Weak positive feedback means once awake, they stay awake\r\n            design.add_connection(\"sleepiness\", \"anxiety\", 0.2) # Being tired makes them anxious\r\n\r\n        elif key == 'hyperactive':\r\n            # A brain with a \"Noise\" layer that floods the system\r\n            design.layers = [\r\n                DesignerLayer(\"Vision\", NeuronType.INPUT, 150), \r\n                DesignerLayer(\"Core\", NeuronType.HIDDEN, 200), \r\n                DesignerLayer(\"Noise\", NeuronType.HIDDEN, 300), # Extra space for noise neurons\r\n                DesignerLayer(\"Output\", NeuronType.OUTPUT, 400)\r\n            ]\r\n            design.add_missing_required_neurons()\r\n\r\n            # Create 12 \"Noise\" neurons that act as a distraction generator\r\n            noise_neurons = [f\"noise_{i}\" for i in range(12)]\r\n            for n in noise_neurons:\r\n                design.add_neuron(n, \"Noise\")\r\n                \r\n                # The \"Overwhelm\" Logic:\r\n                # These neurons excite sleepiness (crash) and curiosity (distraction)\r\n                # Randomize weights slightly so they aren't identical\r\n                w_sleep = random.uniform(0.6, 0.9)\r\n                w_curiosity = random.uniform(0.5, 0.8)\r\n                \r\n                design.add_connection(n, \"sleepiness\", w_sleep)   # Floods sleep buffer\r\n                design.add_connection(n, \"curiosity\", w_curiosity) # Forces erratic attention\r\n                \r\n                # Connect noise to itself for chaotic sustainability\r\n                if random.random() > 0.5:\r\n                    target = random.choice(noise_neurons)\r\n                    design.add_connection(n, target, 0.4)\r\n\r\n        elif key == 'hangry':\r\n            # Metabolic mood disorder\r\n            design.layers = [DesignerLayer(\"Sensors\", NeuronType.INPUT, 100), DesignerLayer(\"Gut-Brain\", NeuronType.HIDDEN, 200)]\r\n            design.add_missing_required_neurons()\r\n            \r\n            # Hunger overrides all positive emotions and triggers anxiety/stress\r\n            design.add_connection(\"hunger\", \"happiness\", -1.0) # Cannot be happy if hungry\r\n            design.add_connection(\"hunger\", \"satisfaction\", -1.0)\r\n            design.add_connection(\"hunger\", \"anxiety\", 0.9) # Extreme stress when hungry\r\n            design.add_connection(\"can_see_food\", \"anxiety\", 0.5) # Seeing food but not eating it is stressful\r\n\r\n        elif key == 'depressive':\r\n            # Anhedonia model: High resistance to positive weights\r\n            design.layers = [DesignerLayer(\"Input\", NeuronType.INPUT, 150), DesignerLayer(\"Gray\", NeuronType.HIDDEN, 200)]\r\n            design.add_missing_required_neurons()\r\n            \r\n            # Hard to get happy, easy to get sad\r\n            design.add_connection(\"satisfaction\", \"happiness\", 0.1) # Drastically reduced reward\r\n            design.add_connection(\"curiosity\", \"happiness\", 0.0) # No joy from exploration\r\n            design.add_connection(\"anxiety\", \"happiness\", -0.8) # Anxiety kills happiness easily\r\n            design.add_connection(\"sleepiness\", \"happiness\", -0.6)\r\n\r\n        elif key == 'obsessive':\r\n            # Feedback loop central\r\n            design.layers = [DesignerLayer(\"Input\", NeuronType.INPUT, 150), DesignerLayer(\"Loop\", NeuronType.HIDDEN, 200)]\r\n            design.add_missing_required_neurons()\r\n            \r\n            # Tight feedback loop between anxiety and curiosity (Worrying/Checking)\r\n            design.add_connection(\"anxiety\", \"curiosity\", 0.7) # Worry makes you check things\r\n            design.add_connection(\"curiosity\", \"anxiety\", 0.5) # Checking things makes you worry\r\n            design.add_connection(\"sleepiness\", \"anxiety\", 0.4) # Being tired makes the loops worse\r\n\r\n        else: # Default Dosidicus\r\n            design.layers = [DesignerLayer(\"Vision\", NeuronType.INPUT, 200), DesignerLayer(\"Stats\", NeuronType.HIDDEN, 81), DesignerLayer(\"Emotions\", NeuronType.OUTPUT, 385)]\r\n            design.add_missing_required_neurons()\r\n            # Default connections\r\n            design.add_connection(\"can_see_food\", \"hunger\", 0.3)\r\n            design.add_connection(\"can_see_food\", \"happiness\", 0.2)\r\n            design.add_connection(\"hunger\", \"satisfaction\", -0.5)\r\n            \r\n\r\n        return design"
  },
  {
    "path": "src/designer_window.py",
    "content": "\"\"\"\r\nBrain Designer Window - Main application window for designing custom neural networks.\r\n\r\nThis module provides a visual editor for creating and editing squid brain configurations\r\nincluding neurons, connections, layers, sensors, and output bindings.\r\n\"\"\"\r\n\r\nimport sys\r\nfrom PyQt5.QtWidgets import (\r\n    QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QTabWidget, QStatusBar,\r\n    QAction, QToolBar, QLabel, QComboBox, QPushButton, QMessageBox, QFileDialog,\r\n    QDialog, QInputDialog, QFrame, QSizePolicy, QToolButton, QMenu, QSplitter\r\n)\r\nfrom PyQt5.QtGui import (\r\n    QKeySequence, QIcon, QFont, QFontMetrics, QPainter, QColor, QPalette,\r\n    QTextDocument, QAbstractTextDocumentLayout\r\n)\r\n# [FIX] Added QRectF to imports\r\nfrom PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QRect, QRectF, QSizeF, pyqtSignal\r\n\r\nfrom .designer_logging import get_logger, log_exceptions, safe_call, OperationLogger\r\nfrom .designer_core import BrainDesign\r\nfrom .designer_canvas import BrainCanvas\r\n\r\n# Import brain state bridge for game communication\r\ntry:\r\n    from .brain_state_bridge import (\r\n        is_game_running,\r\n        import_brain_state_for_designer,\r\n        convert_to_brain_design,\r\n        export_design_to_game\r\n    )\r\n    _HAS_BRAIN_BRIDGE = True\r\nexcept ImportError:\r\n    _HAS_BRAIN_BRIDGE = False\r\n    print(\"[BrainDesigner] Warning: brain_state_bridge not found, live import disabled\")\r\nfrom .designer_panels import (\r\n    LayersPanel, SensorsPanel, NeuronPropertiesPanel, ConnectionsTable, AddNeuronDialog\r\n)\r\nfrom .designer_templates import TemplateManager\r\nfrom .designer_dialogs import SparseNetworkDialog, ActivationEditorDialog\r\nfrom .designer_network_generator import SparseNetworkGenerator\r\n\r\n# Import the new outputs panel\r\ntry:\r\n    from .designer_outputs_panel import NeuronOutputsPanel\r\n    _HAS_OUTPUTS_PANEL = True\r\nexcept ImportError:\r\n    _HAS_OUTPUTS_PANEL = False\r\n    print(\"[BrainDesigner] Warning: designer_outputs_panel not found, outputs tab disabled\")\r\n\r\n\r\nclass ScrollingTicker(QWidget):\r\n    \"\"\"A widget that smoothly scrolls rich text (HTML) horizontally.\"\"\"\r\n    \r\n    def __init__(self, text, parent=None):\r\n        super().__init__(parent)\r\n        self.text = text\r\n        self.offset = 0\r\n        \r\n        # Prepare text document for HTML rendering\r\n        self.doc = QTextDocument()\r\n        self.doc.setDefaultFont(self.font())\r\n        self.doc.setHtml(self.text)\r\n        self.doc.setTextWidth(-1)  # No wrap\r\n        \r\n        # Calculate dimensions\r\n        self.text_width = self.doc.idealWidth()\r\n        self.text_height = self.doc.size().height()  # FIX: Call method with ()\r\n        \r\n        # Separator\r\n        self.sep_doc = QTextDocument()\r\n        self.sep_doc.setDefaultFont(self.font())\r\n        self.sep_doc.setHtml(\"<span style='color:gray'>   •   </span>\")\r\n        self.sep_doc.setTextWidth(-1)\r\n        self.sep_width = self.sep_doc.idealWidth()\r\n        \r\n        # Timer - don't start it yet\r\n        self.timer = QTimer(self)\r\n        self.timer.timeout.connect(self.scroll_text)\r\n        self.timer.setInterval(30)  # ~33fps\r\n        \r\n        # Style\r\n        self.setStyleSheet(\"background-color: transparent;\")\r\n        self.setFixedHeight(30)\r\n    \r\n    def showEvent(self, event):\r\n        \"\"\"Start timer when widget becomes visible.\"\"\"\r\n        super().showEvent(event)\r\n        # Start timer on first show (prevents premature updates)\r\n        if not self.timer.isActive():\r\n            self.timer.start()\r\n    \r\n    def hideEvent(self, event):\r\n        \"\"\"Stop timer when widget becomes hidden.\"\"\"\r\n        super().hideEvent(event)\r\n        self.timer.stop()\r\n    \r\n    def scroll_text(self):\r\n        \"\"\"Update scroll offset and trigger repaint.\"\"\"\r\n        self.offset -= 1\r\n        if self.offset <= -(self.text_width + self.sep_width):\r\n            self.offset = 0\r\n        self.update()\r\n    \r\n    def paintEvent(self, event):\r\n        \"\"\"Paint the scrolling text with proper painter lifecycle.\"\"\"\r\n        painter = QPainter(self)\r\n        \r\n        # Ensure painter is valid before proceeding\r\n        if not painter.isActive():\r\n            return\r\n        \r\n        # Removed the try...finally block - Qt handles painter lifecycle automatically\r\n        painter.setRenderHint(QPainter.Antialiasing)\r\n        \r\n        # Center vertically\r\n        y_pos = (self.height() - self.text_height) / 2\r\n        \r\n        # Draw copies of the text until we fill the width\r\n        current_x = self.offset\r\n        while current_x < self.width():\r\n            # Draw Main Text\r\n            painter.save()\r\n            painter.translate(current_x, y_pos)\r\n            self.doc.drawContents(painter, QRectF(0, 0, self.text_width, self.text_height))\r\n            painter.restore()\r\n            \r\n            current_x += self.text_width\r\n            \r\n            # Draw Separator\r\n            painter.save()\r\n            painter.translate(current_x, y_pos)\r\n            self.sep_doc.drawContents(painter, QRectF(0, 0, self.sep_width, self.text_height))\r\n            painter.restore()\r\n            \r\n            current_x += self.sep_width\r\n        \r\n        # REMOVED: No need for finally block or manual painter.end()\r\n\r\n\r\nclass BrainDesignerWindow(QMainWindow):\r\n    \"\"\"Main window for the Brain Designer application.\"\"\"\r\n\r\n    # Signal to notify parent when user wants to exit/return\r\n    exitRequested = pyqtSignal()\r\n\r\n    def __init__(self, parent=None, embedded_mode=False):\r\n        super().__init__(parent)\r\n        self.logger = get_logger(\"brain_designer.window\")\r\n        self.logger.info(\"Initializing BrainDesignerWindow\")\r\n        \r\n        self.embedded_mode = embedded_mode\r\n\r\n        try:\r\n            self.design = BrainDesign()\r\n            self.design.add_missing_required_neurons()\r\n\r\n            self.setWindowTitle(\"Brain Designer - Dosidicus-2\")\r\n            self.setMinimumSize(1280, 900)\r\n\r\n            with OperationLogger(\"Setting up UI\", self.logger):\r\n                self.setup_ui()\r\n\r\n            with OperationLogger(\"Setting up menus\", self.logger):\r\n                self.setup_menus()\r\n\r\n            with OperationLogger(\"Setting up toolbar\", self.logger):\r\n                self.setup_toolbar()\r\n\r\n            # Only auto-generate if NOT embedded (embedded mode will load existing state)\r\n            if not self.embedded_mode:\r\n                with OperationLogger(\"Initializing brain network\", self.logger):\r\n                    self._imported_from_game = False\r\n                    if not self._try_import_from_game():\r\n                        # No game running, generate random network\r\n                        self.generate_initial_network()\r\n                \r\n                # Show import notification if applicable\r\n                if self._imported_from_game:\r\n                    self._show_import_notification()\r\n\r\n            # Force UI refresh\r\n            self.refresh_all()\r\n            self.update_status()\r\n            self.logger.info(\"BrainDesignerWindow initialized successfully\")\r\n\r\n        except Exception as e:\r\n            self.logger.critical(f\"Failed to initialize window: {e}\", exc_info=True)\r\n            raise\r\n\r\n    # ==========================================================================\r\n    # INITIALIZATION AND SETUP\r\n    # ==========================================================================\r\n\r\n    def setup_ui(self):\r\n        \"\"\"Setup the main UI layout.\"\"\"\r\n        # Main splitter layout\r\n        self.splitter = QSplitter(Qt.Horizontal)\r\n        self.setCentralWidget(self.splitter)\r\n\r\n        # === LEFT/CENTER: CANVAS AREA ===\r\n        canvas_wrapper = QWidget()\r\n        canvas_wrapper.setMinimumWidth(500)\r\n        \r\n        canvas_container = QVBoxLayout(canvas_wrapper)\r\n        canvas_container.setContentsMargins(0, 0, 0, 0)\r\n        canvas_container.setSpacing(5)\r\n\r\n        # Canvas toolbar\r\n        canvas_toolbar = self.create_canvas_toolbar()\r\n        canvas_container.addWidget(canvas_toolbar)\r\n\r\n        # Main canvas\r\n        self.canvas = BrainCanvas(self.design)\r\n        self.canvas.neuronSelected.connect(self.on_neuron_selected)\r\n        self.canvas.connectionCreated.connect(self.on_connection_created)\r\n        self.canvas.connectionSelected.connect(self.on_connection_selected)\r\n        self.canvas.weightChanged.connect(self.on_weight_changed)\r\n        self.canvas.connectionDeleted.connect(self.on_connection_deleted)\r\n\r\n        canvas_container.addWidget(self.canvas)\r\n\r\n        # Canvas help bar\r\n        help_bar = self.create_help_bar()\r\n        canvas_container.addWidget(help_bar)\r\n\r\n        self.splitter.addWidget(canvas_wrapper)\r\n\r\n        # === RIGHT PANEL (Tabbed Panels) ===\r\n        self.right_panel = QTabWidget()\r\n        self.right_panel.setTabPosition(QTabWidget.West)  # Vertical tabs on the left edge\r\n        self.right_panel.setMinimumWidth(350)\r\n\r\n        # 1. Layers Panel\r\n        self.layers_panel = LayersPanel(self.design)\r\n        self.layers_panel.layersChanged.connect(self.on_design_changed)\r\n        self.right_panel.addTab(self.layers_panel, \"Layers\")\r\n\r\n        # 2. Sensors Panel (Input neurons)\r\n        self.sensors_panel = SensorsPanel(self.design)\r\n        self.sensors_panel.sensorsChanged.connect(self.on_design_changed)\r\n        self.right_panel.addTab(self.sensors_panel, \"Sensors\")\r\n\r\n        # 3. Properties Panel\r\n        self.props_panel = NeuronPropertiesPanel(self.design)\r\n        self.props_panel.neuronChanged.connect(self.on_design_changed)\r\n        self.right_panel.addTab(self.props_panel, \"Properties\")\r\n\r\n        # 4. Connections Table\r\n        self.connections_table = ConnectionsTable(self.design)\r\n        self.right_panel.addTab(self.connections_table, \"Connections\")\r\n\r\n        # 5. Outputs Panel (Actuator neurons) - NEW!\r\n        if _HAS_OUTPUTS_PANEL:\r\n            self.outputs_panel = NeuronOutputsPanel(self.design)\r\n            self.outputs_panel.outputsChanged.connect(self.on_design_changed)\r\n            self.right_panel.addTab(self.outputs_panel, \"Outputs\")\r\n        else:\r\n            self.outputs_panel = None\r\n\r\n        self.splitter.addWidget(self.right_panel)\r\n        \r\n        # Set Sensors as the default tab (index 1)\r\n        self.right_panel.setCurrentIndex(1)\r\n\r\n        # Set initial splitter sizes (approx 70% canvas, 30% sidebar)\r\n        self.splitter.setCollapsible(0, False)\r\n        self.splitter.setCollapsible(1, False)\r\n        \r\n        self.splitter.setStretchFactor(0, 1)\r\n        self.splitter.setStretchFactor(1, 0)\r\n        \r\n        self.splitter.setSizes([850, 400])\r\n\r\n        # Status bar\r\n        self.status_bar = QStatusBar()\r\n        self.setStatusBar(self.status_bar)\r\n\r\n    def create_canvas_toolbar(self) -> QFrame:\r\n        \"\"\"Create the toolbar above the canvas.\"\"\"\r\n        toolbar = QFrame()\r\n        toolbar.setFrameStyle(QFrame.StyledPanel)\r\n        toolbar.setMaximumHeight(45)\r\n        layout = QHBoxLayout(toolbar)\r\n        layout.setContentsMargins(8, 4, 8, 4)\r\n\r\n        # Generate button (prominent)\r\n        generate_btn = QPushButton(\"🎲 Generate\")\r\n        generate_btn.setToolTip(\"Generate random connections between core neurons\")\r\n        generate_btn.setStyleSheet(\"\"\"\r\n            QPushButton {\r\n                background-color: #2196F3;\r\n                color: white;\r\n                font-weight: bold;\r\n                padding: 6px 16px;\r\n                border-radius: 4px;\r\n            }\r\n            QPushButton:hover {\r\n                background-color: #1976D2;\r\n            }\r\n        \"\"\")\r\n        generate_btn.clicked.connect(self.show_sparse_network_dialog)\r\n        layout.addWidget(generate_btn)\r\n\r\n        # Quick generate menu\r\n        quick_gen_btn = QToolButton()\r\n        quick_gen_btn.setText(\"▼\")\r\n        quick_gen_btn.setPopupMode(QToolButton.InstantPopup)\r\n        quick_menu = QMenu(quick_gen_btn)\r\n\r\n        generator = SparseNetworkGenerator()\r\n        for key, info in generator.get_preset_styles().items():\r\n            action = quick_menu.addAction(f\"{info['name']} - {info['description']}\")\r\n            action.setData(key)\r\n            action.triggered.connect(lambda checked, k=key: self.quick_generate(k))\r\n\r\n        quick_gen_btn.setMenu(quick_menu)\r\n        layout.addWidget(quick_gen_btn)\r\n        \r\n        # Quick dice button - instant random generation\r\n        dice_btn = QPushButton(\"🎲\")\r\n        dice_btn.setToolTip(\"Instantly shuffle positions and generate a chaotic network (no dialog)\")\r\n        dice_btn.setFixedWidth(40)\r\n        dice_btn.setStyleSheet(\"\"\"\r\n            QPushButton {\r\n                background-color: #FF9800;\r\n                color: white;\r\n                font-size: 18px;\r\n                padding: 6px;\r\n                border-radius: 4px;\r\n            }\r\n            QPushButton:hover {\r\n                background-color: #F57C00;\r\n            }\r\n        \"\"\")\r\n        dice_btn.clicked.connect(self.instant_random_generate)\r\n        layout.addWidget(dice_btn)\r\n\r\n        layout.addSpacing(20)\r\n\r\n        # Divider\r\n        divider = QFrame()\r\n        divider.setFrameShape(QFrame.VLine)\r\n        divider.setFrameShadow(QFrame.Sunken)\r\n        layout.addWidget(divider)\r\n\r\n        layout.addSpacing(10)\r\n\r\n        # + Neuron button (colorful, prominent)\r\n        add_neuron_btn = QPushButton(\"➕ Neuron\")\r\n        add_neuron_btn.setToolTip(\"Add a new neuron (Shift+N)\")\r\n        add_neuron_btn.setShortcut(\"Shift+N\")\r\n        add_neuron_btn.setStyleSheet(\"\"\"\r\n            QPushButton {\r\n                background-color: #4CAF50;\r\n                color: white;\r\n                font-weight: bold;\r\n                padding: 6px 14px;\r\n                border-radius: 4px;\r\n            }\r\n            QPushButton:hover {\r\n                background-color: #43A047;\r\n            }\r\n        \"\"\")\r\n        add_neuron_btn.clicked.connect(self.show_add_neuron_dialog)\r\n        layout.addWidget(add_neuron_btn)\r\n\r\n        layout.addSpacing(10)\r\n\r\n        # Auto-fix button\r\n        fix_btn = QPushButton(\"🔧 Auto-Fix\")\r\n        fix_btn.setToolTip(\"Automatically fix orphan neurons and connectivity issues\")\r\n        fix_btn.clicked.connect(self.run_auto_fix)\r\n        #layout.addWidget(fix_btn)\r\n\r\n        # Validate button\r\n        validate_btn = QPushButton(\"✓ Validate\")\r\n        validate_btn.setToolTip(\"Check design for issues\")\r\n        validate_btn.clicked.connect(self.check_status)\r\n        #layout.addWidget(validate_btn)\r\n\r\n        # Placeholder for sync button - will be added later if game is detected\r\n        self._sync_btn_container = QFrame()\r\n        self._sync_btn_layout = QHBoxLayout(self._sync_btn_container)\r\n        self._sync_btn_layout.setContentsMargins(0, 0, 0, 0)\r\n        self._sync_btn_container.hide()  # Hidden by default\r\n        layout.addWidget(self._sync_btn_container)\r\n        \r\n        # Add RETURN button if embedded\r\n        if self.embedded_mode:\r\n            layout.addSpacing(20)\r\n            return_btn = QPushButton(\"💾 Save & Return to Game\")\r\n            return_btn.setToolTip(\"Save changes and return to game view\")\r\n            return_btn.setStyleSheet(\"\"\"\r\n                QPushButton {\r\n                    background-color: #673AB7;\r\n                    color: white;\r\n                    font-weight: bold;\r\n                    padding: 6px 14px;\r\n                    border-radius: 4px;\r\n                }\r\n                QPushButton:hover {\r\n                    background-color: #5E35B1;\r\n                }\r\n            \"\"\")\r\n            return_btn.clicked.connect(self.exitRequested.emit)\r\n            layout.addWidget(return_btn)\r\n\r\n        layout.addStretch()\r\n\r\n        # Clear connections button\r\n        clear_btn = QPushButton(\"🗑 Clear Connections\")\r\n        clear_btn.setToolTip(\"Remove all connections (keeps neurons)\")\r\n        clear_btn.setStyleSheet(\"color: #d32f2f;\")\r\n        clear_btn.clicked.connect(self.clear_all_connections)\r\n        layout.addWidget(clear_btn)\r\n\r\n        return toolbar\r\n\r\n    def create_help_bar(self) -> QFrame:\r\n        \"\"\"Create the help bar below the canvas with scrolling ticker.\"\"\"\r\n        help_bar = QFrame()\r\n        help_bar.setFrameStyle(QFrame.StyledPanel)\r\n        help_bar.setMaximumHeight(30)\r\n        layout = QHBoxLayout(help_bar)\r\n        layout.setContentsMargins(0, 0, 0, 0)\r\n\r\n        # Create scrolling ticker with comprehensive shortcuts\r\n        self.help_ticker = ScrollingTicker(\r\n            \"<span style='color:#333'>💡 <b> Left-Drag</b> from neuron to create connection </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+Drag</b> neuron to move it </span>\"\r\n            \"<span style='color:#333'><b> Right-Drag</b> to pan canvas </span>\"\r\n            \"<span style='color:#333'><b> Scroll Wheel</b> to zoom (or adjust weight on connection) </span>\"\r\n            \"<span style='color:#333'><b> Double-Click</b> connection to edit weight </span>\"\r\n            \"<span style='color:#333'><b> Click</b> neuron/connection to select </span>\"\r\n            \"<span style='color:#333'><b> Del</b> to delete selected </span>\"\r\n            \"<span style='color:#333'><b> Space</b> to reverse connection direction </span>\"\r\n            \"<span style='color:#333'><b> +/-</b> keys to adjust weight (Shift for larger steps) </span>\"\r\n            \"<span style='color:#333'><b> Page Up/Down</b> to adjust weight (large steps) </span>\"\r\n            \"<span style='color:#333'><b> Shift+N</b> to add neuron </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+S</b> to save </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+O</b> to open </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+E</b> to export </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+N</b> for new design </span>\"\r\n            \"<span style='color:#333'><b> Ctrl+G</b> to generate network </span>\"\r\n            \"<span style='color:#333'>🎲 <b>Dice button</b> for instant chaotic shuffle & generation </span>\"\r\n            \"<span style='color:#333'><b>Outputs tab</b> to bind neurons to squid behaviors </span>\"\r\n        )\r\n        layout.addWidget(self.help_ticker)\r\n\r\n        return help_bar\r\n\r\n    def setup_menus(self):\r\n        \"\"\"Setup the menu bar.\"\"\"\r\n        # In embedded mode, menus might not show up if parent is not main window.\r\n        # But for QMainWindow they usually work.\r\n        menu = self.menuBar()\r\n\r\n        # File menu\r\n        file_menu = menu.addMenu(\"File\")\r\n        \r\n        if self.embedded_mode:\r\n            return_action = QAction(\"Save & Return to Game\", self)\r\n            return_action.setShortcut(\"Ctrl+Return\")\r\n            return_action.triggered.connect(self.exitRequested.emit)\r\n            file_menu.addAction(return_action)\r\n            file_menu.addSeparator()\r\n\r\n        new_action = QAction(\"New Design\", self)\r\n        new_action.setShortcut(\"Ctrl+N\")\r\n        new_action.triggered.connect(self.new_design)\r\n        file_menu.addAction(new_action)\r\n\r\n        file_menu.addSeparator()\r\n\r\n        save_action = QAction(\"Save...\", self)\r\n        save_action.setShortcut(\"Ctrl+S\")\r\n        save_action.triggered.connect(self.save_design)\r\n        #file_menu.addAction(save_action)\r\n\r\n        export_action = QAction(\"Export for Dosidicus...\", self)\r\n        export_action.setShortcut(\"Ctrl+E\")\r\n        export_action.triggered.connect(self.export_design)\r\n        file_menu.addAction(export_action)\r\n\r\n        file_menu.addSeparator()\r\n\r\n        open_action = QAction(\"Open...\", self)\r\n        open_action.setShortcut(\"Ctrl+O\")\r\n        open_action.triggered.connect(self.open_design)\r\n        file_menu.addAction(open_action)\r\n\r\n        # Edit menu\r\n        edit_menu = menu.addMenu(\"Edit\")\r\n\r\n        generate_action = QAction(\"Generate Network...\", self)\r\n        generate_action.setShortcut(\"Ctrl+G\")\r\n        generate_action.triggered.connect(self.show_sparse_network_dialog)\r\n        edit_menu.addAction(generate_action)\r\n\r\n        edit_menu.addSeparator()\r\n\r\n        auto_fix = QAction(\"Auto-Fix Connectivity\", self)\r\n        auto_fix.triggered.connect(self.run_auto_fix)\r\n        edit_menu.addAction(auto_fix)\r\n\r\n        validate_action = QAction(\"Validate Design\", self)\r\n        validate_action.triggered.connect(self.check_status)\r\n        #edit_menu.addAction(validate_action)\r\n\r\n        edit_menu.addSeparator()\r\n\r\n        clear_conn_action = QAction(\"Clear All\", self)\r\n        clear_conn_action.triggered.connect(self.clear_all_connections)\r\n        edit_menu.addAction(clear_conn_action)\r\n        \r\n        # Clear outputs action\r\n        if _HAS_OUTPUTS_PANEL:\r\n            clear_outputs_action = QAction(\"Clear all Bindings\", self)\r\n            clear_outputs_action.triggered.connect(self.clear_all_outputs)\r\n            edit_menu.addAction(clear_outputs_action)\r\n\r\n        # Templates menu\r\n        tpl_menu = menu.addMenu(\"Templates\")\r\n        for key, info in TemplateManager.get_templates().items():\r\n            a = QAction(info['name'], self)\r\n            a.setData(key)\r\n            a.triggered.connect(self.load_template)\r\n            tpl_menu.addAction(a)\r\n\r\n        # Network generation presets\r\n        gen_menu = menu.addMenu(\"Generate\")\r\n\r\n        gen_dialog_action = QAction(\"🎲 Generate...\", self)\r\n        gen_dialog_action.setShortcut(\"Ctrl+G\")\r\n        gen_dialog_action.triggered.connect(self.show_sparse_network_dialog)\r\n        gen_menu.addAction(gen_dialog_action)\r\n\r\n        gen_menu.addSeparator()\r\n\r\n        generator = SparseNetworkGenerator()\r\n        for key, info in generator.get_preset_styles().items():\r\n            action = QAction(f\"{info['name']}\", self)\r\n            action.setToolTip(info['description'])\r\n            action.setData(key)\r\n            action.triggered.connect(lambda checked, k=key: self.quick_generate(k))\r\n            gen_menu.addAction(action)\r\n\r\n    def setup_toolbar(self):\r\n        \"\"\"Setup the main toolbar.\"\"\"\r\n        toolbar = QToolBar(\"Main\")\r\n        toolbar.setMovable(False)\r\n        self.addToolBar(toolbar)\r\n\r\n        # File actions\r\n        toolbar.addAction(\"📂 Open\", self.open_design)\r\n        toolbar.addAction(\"💾 Save\", self.save_design)\r\n\r\n        toolbar.addSeparator()\r\n\r\n        if _HAS_BRAIN_BRIDGE:\r\n            push_action = QAction(\"🚀 Push to Game\", self)\r\n            push_action.setToolTip(\"Export current design directly to the running Dosidicus game\")\r\n            push_action.triggered.connect(self.push_to_game)\r\n            toolbar.addAction(push_action)\r\n            toolbar.addSeparator()\r\n\r\n        # Template dropdown\r\n        toolbar.addAction(\"📋 Templates\", self.show_template_menu)\r\n\r\n    def push_to_game(self):\r\n        \"\"\"Export current design, state, and bindings directly to the running game.\"\"\"\r\n        if not _HAS_BRAIN_BRIDGE:\r\n            return\r\n\r\n        if not is_game_running():\r\n            QMessageBox.warning(self, \"Game Not Found\", \r\n                              \"Dosidicus does not appear to be running.\\nStart the game to push designs.\")\r\n            return\r\n\r\n        try:\r\n            # 1. Ensure bindings are up to date\r\n            if self.outputs_panel:\r\n                self.design.output_bindings = self.outputs_panel.export_bindings()\r\n\r\n            # 2. Convert to the format the game expects\r\n            data = self.design.to_dosidicus_format()\r\n            \r\n            # 3. Push via bridge\r\n            if export_design_to_game(data):\r\n                self.status_bar.showMessage(\"Design pushed to running game\", 3000)\r\n                self.tamagotchi_logic.show_message(\"Custom Brain was pushed from Designer\")\r\n            else:\r\n                QMessageBox.warning(self, \"Export Failed\", \"Could not write bridge file.\")\r\n                \r\n        except Exception as e:\r\n            self.logger.error(f\"Push to game failed: {e}\", exc_info=True)\r\n            QMessageBox.critical(self, \"Error\", f\"Failed to push design:\\n{e}\")\r\n\r\n    # ==========================================================================\r\n    # DATA LOADING AND CONVERSION\r\n    # ==========================================================================\r\n    \r\n    def load_from_brain_widget_state(self, state):\r\n        \"\"\"\r\n        Load design from a brain widget state dictionary.\r\n        Used for in-process switching with robust fallback.\r\n        \"\"\"\r\n        try:\r\n            imported_design = None\r\n            \r\n            # Attempt 1: Try using the bridge if available\r\n            try:\r\n                from .brain_state_bridge import convert_to_brain_design\r\n                imported_design = convert_to_brain_design(state)\r\n            except ImportError:\r\n                # Bridge not found, proceed to fallback\r\n                pass\r\n            \r\n            # Attempt 2: Direct conversion using DesignerCore (Fallback)\r\n            if imported_design is None:\r\n                # If bridge failed or returned None, use the core method directly\r\n                # This ensures we don't need the bridge to be active/present\r\n                from .designer_core import BrainDesign\r\n                # Try dosidicus format first (likely coming from BrainWidget)\r\n                imported_design = BrainDesign.from_dosidicus_format(state)\r\n            \r\n            if imported_design is None:\r\n                self.logger.warning(\"Could not convert state to BrainDesign\")\r\n                return False\r\n            \r\n            # Apply the design\r\n            self.design = imported_design\r\n            self.refresh_all()\r\n            \r\n            # Force canvas to center on the new nodes\r\n            if hasattr(self, 'canvas'):\r\n                self.canvas.center_on_neurons()\r\n            \r\n            self.logger.info(\r\n                f\"Loaded design from memory: {len(self.design.neurons)} neurons\"\r\n            )\r\n            return True\r\n            \r\n        except Exception as e:\r\n            self.logger.error(f\"Error loading from state: {e}\", exc_info=True)\r\n            return False\r\n\r\n    def get_current_design_state(self):\r\n        \"\"\"\r\n        Get the current design as a state dictionary compatible with BrainWidget.\r\n        \"\"\"\r\n        try:\r\n            # Sync output bindings first\r\n            if self.outputs_panel:\r\n                self.design.output_bindings = self.outputs_panel.export_bindings()\r\n            \r\n            # Export to dosidicus format (compatible with loader)\r\n            return self.design.to_dosidicus_format()\r\n        except Exception as e:\r\n            self.logger.error(f\"Error exporting state: {e}\", exc_info=True)\r\n            return None\r\n\r\n    # ==========================================================================\r\n    # REFRESH AND STATUS\r\n    # ==========================================================================\r\n\r\n    def refresh_all(self):\r\n        \"\"\"Refresh all panels with current design.\"\"\"\r\n        self.canvas.design = self.design\r\n        self.props_panel.design = self.design\r\n        self.layers_panel.design = self.design\r\n        self.sensors_panel.design = self.design\r\n        self.connections_table.design = self.design\r\n        \r\n        # Refresh outputs panel and load bindings from design\r\n        if self.outputs_panel:\r\n            self.outputs_panel.design = self.design\r\n            self.outputs_panel.load_bindings(self.design.output_bindings)\r\n        \r\n        self.on_design_changed()\r\n\r\n    def update_status(self):\r\n        \"\"\"Update the status bar.\"\"\"\r\n        stats = self.design.get_stats()\r\n        \r\n        # Build status message\r\n        status_parts = [\r\n            f\"Neurons: {stats['total_neurons']}\",\r\n            f\"Connections: {stats['connections']}\",\r\n            f\"Required: {'✓' if stats['has_all_required'] else '✗'}\"\r\n        ]\r\n        \r\n        # Add output bindings count if available\r\n        if self.outputs_panel:\r\n            binding_count = len(self.outputs_panel.bindings)\r\n            if binding_count > 0:\r\n                status_parts.append(f\"Outputs: {binding_count}\")\r\n        \r\n        self.status_bar.showMessage(\" | \".join(status_parts))\r\n\r\n    # ==========================================================================\r\n    # NETWORK GENERATION AND CLEARING\r\n    # ==========================================================================\r\n\r\n    def generate_initial_network(self):\r\n        \"\"\"Generate a random network on startup without user interaction.\"\"\"\r\n        import random\r\n        try:\r\n            generator = SparseNetworkGenerator()\r\n            \r\n            # 50% chance of minimalist configuration (sparse, no feedback)\r\n            # This creates a variety of startup experiences for the user\r\n            if random.random() < 0.5:\r\n                density = 0.5\r\n                include_feedback = False\r\n                mode = \"minimalist\"\r\n            else:\r\n                density = 1.0\r\n                include_feedback = True\r\n                mode = \"balanced\"\r\n\r\n            count, _ = generator.generate_for_design(\r\n                self.design,\r\n                clear_existing=True,\r\n                density=density,\r\n                include_feedback=include_feedback,\r\n                silent=True\r\n            )\r\n            self.logger.debug(f\"Generated initial network ({mode}) with {count} connections\")\r\n        except Exception as e:\r\n            self.logger.warning(f\"Could not generate initial network: {e}\")\r\n\r\n    def _try_import_from_game(self) -> bool:\r\n        \"\"\"\r\n        Attempt to import brain state from a running game instance.\r\n        \r\n        Returns:\r\n            True if import was successful, False otherwise\r\n        \"\"\"\r\n        if not _HAS_BRAIN_BRIDGE:\r\n            return False\r\n        \r\n        try:\r\n            # Check if game is running\r\n            if not is_game_running():\r\n                self.logger.debug(\"No running game detected, will generate random network\")\r\n                return False\r\n            \r\n            self.logger.info(\"Detected running game, attempting to import brain state...\")\r\n            \r\n            # Import the brain state\r\n            live_state = import_brain_state_for_designer()\r\n            if live_state is None:\r\n                self.logger.warning(\"Could not import brain state from game\")\r\n                return False\r\n            \r\n            # Convert to BrainDesign\r\n            imported_design = convert_to_brain_design(live_state)\r\n            if imported_design is None:\r\n                self.logger.warning(\"Could not convert imported state to BrainDesign\")\r\n                return False\r\n            \r\n            # Replace current design with imported one\r\n            self.design = imported_design\r\n            \r\n            # Update canvas reference\r\n            if hasattr(self, 'canvas'):\r\n                self.canvas.design = self.design\r\n            \r\n            self._imported_from_game = True\r\n            self.logger.info(\r\n                f\"Successfully imported brain from game: \"\r\n                f\"{len(self.design.neurons)} neurons, \"\r\n                f\"{len(self.design.connections)} connections\"\r\n            )\r\n            return True\r\n            \r\n        except Exception as e:\r\n            self.logger.error(f\"Error importing from game: {e}\", exc_info=True)\r\n            return False\r\n\r\n    def _show_import_notification(self):\r\n        \"\"\"Show notification that brain was imported from running game.\"\"\"\r\n        from PyQt5.QtWidgets import QMessageBox\r\n        from PyQt5.QtCore import QTimer\r\n        \r\n        # Show the sync button now that we know game is running\r\n        self._show_sync_button()\r\n        \r\n        # Create a non-modal notification\r\n        msg = QMessageBox(self)\r\n        msg.setIcon(QMessageBox.Information)\r\n        msg.setWindowTitle(\"Live Brain Import\")\r\n        msg.setText(\"🧠 Active brain imported from running game\")\r\n        msg.setInformativeText(\r\n            f\"The designer is now showing the exact neural network \"\r\n            f\"from your running Dosidicus game.\\n\\n\"\r\n            f\"• {len(self.design.neurons)} neurons\\n\"\r\n            f\"• {len(self.design.connections)} connections\\n\\n\"\r\n            f\"Changes made here will NOT affect the running game.\"\r\n        )\r\n        msg.setStandardButtons(QMessageBox.Ok)\r\n        \r\n        # Show and auto-close after 5 seconds\r\n        msg.show()\r\n        QTimer.singleShot(5000, msg.close)\r\n        \r\n        # Update window title to indicate imported state\r\n        self.setWindowTitle(\"Brain Designer - Dosidicus-2 [Imported from Game]\")\r\n        \r\n        # Update status bar\r\n        self.status_bar.showMessage(\r\n            \"✨ Active brain imported from running game\", 10000\r\n        )\r\n\r\n    def _show_sync_button(self):\r\n        \"\"\"Show the sync button when game connection is established.\"\"\"\r\n        if not _HAS_BRAIN_BRIDGE:\r\n            return\r\n        \r\n        # Clear any existing content\r\n        while self._sync_btn_layout.count():\r\n            item = self._sync_btn_layout.takeAt(0)\r\n            if item.widget():\r\n                item.widget().deleteLater()\r\n        \r\n        # Add divider\r\n        divider = QFrame()\r\n        divider.setFrameShape(QFrame.VLine)\r\n        divider.setFrameShadow(QFrame.Sunken)\r\n        self._sync_btn_layout.addWidget(divider)\r\n        \r\n        # Add sync button\r\n        sync_btn = QPushButton(\"🔄 Sync from Game\")\r\n        sync_btn.setToolTip(\"Refresh brain state from running Dosidicus game\")\r\n        sync_btn.setStyleSheet(\"\"\"\r\n            QPushButton {\r\n                background-color: #9C27B0;\r\n                color: white;\r\n                font-weight: bold;\r\n                padding: 6px 12px;\r\n                border-radius: 4px;\r\n            }\r\n            QPushButton:hover {\r\n                background-color: #7B1FA2;\r\n            }\r\n        \"\"\")\r\n        sync_btn.clicked.connect(self.sync_from_running_game)\r\n        self._sync_btn_layout.addWidget(sync_btn)\r\n        \r\n        # Show the container\r\n        self._sync_btn_container.show()\r\n\r\n    def sync_from_running_game(self):\r\n        \"\"\"\r\n        Manually sync brain state from running game.\r\n        Called when user clicks the Sync from Game button.\r\n        \"\"\"\r\n        # Check if game is still running\r\n        if not is_game_running():\r\n            QMessageBox.information(\r\n                self, \"Game Not Running\",\r\n                \"The Dosidicus game is no longer running.\\n\\n\"\r\n                \"Start the game again to sync.\"\r\n            )\r\n            # Hide the sync button since game is gone\r\n            self._sync_btn_container.hide()\r\n            self.setWindowTitle(\"Brain Designer - Dosidicus-2\")\r\n            return\r\n        \r\n        # Confirm before replacing current design\r\n        reply = QMessageBox.question(\r\n            self, \"Sync from Game\",\r\n            \"Replace current design with the latest brain state from the game?\",\r\n            QMessageBox.Yes | QMessageBox.No,\r\n            QMessageBox.No\r\n        )\r\n        \r\n        if reply != QMessageBox.Yes:\r\n            return\r\n        \r\n        # Try to import\r\n        if self._try_import_from_game():\r\n            self.refresh_all()\r\n            self.status_bar.showMessage(\r\n                f\"✨ Synced: {len(self.design.neurons)} neurons, \"\r\n                f\"{len(self.design.connections)} connections\", 5000\r\n            )\r\n        else:\r\n            QMessageBox.warning(\r\n                self, \"Sync Failed\",\r\n                \"Could not import brain state from game.\"\r\n            )\r\n\r\n    def show_sparse_network_dialog(self):\r\n        \"\"\"Show the sparse network generation dialog.\"\"\"\r\n        dialog = SparseNetworkDialog(self.design, self)\r\n        if dialog.exec_() == QDialog.Accepted:\r\n            self.on_design_changed()\r\n\r\n    def quick_generate(self, style_key):\r\n        \"\"\"Quickly generate a network using a preset style.\"\"\"\r\n        try:\r\n            generator = SparseNetworkGenerator()\r\n            presets = generator.get_preset_styles()\r\n            \r\n            if style_key not in presets:\r\n                return\r\n            \r\n            preset = presets[style_key]\r\n            \r\n            # Determine dynamic bounds from the canvas view if available\r\n            bounds = (-450, -250, 650, 500)\r\n            if hasattr(self, 'canvas') and self.canvas.viewport():\r\n                try:\r\n                    view_rect = self.canvas.viewport().rect()\r\n                    scene_poly = self.canvas.mapToScene(view_rect)\r\n                    brect = scene_poly.boundingRect()\r\n                    bounds = (brect.left(), brect.top(), brect.right(), brect.bottom())\r\n                except Exception:\r\n                    pass\r\n\r\n            count, connections = generator.generate_for_design(\r\n                self.design,\r\n                clear_existing=True,\r\n                density=preset.get('density', 1.0),\r\n                include_feedback=preset.get('include_feedback', False),\r\n                position_variance=preset.get('position_variance', 0.2),\r\n                sensor_probability=preset.get('sensor_probability', 0.0),\r\n                bounds=bounds\r\n            )\r\n            \r\n            self.on_design_changed()\r\n            self.status_bar.showMessage(\r\n                f\"Generated {count} connections using '{preset['name']}' preset\", 3000\r\n            )\r\n        except Exception as e:\r\n            self.logger.error(f\"Error in quick_generate: {e}\", exc_info=True)\r\n            QMessageBox.warning(self, \"Error\", f\"Generation failed: {e}\")\r\n\r\n    def instant_random_generate(self):\r\n        \"\"\"Instantly generate a random network without any dialog, shuffling positions.\"\"\"\r\n        import random\r\n        \r\n        try:\r\n            generator = SparseNetworkGenerator()\r\n            presets = generator.get_preset_styles()\r\n            \r\n            # Pick a random preset as a base\r\n            style_key = random.choice(list(presets.keys()))\r\n            preset = presets[style_key]\r\n            \r\n            # Randomize connection parameters\r\n            density = random.uniform(0.7, 1.4)\r\n            \r\n            # !! CHAOS MODE !!\r\n            # Use high variance to shuffle neurons around the visible canvas\r\n            position_variance = random.uniform(0.6, 1.0) \r\n            \r\n            # Calculate dynamic bounds from the actual visible canvas area\r\n            if hasattr(self, 'canvas') and self.canvas.viewport():\r\n                try:\r\n                    view_rect = self.canvas.viewport().rect()\r\n                    scene_poly = self.canvas.mapToScene(view_rect)\r\n                    brect = scene_poly.boundingRect()\r\n                    bounds = (brect.left(), brect.top(), brect.right(), brect.bottom())\r\n                except Exception:\r\n                    # Fallback if calculation fails\r\n                    bounds = (-450, -250, 650, 500)\r\n            else:\r\n                bounds = (-450, -250, 650, 500)\r\n            \r\n            count, _ = generator.generate_for_design(\r\n                self.design,\r\n                clear_existing=True,\r\n                density=density,\r\n                include_feedback=random.random() > 0.3,\r\n                position_variance=position_variance,\r\n                bounds=bounds,\r\n                sensor_probability=0.2  # Add some random sensors for flavor\r\n            )\r\n            \r\n            self.on_design_changed()\r\n            \r\n            # Ensure the canvas recenters on the new chaotic layout\r\n            if self.canvas:\r\n                self.canvas.center_on_neurons()\r\n                \r\n            self.status_bar.showMessage(\r\n                f\"🎲 Chaos! Shuffled positions & made {count} connections ({preset['name']} style)\", 3000\r\n            )\r\n        except Exception as e:\r\n            self.logger.error(f\"Error in instant_random_generate: {e}\", exc_info=True)\r\n\r\n    def clear_all_connections(self):\r\n        \"\"\"Clear all connections from the design.\"\"\"\r\n        reply = QMessageBox.question(\r\n            self, \"Clear Connections\",\r\n            f\"Remove all {len(self.design.connections)} connections?\\n\\n\"\r\n            \"Neurons will be kept.\",\r\n            QMessageBox.Yes | QMessageBox.No\r\n        )\r\n\r\n        if reply == QMessageBox.Yes:\r\n            count = len(self.design.connections)\r\n            self.design.connections.clear()\r\n            self.on_design_changed()\r\n            self.status_bar.showMessage(f\"Cleared {count} connections\", 3000)\r\n\r\n    def clear_all_outputs(self):\r\n        \"\"\"Clear all output bindings from the design.\"\"\"\r\n        if not self.outputs_panel:\r\n            return\r\n        \r\n        if not self.outputs_panel.bindings:\r\n            QMessageBox.information(self, \"Clear Outputs\", \"No output bindings to clear.\")\r\n            return\r\n        \r\n        reply = QMessageBox.question(\r\n            self, \"Clear Output Bindings\",\r\n            f\"Remove all {len(self.outputs_panel.bindings)} output bindings?\",\r\n            QMessageBox.Yes | QMessageBox.No\r\n        )\r\n        \r\n        if reply == QMessageBox.Yes:\r\n            count = len(self.outputs_panel.bindings)\r\n            self.outputs_panel.bindings.clear()\r\n            self.outputs_panel.refresh()\r\n            self.design.output_bindings = []\r\n            self.on_design_changed()\r\n            self.status_bar.showMessage(f\"Cleared {count} output bindings\", 3000)\r\n\r\n    # ==========================================================================\r\n    # EVENT HANDLERS / CALLBACKS\r\n    # ==========================================================================\r\n\r\n    def on_design_changed(self):\r\n        \"\"\"Called when the design is modified.\"\"\"\r\n        try:\r\n            self.canvas.rebuild()\r\n            self.connections_table.refresh()\r\n            self.sensors_panel.refresh()\r\n            self.layers_panel.refresh()\r\n            \r\n            # Refresh outputs panel\r\n            if self.outputs_panel:\r\n                self.outputs_panel.refresh()\r\n            \r\n            self.update_status()\r\n        except Exception as e:\r\n            self.logger.error(f\"Error updating design view: {e}\", exc_info=True)\r\n\r\n    def on_neuron_selected(self, name):\r\n        \"\"\"Called when a neuron is selected on the canvas.\"\"\"\r\n        self.props_panel.set_neuron(name)\r\n        # Switch to properties tab if a neuron is selected\r\n        if name:\r\n            self.right_panel.setCurrentWidget(self.props_panel)\r\n\r\n    def show_add_neuron_dialog(self, x=None, y=None):\r\n        \"\"\"Show dialog to add a new neuron.\"\"\"\r\n        pos = (x, y) if x is not None else None\r\n        dlg = AddNeuronDialog(self.design, pos, self)\r\n        if dlg.exec_() == QDialog.Accepted:\r\n            self.on_design_changed()\r\n\r\n    def on_connection_created(self, source, target):\r\n        \"\"\"Called when a new connection is created via drag.\"\"\"\r\n        weight, ok = QInputDialog.getDouble(\r\n            self, \"Connection Weight\",\r\n            f\"Set weight for {source} → {target}:\",\r\n            0.5, -1.0, 1.0, 2\r\n        )\r\n        if ok:\r\n            conn = self.design.get_connection(source, target)\r\n            if conn:\r\n                conn.weight = weight\r\n            self.on_design_changed()\r\n        else:\r\n            # Cancel: remove the connection\r\n            self.design.remove_connection(source, target)\r\n            self.on_design_changed()\r\n\r\n    def on_connection_selected(self, source, target):\r\n        \"\"\"Called when a connection is selected.\"\"\"\r\n        conn = self.design.get_connection(source, target)\r\n        if conn:\r\n            self.status_bar.showMessage(\r\n                f\"Selected: {source} → {target} (weight: {conn.weight:+.3f})\"\r\n            )\r\n\r\n    def on_weight_changed(self, source, target, new_weight):\r\n        \"\"\"Called when a connection weight is changed.\"\"\"\r\n        self.connections_table.refresh()\r\n        self.status_bar.showMessage(\r\n            f\"Weight updated: {source} → {target} = {new_weight:+.3f}\", 2000\r\n        )\r\n\r\n    def on_connection_deleted(self, source, target):\r\n        \"\"\"Called when a connection is deleted.\"\"\"\r\n        self.on_design_changed()\r\n        self.status_bar.showMessage(f\"Deleted connection: {source} → {target}\", 2000)\r\n\r\n    # ==========================================================================\r\n    # FILE AND UTILITY OPERATIONS\r\n    # ==========================================================================\r\n\r\n    def new_design(self):\r\n        \"\"\"Create a new empty design.\"\"\"\r\n        reply = QMessageBox.question(\r\n            self, \"New Design\",\r\n            \"Start a new design? Unsaved changes will be lost.\",\r\n            QMessageBox.Yes | QMessageBox.No\r\n        )\r\n        if reply == QMessageBox.Yes:\r\n            self.design = BrainDesign()\r\n            self.design.add_missing_required_neurons()\r\n            self.refresh_all()\r\n\r\n    def run_auto_fix(self):\r\n        \"\"\"Run auto-fix on the design.\"\"\"\r\n        count, actions = self.design.auto_fix_connectivity()\r\n        if count > 0:\r\n            self.on_design_changed()\r\n            # [FIX] Force immediate repaint so changes are visible behind the message box\r\n            if self.canvas:\r\n                self.canvas.viewport().repaint()\r\n            \r\n            QMessageBox.information(\r\n                self, \"Auto-Fix\",\r\n                f\"Created {count} connections:\\n\\n\" + \"\\n\".join(actions[:10])\r\n            )\r\n        else:\r\n            QMessageBox.information(self, \"Auto-Fix\", \"No issues found.\")\r\n\r\n    def save_design(self):\r\n        \"\"\"Save the design to file.\"\"\"\r\n        try:\r\n            # Sync output bindings from panel to design BEFORE saving\r\n            if self.outputs_panel:\r\n                self.design.output_bindings = self.outputs_panel.export_bindings()\r\n            \r\n            path, _ = QFileDialog.getSaveFileName(\r\n                self, \"Save Design\", \"brain.json\", \"JSON (*.json)\"\r\n            )\r\n            if path:\r\n                self.logger.info(f\"Saving design to: {path}\")\r\n                success, msg = self.design.save(path)\r\n                if success:\r\n                    self.logger.info(f\"Design saved successfully: {msg}\")\r\n                    \r\n                    # Add output binding info to message\r\n                    if self.outputs_panel and self.outputs_panel.bindings:\r\n                        msg += f\"\\n({len(self.outputs_panel.bindings)} output bindings included)\"\r\n                    \r\n                    QMessageBox.information(self, \"Saved\", msg)\r\n                else:\r\n                    self.logger.warning(f\"Save failed: {msg}\")\r\n                    QMessageBox.warning(self, \"Error\", msg)\r\n        except Exception as e:\r\n            self.logger.error(f\"Error saving design: {e}\", exc_info=True)\r\n            QMessageBox.warning(self, \"Error\", f\"Failed to save design:\\n\\n{e}\")\r\n\r\n    def export_design(self):\r\n        \"\"\"Export in Dosidicus format.\"\"\"\r\n        try:\r\n            # Sync output bindings from panel to design BEFORE exporting\r\n            if self.outputs_panel:\r\n                self.design.output_bindings = self.outputs_panel.export_bindings()\r\n            \r\n            path, _ = QFileDialog.getSaveFileName(\r\n                self, \"Export\", \"dosidicus_brain.json\", \"JSON (*.json)\"\r\n            )\r\n            if path:\r\n                self.logger.info(f\"Exporting design to: {path}\")\r\n                success, msg = self.design.export_dosidicus(path)\r\n                if success:\r\n                    self.logger.info(f\"Design exported successfully\")\r\n                    \r\n                    # Add output binding info to message\r\n                    if self.outputs_panel and self.outputs_panel.bindings:\r\n                        msg += f\"\\n({len(self.outputs_panel.bindings)} output bindings included)\"\r\n                    \r\n                    QMessageBox.information(self, \"Exported\", msg)\r\n                else:\r\n                    self.logger.warning(f\"Export failed: {msg}\")\r\n                    QMessageBox.warning(self, \"Error\", msg)\r\n        except Exception as e:\r\n            self.logger.error(f\"Error exporting design: {e}\", exc_info=True)\r\n            QMessageBox.warning(self, \"Error\", f\"Failed to export design:\\n\\n{e}\")\r\n\r\n    def open_design(self):\r\n        \"\"\"Open a design file.\"\"\"\r\n        try:\r\n            path, _ = QFileDialog.getOpenFileName(\r\n                self, \"Open Design\", \"\", \"JSON (*.json)\"\r\n            )\r\n            if path:\r\n                self.logger.info(f\"Opening design from: {path}\")\r\n                self.design = BrainDesign.load(path)\r\n                self.logger.info(\r\n                    f\"Design loaded: {len(self.design.neurons)} neurons, \"\r\n                    f\"{len(self.design.connections)} connections\"\r\n                )\r\n                \r\n                # Load output bindings into the panel\r\n                if self.outputs_panel:\r\n                    self.outputs_panel.load_bindings(self.design.output_bindings)\r\n                    if self.design.output_bindings:\r\n                        self.logger.info(f\"Loaded {len(self.design.output_bindings)} output bindings\")\r\n                \r\n                self.refresh_all()\r\n        except Exception as e:\r\n            self.logger.error(f\"Error opening design: {e}\", exc_info=True)\r\n            QMessageBox.warning(self, \"Error\", f\"Could not load design:\\n\\n{e}\")\r\n\r\n    def show_template_menu(self):\r\n        \"\"\"Show templates as a popup.\"\"\"\r\n        templates = TemplateManager.get_templates()\r\n        items = [f\"{info['name']} - {info['description']}\" for info in templates.values()]\r\n        keys = list(templates.keys())\r\n\r\n        item, ok = QInputDialog.getItem(\r\n            self, \"Load Template\", \"Select a template:\", items, 0, False\r\n        )\r\n        if ok and item:\r\n            idx = items.index(item)\r\n            key = keys[idx]\r\n            if QMessageBox.question(\r\n                self, \"Load Template\", \"Replace current design?\",\r\n                QMessageBox.Yes | QMessageBox.No\r\n            ) == QMessageBox.Yes:\r\n                self.design = TemplateManager.create_template(key)\r\n                self.refresh_all()\r\n\r\n    def load_template(self):\r\n        \"\"\"Load a template from menu action.\"\"\"\r\n        key = self.sender().data()\r\n        if QMessageBox.question(\r\n            self, \"Load Template\", \"Replace current design?\",\r\n            QMessageBox.Yes | QMessageBox.No\r\n        ) == QMessageBox.Yes:\r\n            self.design = TemplateManager.create_template(key)\r\n            self.refresh_all()\r\n\r\n    def check_status(self):\r\n        \"\"\"Validate and show design status.\"\"\"\r\n        self.on_design_changed()\r\n        stats = self.design.get_stats()\r\n        _, issues, _ = self.design.validate(auto_fix=False)\r\n\r\n        msg = (\r\n            f\"Neurons: {stats['total_neurons']}\\n\"\r\n            f\"  • Required: {stats['required_neurons']}\\n\"\r\n            f\"  • Sensors: {stats['sensor_neurons']}\\n\"\r\n            f\"  • Custom: {stats['custom_neurons']}\\n\\n\"\r\n            f\"Connections: {stats['connections']}\\n\"\r\n            f\"Layers: {stats['layers']}\\n\"\r\n        )\r\n        \r\n        # Add output binding info\r\n        if self.outputs_panel:\r\n            binding_count = len(self.outputs_panel.bindings)\r\n            enabled_count = sum(1 for b in self.outputs_panel.bindings if b.enabled)\r\n            msg += f\"Output Bindings: {binding_count} ({enabled_count} enabled)\\n\"\r\n            \r\n            # Validate bindings\r\n            binding_warnings = self.outputs_panel.validate_bindings()\r\n            if binding_warnings:\r\n                issues.extend(binding_warnings)\r\n\r\n        if issues:\r\n            msg += \"\\n⚠️ ISSUES:\\n\" + \"\\n\".join(f\"  • {i}\" for i in issues)\r\n        else:\r\n            msg += \"\\n✅ Status: OK\"\r\n\r\n        QMessageBox.information(self, \"Design Status\", msg)\r\n\r\n\r\n# =============================================================================\r\n# MAIN ENTRY POINT\r\n# =============================================================================\r\n\r\ndef main():\r\n    \"\"\"Main entry point for the Brain Designer application.\"\"\"\r\n    from PyQt5.QtWidgets import QApplication\r\n    \r\n    app = QApplication(sys.argv)\r\n    app.setApplicationName(\"Brain Designer (Beta)\")\r\n    app.setOrganizationName(\"Dosidicus\")\r\n    \r\n    window = BrainDesignerWindow()\r\n    window.show()\r\n    \r\n    sys.exit(app.exec_())\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()"
  },
  {
    "path": "src/display_scaling.py",
    "content": "\r\nclass DisplayScaling:\r\n    DESIGN_WIDTH = 2880\r\n    DESIGN_HEIGHT = 1920\r\n    _scale_factor = 1.0\r\n    \r\n    # Default scale factor (1.0 means no scaling)\r\n    _scale_factor = 1.0\r\n    \r\n    @classmethod\r\n    def initialize(cls, current_width, current_height):\r\n        width_ratio = current_width / cls.DESIGN_WIDTH\r\n        height_ratio = current_height / cls.DESIGN_HEIGHT\r\n        base_scale_factor = min(width_ratio, height_ratio)\r\n        \r\n        if current_width <= 1920 and current_height <= 1080:\r\n            cls._scale_factor = base_scale_factor * 0.85\r\n            print(f\"1080p display detected: applying 85% scaling (factor={cls._scale_factor:.2f})\")\r\n        else:\r\n            cls._scale_factor = base_scale_factor\r\n            print(f\"High resolution display ({current_width}x{current_height}): standard scaling (factor={cls._scale_factor:.2f})\")\r\n    \r\n    @classmethod\r\n    def scale(cls, value):\r\n        return int(value * cls._scale_factor)\r\n        \r\n    @classmethod\r\n    def font_size(cls, size):\r\n        scaled = cls.scale(size)\r\n        return max(10, scaled)\r\n    \r\n    @classmethod\r\n    def get_scale_factor(cls):\r\n        return cls._scale_factor\r\n        \r\n    @classmethod\r\n    def scale_css(cls, css_string):\r\n        import re\r\n        pattern = r'font-size:\\s*(\\d+)px'\r\n        def replace_size(match):\r\n            original_size = int(match.group(1))\r\n            scaled_size = cls.font_size(original_size)\r\n            return f'font-size: {scaled_size}px'\r\n        return re.sub(pattern, replace_size, css_string)\r\n"
  },
  {
    "path": "src/hidden_imports.txt",
    "content": "plugins.achievements.display_scaling\r\nplugins.achievements.achievements_data\r\nuuid\r\nbrain_designer\r\ndesigner_window\r\ndesigner_core\r\ndesigner_constants\r\ndesigner_logging\r\ndesigner_dialogs\r\nbrain_state_bridge"
  },
  {
    "path": "src/image_cache.py",
    "content": "from PyQt5 import QtGui\r\n\r\nclass ImageCache:\r\n    \"\"\"Global image cache to prevent duplicate loading\"\"\"\r\n    _cache = {}\r\n    \r\n    @classmethod\r\n    def get_pixmap(cls, path):\r\n        \"\"\"Get a pixmap from cache or load it\"\"\"\r\n        if path not in cls._cache:\r\n            pixmap = QtGui.QPixmap(path)\r\n            cls._cache[path] = pixmap\r\n        return cls._cache[path]\r\n    \r\n    @classmethod\r\n    def clear(cls):\r\n        \"\"\"Clear cache to free memory\"\"\"\r\n        cls._cache.clear()"
  },
  {
    "path": "src/interactions.py",
    "content": "\r\n# ROCK INTERACTIONS\r\n\r\nimport math\r\nimport time\r\nimport random\r\nimport os\r\nfrom PyQt5 import QtCore, QtWidgets, QtGui\r\n\r\nclass RockInteractionManager:\r\n    def __init__(self, squid, logic, scene, message_callback, config_manager):\r\n        self.squid = squid\r\n        self.logic = logic\r\n        self.scene = scene\r\n        self.show_message = message_callback\r\n        self.config_manager = config_manager\r\n        self.rock_config = config_manager.get_rock_config()\r\n\r\n        # Rock interaction state\r\n        self.target_rock = None\r\n        self.rock_test_phase = 0  # 0=approach, 1=carry, 2=throw\r\n        self.rock_carry_time = 0\r\n        self.rock_carry_duration = 0\r\n        \r\n        # Initialize timers\r\n        self.rock_test_timer = QtCore.QTimer()\r\n        self.throw_animation_timer = QtCore.QTimer()\r\n        \r\n        # Connect timers\r\n        self.rock_test_timer.timeout.connect(self.update_rock_test)\r\n        self.throw_animation_timer.timeout.connect(self.update_throw_animation)\r\n        \r\n        # Initialize throw velocity variables\r\n        self.throw_velocity_x = 0\r\n        self.throw_velocity_y = 0\r\n\r\n        # Add multiplayer support\r\n        self.multiplayer_plugin = None\r\n        self.setup_multiplayer_integration()\r\n\r\n    def setup_multiplayer_integration(self):\r\n        \"\"\"Set up hooks for multiplayer integration\"\"\"\r\n        if not hasattr(self.logic, 'plugin_manager'):\r\n            return False\r\n            \r\n        # Get multiplayer plugin\r\n        multiplayer_plugin = None\r\n        for plugin_name, plugin_data in self.logic.plugin_manager.plugins.items():\r\n            if plugin_name == \"multiplayer_plugin\":\r\n                # Get the actual plugin instance\r\n                multiplayer_plugin = plugin_data.get('instance')\r\n                break\r\n        \r\n        if multiplayer_plugin:\r\n            # Store reference\r\n            self.multiplayer_plugin = multiplayer_plugin\r\n            \r\n            print(\"[RockInteraction] Successfully integrated with multiplayer plugin\")\r\n            return True\r\n        \r\n        return False\r\n\r\n    def is_valid_rock(self, item):\r\n        \"\"\"Check if item is a valid rock\"\"\"\r\n        if not isinstance(item, QtWidgets.QGraphicsPixmapItem):\r\n            return False\r\n        return (hasattr(item, 'category') and item.category == 'rock') or \\\r\n               (hasattr(item, 'filename') and 'rock' in item.filename.lower())\r\n\r\n    def can_pick_up_rock(self, rock):\r\n        \"\"\"Check if squid can pick up this rock\"\"\"\r\n        if not self.is_valid_rock(rock):\r\n            return False\r\n        if hasattr(rock, 'is_being_carried') and rock.is_being_carried:\r\n            return False\r\n        # Check if rock is in cooldown after being thrown\r\n        if hasattr(rock, 'throw_cooldown_until'):\r\n            if time.time() < rock.throw_cooldown_until:\r\n                return False\r\n        return True\r\n\r\n    def attach_rock_to_squid(self, rock):\r\n        \"\"\"Visually attach rock to squid at tentacle position\"\"\"\r\n        rock.setParentItem(self.squid.squid_item)\r\n        \r\n        # Use config values for hold duration\r\n        config = self.rock_config\r\n        self.squid.rock_hold_duration = random.uniform(\r\n            config['min_carry_duration'],\r\n            config['max_carry_duration']\r\n        )\r\n        self.squid.rock_hold_start_time = time.time()\r\n        self.squid.rock_decision_made = False\r\n        \r\n        offset = -50  # Both vertical and horizontal offset\r\n        \r\n        # Calculate position based on squid direction\r\n        if self.squid.squid_direction == \"right\":\r\n            # Position rock near right tentacles\r\n            rock.setPos(self.squid.squid_width - 40 + offset, \r\n                    self.squid.squid_height - 30 + offset)\r\n        elif self.squid.squid_direction == \"left\":\r\n            # Position rock near left tentacles\r\n            rock.setPos(10 + offset, \r\n                    self.squid.squid_height - 30 + offset)\r\n        elif self.squid.squid_direction == \"up\":\r\n            # Position rock near upper tentacles\r\n            rock.setPos(self.squid.squid_width//2 - 15 + offset, \r\n                    self.squid.squid_height - 40 + offset)\r\n        else:  # down/default\r\n            rock.setPos(self.squid.squid_width//2 - 15 + offset, \r\n                    self.squid.squid_height - 20 + offset)\r\n        \r\n        rock.is_being_carried = True\r\n        self.squid.is_carrying_rock = True\r\n        self.squid.carried_rock = rock\r\n        rock.setZValue(self.squid.squid_item.zValue() + 1)\r\n        \r\n        # Scale rock to appropriate size\r\n        rock.setScale(1.0)\r\n        \r\n        # Update squid status\r\n        self.squid.status = \"carrying rock\"\r\n        \r\n        return True\r\n    \r\n    def check_rock_hold_time(self):\r\n        \"\"\"Check if holding time elapsed and make decision\"\"\"\r\n        if not hasattr(self.squid, 'carrying_rock') or not self.squid.carrying_rock or not hasattr(self.squid, 'rock_decision_made') or self.squid.rock_decision_made:\r\n            return\r\n        \r\n        current_time = time.time()\r\n        if current_time - self.squid.rock_hold_start_time >= self.squid.rock_hold_duration:\r\n            self.squid.rock_decision_made = True\r\n            self.decide_rock_action()\r\n\r\n    def decide_rock_action(self):\r\n        \"\"\"Randomly decide to throw or drop the rock\"\"\"\r\n        if random.random() < 0.7:  # 70% chance to throw\r\n            direction = \"right\" if random.random() < 0.5 else \"left\"\r\n            self.throw_rock(direction)\r\n            if self.show_message:\r\n                self.show_message(\"Squid threw the rock!\")\r\n        else:  # 30% chance to drop\r\n            self.drop_rock()\r\n            if self.show_message:\r\n                self.show_message(\"Squid dropped the rock\")\r\n\r\n    def drop_rock(self):\r\n        \"\"\"Gently place the rock below the squid\"\"\"\r\n        if not hasattr(self.squid, 'carrying_rock') or not self.squid.carrying_rock or not hasattr(self.squid, 'carried_rock') or not self.squid.carried_rock:\r\n            return\r\n        \r\n        rock = self.squid.carried_rock\r\n        rock.setParentItem(None)\r\n        rock.setPos(\r\n            self.squid.squid_x + self.squid.squid_width//2 - rock.boundingRect().width()//2,\r\n            self.squid.squid_y + self.squid.squid_height + 10\r\n        )\r\n        self.squid.is_carrying_rock = False\r\n        self.squid.carried_rock = None\r\n\r\n    def start_rock_test(self, rock=None):\r\n        \"\"\"Start test with guaranteed clean state and random carry duration\"\"\"\r\n        self.cleanup()  # Reset everything first\r\n        \r\n        if rock is None:\r\n            # Filter rocks that are valid, visible, and not on cooldown\r\n            rocks = [item for item in self.scene.items() \r\n                    if self.is_valid_rock(item) and item.isVisible() and self.can_pick_up_rock(item)]\r\n            if not rocks:\r\n                if self.show_message:\r\n                    self.show_message(\"No available rocks!\")\r\n                return False\r\n            rock = min(rocks, key=lambda r: math.hypot(\r\n                r.sceneBoundingRect().center().x() - self.squid.squid_x,\r\n                r.sceneBoundingRect().center().y() - self.squid.squid_y\r\n            ))\r\n        elif not self.can_pick_up_rock(rock):\r\n            if self.show_message:\r\n                self.show_message(\"Rock is on cooldown!\")\r\n            return False\r\n        \r\n        self.target_rock = rock\r\n        self.rock_test_phase = 0\r\n        # Use the config values for duration\r\n        self.rock_carry_duration = random.uniform(\r\n            self.rock_config['min_carry_duration'],\r\n            self.rock_config['max_carry_duration']\r\n        )\r\n        self.rock_carry_time = 0  # Reset carry timer\r\n        self.rock_test_timer.start(100)  # 100ms updates\r\n        return True\r\n\r\n    def highlight_rock(self, rock):\r\n        \"\"\"Visual feedback for selected rock\"\"\"\r\n        highlight = QtWidgets.QGraphicsOpacityEffect()\r\n        highlight.setOpacity(0.3)  # Initial opacity\r\n        rock.setGraphicsEffect(highlight)\r\n        \r\n        # Animate highlight\r\n        self.animate_highlight(highlight)\r\n\r\n    def animate_highlight(self, highlight_effect):\r\n        \"\"\"Pulse animation for rock highlight\"\"\"\r\n        self.highlight_animation = QtCore.QPropertyAnimation(highlight_effect, b\"opacity\")\r\n        self.highlight_animation.setDuration(1000)\r\n        self.highlight_animation.setStartValue(0.3)\r\n        self.highlight_animation.setEndValue(0.8)\r\n        self.highlight_animation.setLoopCount(3)\r\n        self.highlight_animation.start()\r\n\r\n    def throw_rock(self, direction=\"right\"):\r\n        \"\"\"Initiates a rock throw with positive memory formation\"\"\"\r\n        # Prevent multiple throws\r\n        if self.throw_animation_timer.isActive():\r\n            return False\r\n            \r\n        if not hasattr(self.squid, 'carried_rock') or not self.squid.carried_rock:\r\n            return False\r\n        \r\n        config = self.rock_config\r\n        rock = self.squid.carried_rock\r\n        \r\n        # Set squid status to throwing rock with more detail\r\n        if hasattr(self.squid, 'status'):\r\n            if random.random() < 0.3:\r\n                self.squid.status = \"throwing rock\"\r\n            else:\r\n                self.squid.status = \"playing with rock\"\r\n        \r\n        # Detach from squid and reset parent to scene\r\n        rock.setParentItem(None)\r\n        \r\n        # Calculate throw vectors\r\n        throw_power = 12  # Increased from 8\r\n        angle = math.radians(30 if direction == \"right\" else 150)\r\n        self.throw_velocity_x = throw_power * math.cos(angle)\r\n        self.throw_velocity_y = -throw_power * math.sin(angle)  # Negative for upward\r\n        \r\n        # Set position to squid's center in scene coordinates\r\n        squid_rect = self.squid.squid_item.sceneBoundingRect()\r\n        rock_rect = rock.boundingRect()\r\n        rock.setPos(\r\n            squid_rect.center().x() - rock_rect.width()/2,\r\n            squid_rect.center().y() - rock_rect.height()/2\r\n        )\r\n        \r\n        rock.setVisible(True)\r\n        \r\n        # Apply stat changes\r\n        self.squid.happiness = min(100, self.squid.happiness + config['happiness_boost'])\r\n        self.squid.satisfaction = min(100, self.squid.satisfaction + config['satisfaction_boost'])\r\n        self.squid.anxiety = max(0, self.squid.anxiety - config['anxiety_reduction'])\r\n        self.logic.statistics_window.award(50)\r\n        \r\n        # Simplified positive memory\r\n        rock_filename = getattr(rock, 'filename', '') or ''\r\n        is_urchin = 'rock03' in rock_filename.lower()\r\n        memory_details = {\r\n            \"activity\": \"urchin_throwing\" if is_urchin else \"rock_throwing\",\r\n            \"item\": rock_filename,\r\n            \"effects\": {\r\n                \"happiness\": config['happiness_boost'],\r\n                \"satisfaction\": config['satisfaction_boost'],\r\n                \"anxiety\": -config['anxiety_reduction']\r\n            },\r\n            \"description\": \"Had fun throwing an urchin!\" if is_urchin else \"Had fun throwing a rock!\",\r\n            \"is_positive\": True\r\n        }\r\n\r\n        # Update rocks thrown counter\r\n        if hasattr(self.squid, 'statistics'):\r\n            self.squid.statistics.total_rocks_thrown += 1\r\n\r\n        # Update statistics tab and achievements plugin\r\n        if hasattr(self.logic, 'track_rock_thrown'):\r\n            self.logic.track_rock_thrown()\r\n        \r\n        # Add with importance (2) and positive formatting\r\n        self.squid.memory_manager.add_short_term_memory(\r\n            'play',\r\n            memory_details[\"activity\"],\r\n            memory_details,\r\n            importance=2\r\n        )\r\n        \r\n        # Broadcast to network if multiplayer plugin is available\r\n        if hasattr(self, 'multiplayer_plugin') and self.multiplayer_plugin:\r\n            try:\r\n                self.multiplayer_plugin.throw_rock_network(rock, direction)\r\n            except Exception as e:\r\n                print(f\"[RockInteraction] Error broadcasting rock throw: {e}\")\r\n        \r\n        self.throw_animation_timer.start(50)\r\n        return True\r\n\r\n\r\n    def update_rock_test(self):\r\n        \"\"\"Handle the rock test sequence (approach, carry)\"\"\"\r\n        if not self.target_rock:\r\n            self.cleanup()\r\n            return\r\n        \r\n        if self.rock_test_phase == 0:  # Approach phase\r\n            rock_center = self.target_rock.sceneBoundingRect().center()\r\n            squid_center = self.squid.squid_item.sceneBoundingRect().center()\r\n            distance = math.hypot(rock_center.x()-squid_center.x(),\r\n                                rock_center.y()-squid_center.y())\r\n\r\n            if distance < 50:  # Close enough to pick up\r\n                if self.attach_rock_to_squid(self.target_rock):\r\n                    self.rock_test_phase = 1  # Move to carry phase\r\n                else:\r\n                    self.cleanup()\r\n            else:\r\n                self.squid.move_toward_position(rock_center)\r\n                \r\n        elif self.rock_test_phase == 1:  # Carry phase\r\n            # Update the carry time\r\n            self.rock_carry_time += 0.1  # Since timer fires every 100ms\r\n            \r\n            # Check if carry duration has elapsed\r\n            if self.rock_carry_time >= self.rock_carry_duration:\r\n                # Time to make a decision\r\n                if random.random() < 0.7:  # 70% chance to throw\r\n                    direction = \"right\" if random.random() < 0.5 else \"left\"\r\n                    self.throw_rock(direction)\r\n                    if self.show_message:\r\n                        self.show_message(\"Squid threw the rock!\")\r\n                else:  # 30% chance to drop\r\n                    self.drop_rock()\r\n                    if self.show_message:\r\n                        self.show_message(\"Squid dropped the rock\")\r\n                        \r\n                # End the test\r\n                self.cleanup()\r\n\r\n    def update_throw_animation(self):\r\n        \"\"\"Handles the physics update for thrown rocks\"\"\"\r\n        if not hasattr(self, 'squid') or not self.squid.carried_rock:\r\n            self.throw_animation_timer.stop()\r\n            self.cleanup_after_throw()\r\n            return\r\n\r\n        rock = self.squid.carried_rock\r\n        new_x = rock.x() + self.throw_velocity_x\r\n        new_y = rock.y() + self.throw_velocity_y\r\n\r\n        # Apply gravity (reduced from 0.5 to 0.3 for slower descent)\r\n        self.throw_velocity_y += 0.3\r\n\r\n        # Apply friction to the horizontal velocity\r\n        friction_coefficient = 0.98\r\n        self.throw_velocity_x *= friction_coefficient\r\n\r\n        # Boundary checks\r\n        scene_rect = self.scene.sceneRect()\r\n        rock_rect = rock.boundingRect()\r\n\r\n        # Left/right boundaries with reduced bounce\r\n        if new_x < scene_rect.left():\r\n            new_x = scene_rect.left()\r\n            self.throw_velocity_x *= -0.2\r\n        elif new_x > scene_rect.right() - rock_rect.width():\r\n            new_x = scene_rect.right() - rock_rect.width()\r\n            self.throw_velocity_x *= -0.2\r\n\r\n        # Top boundary - small bounce\r\n        if new_y < scene_rect.top():\r\n            new_y = scene_rect.top()\r\n            self.throw_velocity_y *= -0.2\r\n        # Bottom boundary - stop immediately (no sliding)\r\n        elif new_y > scene_rect.bottom() - rock_rect.height() - 50:\r\n            new_y = scene_rect.bottom() - rock_rect.height() - 50\r\n            # Stop all momentum when hitting the bottom\r\n            self.throw_velocity_x = 0\r\n            self.throw_velocity_y = 0\r\n            rock.setPos(new_x, new_y)\r\n            self.throw_animation_timer.stop()\r\n            self.cleanup_after_throw()\r\n            return\r\n\r\n        rock.setPos(new_x, new_y)\r\n\r\n        # Stop the animation if the rock has slowed down enough\r\n        if abs(self.throw_velocity_x) < 0.1 and abs(self.throw_velocity_y) < 0.1:\r\n            self.throw_animation_timer.stop()\r\n            self.cleanup_after_throw()\r\n\r\n    def cleanup(self):\r\n        \"\"\"Reset all rock interaction state\"\"\"\r\n        self.rock_test_timer.stop()\r\n        self.throw_animation_timer.stop()\r\n        \r\n        self.target_rock = None\r\n        self.rock_test_phase = 0\r\n        self.rock_carry_time = 0\r\n        self.rock_carry_duration = 0\r\n        \r\n        if hasattr(self.squid, 'is_carrying_rock') and self.squid.is_carrying_rock:\r\n            self.squid.is_carrying_rock = False\r\n            self.squid.carried_rock = None\r\n\r\n    def cleanup_after_throw(self):\r\n        if hasattr(self.squid, 'carried_rock') and self.squid.carried_rock:\r\n            rock = self.squid.carried_rock\r\n            # Set cooldown on the rock so it can't be picked up immediately\r\n            cooldown = self.rock_config.get('cooldown_after_throw', 10.0)\r\n            rock.throw_cooldown_until = time.time() + cooldown\r\n            # Make sure to reset all rock-related states\r\n            rock.is_being_carried = False\r\n            self.squid.carried_rock = None\r\n        \r\n        # Reset squid states\r\n        self.squid.is_carrying_rock = False\r\n        \r\n        # Set status based on current stats\r\n        if self.squid.happiness > 80:\r\n            self.squid.status = \"playful\"\r\n        elif self.squid.anxiety > 60:\r\n            self.squid.status = \"anxious\"\r\n        elif self.squid.curiosity > 60:\r\n            self.squid.status = \"curious\"\r\n        else:\r\n            self.squid.status = \"exploring surroundings\"\r\n        \r\n        self.throw_velocity_x = 0\r\n        self.throw_velocity_y = 0\r\n        self.cleanup()\r\n\r\n\r\n    def setup_timers(self, interval=100):\r\n        \"\"\"Configure timer intervals\"\"\"\r\n        self.rock_test_timer.setInterval(interval)\r\n        self.throw_animation_timer.setInterval(50)\r\n\r\n    # === MULTIPLAYER EXTENSIONS ===\r\n\r\n    def handle_remote_rock_throw(self, source_node_id, rock_data):\r\n        \"\"\"Handle a rock throw from a remote squid\"\"\"\r\n        try:\r\n            # Extract rock data\r\n            rock_filename = rock_data.get('rock_filename')\r\n            direction = rock_data.get('direction')\r\n            initial_pos = rock_data.get('initial_pos')\r\n            \r\n            # Skip if missing required data\r\n            if not all([rock_filename, direction, initial_pos]):\r\n                print(f\"Incomplete remote rock throw data: {rock_data}\")\r\n                return\r\n            \r\n            # Ensure initial_pos is a dict\r\n            if not isinstance(initial_pos, dict):\r\n                initial_pos = {'x': initial_pos[0], 'y': initial_pos[1]} if isinstance(initial_pos, (list, tuple)) else {}\r\n            \r\n            # Find existing rock or create new one\r\n            rock = self._find_or_create_remote_rock(rock_filename, initial_pos)\r\n            \r\n            if rock:\r\n                # Mark as a remote rock\r\n                rock.is_remote = True\r\n                \r\n                # Simulate the throw\r\n                self._simulate_remote_rock_throw(rock, direction)\r\n                \r\n                # Check if our squid is in the path of the thrown rock\r\n                self._check_rock_collision_path(rock, direction, source_node_id)\r\n                \r\n                # Show message\r\n                if self.show_message:\r\n                    self.show_message(f\"Remote squid ({source_node_id[-4:]}) threw a rock {direction}!\")\r\n            \r\n        except Exception as e:\r\n            print(f\"Error handling remote rock throw: {e}\")\r\n            import traceback\r\n            traceback.print_exc()\r\n    \r\n    def _find_or_create_remote_rock(self, filename, pos):\r\n        \"\"\"Find an existing rock or create a new one for remote throws\"\"\"\r\n        # Get position values\r\n        pos_x = pos.get('x', 0)\r\n        pos_y = pos.get('y', 0)\r\n        \r\n        # Look for existing rocks with same filename near position\r\n        for item in self.scene.items():\r\n            if (hasattr(item, 'filename') and item.filename == filename and\r\n                abs(item.pos().x() - pos_x) < 50 and\r\n                abs(item.pos().y() - pos_y) < 50):\r\n                return item\r\n        \r\n        # Create new rock if not found\r\n        try:\r\n            # Check if file exists or if filename is None\r\n            if filename is None or not os.path.exists(filename):\r\n                # Try to find a default rock\r\n                default_rocks = [\r\n                    \"images/decoration/rock01.png\",\r\n                    \"images/decoration/rock02.png\",\r\n                    \"images/rock.png\"\r\n                ]\r\n                \r\n                # Use first valid file\r\n                for rock_file in default_rocks:\r\n                    if os.path.exists(rock_file):\r\n                        filename = rock_file\r\n                        break\r\n                else:\r\n                    print(f\"Could not find a valid rock image file\")\r\n                    return None\r\n            \r\n            rock_pixmap = QtGui.QPixmap(filename)\r\n            \r\n            # Create ResizablePixmapItem if available\r\n            ResizablePixmapItem = None\r\n            if hasattr(self.logic, 'user_interface') and hasattr(self.logic.user_interface, 'ResizablePixmapItem'):\r\n                ResizablePixmapItem = self.logic.user_interface.ResizablePixmapItem\r\n            \r\n            if ResizablePixmapItem:\r\n                rock = ResizablePixmapItem(rock_pixmap, filename)\r\n            else:\r\n                rock = QtWidgets.QGraphicsPixmapItem(rock_pixmap)\r\n                rock.filename = filename\r\n            \r\n            rock.setPos(pos_x, pos_y)\r\n            rock.setOpacity(0.7)  # Make it semi-transparent\r\n            rock.can_be_picked_up = True\r\n            self.scene.addItem(rock)\r\n            \r\n            # Mark as remote rock\r\n            rock.is_remote = True\r\n            \r\n            # Apply red tint to show it's from another instance\r\n            # Check if multiplayer plugin is available\r\n            if hasattr(self.logic, 'multiplayer_plugin') and self.logic.multiplayer_plugin:\r\n                self.logic.multiplayer_plugin.apply_foreign_object_tint(rock)\r\n            else:\r\n                # Apply tint directly if plugin reference isn't available\r\n                color_effect = QtWidgets.QGraphicsColorizeEffect()\r\n                color_effect.setColor(QtGui.QColor(255, 100, 100))\r\n                color_effect.setStrength(0.25)\r\n                rock.setGraphicsEffect(color_effect)\r\n                rock.is_foreign = True\r\n            \r\n            return rock\r\n        except Exception as e:\r\n            print(f\"Error creating remote rock: {e}\")\r\n            return None\r\n    \r\n    def _simulate_remote_rock_throw(self, rock, direction):\r\n        \"\"\"Simulate a remote rock throw with simplified physics\"\"\"\r\n        # Create timer for animation\r\n        throw_timer = QtCore.QTimer()\r\n        throw_counter = [0]  # Use list for mutable counter\r\n        \r\n        # Initial velocity based on direction\r\n        velocity_x = 12 * (1 if direction == \"right\" else -1)\r\n        velocity_y = -10  # Initial upward\r\n        \r\n        def update_position():\r\n            nonlocal velocity_x, velocity_y\r\n            \r\n            # Update position\r\n            current_pos = rock.pos()\r\n            new_x = current_pos.x() + velocity_x\r\n            new_y = current_pos.y() + velocity_y\r\n            \r\n            # Apply gravity\r\n            velocity_y += 0.4\r\n            \r\n            # Check boundaries\r\n            scene_rect = self.scene.sceneRect()\r\n            rock_rect = rock.boundingRect()\r\n            \r\n            # Left/right boundaries\r\n            if new_x < scene_rect.left():\r\n                new_x = scene_rect.left()\r\n                velocity_x *= -0.5\r\n            elif new_x > scene_rect.right() - rock_rect.width():\r\n                new_x = scene_rect.right() - rock_rect.width()\r\n                velocity_x *= -0.5\r\n            \r\n            # Top/bottom boundaries\r\n            if new_y < scene_rect.top():\r\n                new_y = scene_rect.top()\r\n                velocity_y *= -0.5\r\n            elif new_y > scene_rect.bottom() - rock_rect.height():\r\n                new_y = scene_rect.bottom() - rock_rect.height()\r\n                throw_timer.stop()\r\n                return\r\n            \r\n            # Update position\r\n            rock.setPos(new_x, new_y)\r\n            \r\n            # Stop after some time\r\n            throw_counter[0] += 1\r\n            if throw_counter[0] > 100:  # Stop after 100 updates\r\n                throw_timer.stop()\r\n        \r\n        # Start animation\r\n        throw_timer.timeout.connect(update_position)\r\n        throw_timer.start(50)  # 50ms intervals\r\n    \r\n    def _check_rock_collision_path(self, rock, direction, source_node_id):\r\n        \"\"\"Check if our squid is in the path of a thrown rock\"\"\"\r\n        # Skip if squid isn't initialized\r\n        if not self.squid:\r\n            return\r\n        \r\n        # Get rock and squid positions\r\n        rock_pos = rock.pos()\r\n        squid_rect = self.squid.squid_item.sceneBoundingRect()\r\n        squid_center = squid_rect.center()\r\n        \r\n        # Calculate relative positions\r\n        dx = squid_center.x() - rock_pos.x()\r\n        dy = squid_center.y() - rock_pos.y()\r\n        distance = math.sqrt(dx*dx + dy*dy)\r\n        \r\n        # Check if squid is in the direction of throw\r\n        in_path = (direction == \"right\" and dx > 0) or (direction == \"left\" and dx < 0)\r\n        \r\n        # If squid is close enough and in the throw path, react\r\n        if distance < 200 and in_path:\r\n            if hasattr(self.squid, 'react_to_rock_throw'):\r\n                # Use the specialized reaction method\r\n                self.squid.react_to_rock_throw(source_node_id, True)\r\n            elif hasattr(self.logic, 'startle_squid'):\r\n                # Fallback to generic startle\r\n                self.logic.startle_squid(source=\"incoming_rock\")\r\n                \r\n                # Add memory\r\n                if hasattr(self.squid, 'memory_manager'):\r\n                    self.squid.memory_manager.add_short_term_memory(\r\n                        'observation', 'rock_thrown',\r\n                        f\"Startled by rock thrown by remote squid ({source_node_id[-4:]})\"\r\n                    )\r\n\r\n\r\n\r\n"
  },
  {
    "path": "src/interactions2.py",
    "content": "# POOP INTERACTIONS\r\n\r\nimport math\r\nimport time\r\nimport random\r\nimport os\r\nfrom PyQt5 import QtCore, QtWidgets, QtGui\r\n\r\nclass PoopInteractionManager:\r\n    def __init__(self, squid, logic, scene, message_callback, config_manager):\r\n        self.squid = squid\r\n        self.logic = logic\r\n        self.scene = scene\r\n        self.show_message = message_callback\r\n        self.config_manager = config_manager\r\n        self.poop_config = config_manager.get_poop_config()\r\n\r\n        # Poop interaction state\r\n        self.target_poop = None\r\n        self.poop_test_phase = 0  # 0=approach, 1=carry, 2=throw\r\n        self.poop_carry_time = 0\r\n        self.poop_carry_duration = 0\r\n        \r\n        # Initialize timers\r\n        self.poop_test_timer = QtCore.QTimer()\r\n        self.throw_animation_timer = QtCore.QTimer()\r\n        \r\n        # Connect timers\r\n        self.poop_test_timer.timeout.connect(self.update_poop_test)\r\n        self.throw_animation_timer.timeout.connect(self.update_throw_animation)\r\n        \r\n        # Initialize throw velocity variables\r\n        self.throw_velocity_x = 0\r\n        self.throw_velocity_y = 0\r\n\r\n        # Add multiplayer support\r\n        self.multiplayer_plugin = None\r\n        self.setup_multiplayer_integration()\r\n\r\n    def setup_multiplayer_integration(self):\r\n        \"\"\"Set up hooks for multiplayer integration\"\"\"\r\n        if not hasattr(self.logic, 'plugin_manager'):\r\n            return False\r\n            \r\n        # Get multiplayer plugin\r\n        multiplayer_plugin = None\r\n        for plugin_name, plugin_data in self.logic.plugin_manager.plugins.items():\r\n            if plugin_name == \"multiplayer_plugin\":\r\n                # Get the actual plugin instance\r\n                multiplayer_plugin = plugin_data.get('instance')\r\n                break\r\n        \r\n        if multiplayer_plugin:\r\n            # Store reference\r\n            self.multiplayer_plugin = multiplayer_plugin\r\n            \r\n            print(\"[PoopInteraction] Successfully integrated with multiplayer plugin\")\r\n            return True\r\n        \r\n        return False\r\n\r\n    def is_valid_poop(self, item):\r\n        \"\"\"Check if item is a valid poop\"\"\"\r\n        if not isinstance(item, QtWidgets.QGraphicsPixmapItem):\r\n            return False\r\n        return (hasattr(item, 'category') and item.category == 'poop') or \\\r\n               (hasattr(item, 'filename') and 'poop' in item.filename.lower())\r\n\r\n    def can_pick_up_poop(self, poop):\r\n        \"\"\"Check if squid can pick up this poop\"\"\"\r\n        if not self.is_valid_poop(poop):\r\n            return False\r\n        if hasattr(poop, 'is_being_carried') and poop.is_being_carried:\r\n            return False\r\n            \r\n        # Check if poop is in cooldown after being thrown\r\n        if hasattr(poop, 'throw_cooldown_until'):\r\n            if time.time() < poop.throw_cooldown_until:\r\n                return False      \r\n        return True\r\n\r\n    def attach_poop_to_squid(self, poop):\r\n        \"\"\"Visually attach poop to squid at tentacle position\"\"\"\r\n        poop.setParentItem(self.squid.squid_item)\r\n        \r\n        # Set random hold duration between 3-9 seconds\r\n        self.squid.poop_hold_duration = random.uniform(3.0, 9.0)\r\n        self.squid.poop_hold_start_time = time.time()\r\n        self.squid.poop_decision_made = False\r\n        \r\n        offset = -50  # Both vertical and horizontal offset\r\n        \r\n        # Calculate position based on squid direction\r\n        if self.squid.squid_direction == \"right\":\r\n            # Position poop near right tentacles\r\n            poop.setPos(self.squid.squid_width - 40 + offset, \r\n                    self.squid.squid_height - 30 + offset)\r\n        elif self.squid.squid_direction == \"left\":\r\n            # Position poop near left tentacles\r\n            poop.setPos(10 + offset, \r\n                    self.squid.squid_height - 30 + offset)\r\n        elif self.squid.squid_direction == \"up\":\r\n            # Position poop near upper tentacles\r\n            poop.setPos(self.squid.squid_width//2 - 15 + offset, \r\n                    self.squid.squid_height - 40 + offset)\r\n        else:  # down/default\r\n            poop.setPos(self.squid.squid_width//2 - 15 + offset, \r\n                    self.squid.squid_height - 20 + offset)\r\n        \r\n        poop.is_being_carried = True\r\n        self.squid.is_carrying_poop = True\r\n        self.squid.carried_poop = poop\r\n        poop.setZValue(self.squid.squid_item.zValue() + 1)\r\n        \r\n        # Scale poop to appropriate size\r\n        poop.setScale(1.0)\r\n        \r\n        return True\r\n    \r\n    def check_poop_hold_time(self):\r\n        \"\"\"Check if holding time elapsed and make decision\"\"\"\r\n        if not hasattr(self.squid, 'carrying_poop') or not self.squid.carrying_poop or not hasattr(self.squid, 'poop_decision_made') or self.squid.poop_decision_made:\r\n            return\r\n        \r\n        current_time = time.time()\r\n        if current_time - self.squid.poop_hold_start_time >= self.squid.poop_hold_duration:\r\n            self.squid.poop_decision_made = True\r\n            self.decide_poop_action()\r\n\r\n    def decide_poop_action(self):\r\n        \"\"\"Randomly decide to throw or drop the poop\"\"\"\r\n        if random.random() < 0.7:  # 70% chance to throw\r\n            direction = \"right\" if random.random() < 0.5 else \"left\"\r\n            self.throw_poop(direction)\r\n            if self.show_message:\r\n                self.show_message(\"Squid threw the poop!\")\r\n        else:  # 30% chance to drop\r\n            self.drop_poop()\r\n            if self.show_message:\r\n                self.show_message(\"Squid dropped the poop\")\r\n\r\n    def drop_poop(self):\r\n        \"\"\"Gently place the poop below the squid\"\"\"\r\n        if not hasattr(self.squid, 'carrying_poop') or not self.squid.carrying_poop or not hasattr(self.squid, 'carried_poop') or not self.squid.carried_poop:\r\n            return\r\n        \r\n        poop = self.squid.carried_poop\r\n        poop.setParentItem(None)\r\n        poop.setPos(\r\n            self.squid.squid_x + self.squid.squid_width//2 - poop.boundingRect().width()//2,\r\n            self.squid.squid_y + self.squid.squid_height + 10\r\n        )\r\n        self.squid.is_carrying_poop = False\r\n        self.squid.carried_poop = None\r\n\r\n    def start_poop_test(self, poop=None):\r\n        \"\"\"Start test with guaranteed clean state and random carry duration\"\"\"\r\n        self.cleanup()  # Reset everything first\r\n        \r\n        if poop is None:\r\n            poops = [item for item in self.scene.items() \r\n                    if self.is_valid_poop(item) and item.isVisible()]\r\n            if not poops:\r\n                if self.show_message:\r\n                    self.show_message(\"No available poops!\")\r\n                return False\r\n            poop = min(poops, key=lambda p: math.hypot(\r\n                p.sceneBoundingRect().center().x() - self.squid.squid_x,\r\n                p.sceneBoundingRect().center().y() - self.squid.squid_y\r\n            ))\r\n        \r\n        self.target_poop = poop\r\n        self.poop_test_phase = 0\r\n        # Use the config values for duration\r\n        self.poop_carry_duration = random.uniform(\r\n            self.poop_config['min_carry_duration'],\r\n            self.poop_config['max_carry_duration']\r\n        )\r\n        self.poop_carry_time = 0  # Reset carry timer\r\n        self.poop_test_timer.start(100)  # 100ms updates\r\n        return True\r\n\r\n    def throw_poop(self, direction=\"right\"):\r\n        \"\"\"Initiates a poop throw with memory formation\"\"\"\r\n        # Prevent multiple throws\r\n        if self.throw_animation_timer.isActive():\r\n            return False\r\n            \r\n        if not hasattr(self.squid, 'carried_poop') or not self.squid.carried_poop:\r\n            return False\r\n        \r\n        config = self.poop_config\r\n        poop = self.squid.carried_poop\r\n        \r\n        # Set squid status\r\n        if hasattr(self.squid, 'status'):\r\n            self.squid.status = \"throwing poop\"\r\n        \r\n        # Detach from squid and reset parent to scene\r\n        poop.setParentItem(None)\r\n        \r\n        # Calculate throw vectors\r\n        throw_power = 12\r\n        angle = math.radians(30 if direction == \"right\" else 150)\r\n        self.throw_velocity_x = throw_power * math.cos(angle)\r\n        self.throw_velocity_y = -throw_power * math.sin(angle)  # Negative for upward\r\n        \r\n        # Set position to squid's center in scene coordinates\r\n        squid_rect = self.squid.squid_item.sceneBoundingRect()\r\n        poop_rect = poop.boundingRect()\r\n        poop.setPos(\r\n            squid_rect.center().x() - poop_rect.width()/2,\r\n            squid_rect.center().y() - poop_rect.height()/2\r\n        )\r\n        \r\n        poop.setVisible(True)\r\n        \r\n        # Apply stat changes\r\n        self.squid.happiness = min(100, self.squid.happiness - 5)\r\n        self.squid.satisfaction = min(100, self.squid.satisfaction - 3)\r\n        self.squid.anxiety = min(100, self.squid.anxiety + 10)\r\n        self.logic.statistics_window.award(-75)\r\n        \r\n        # Simplified negative memory\r\n        memory_details = {\r\n            \"activity\": \"poop_throwing\",\r\n            \"item\": getattr(poop, 'filename', '') or '',\r\n            \"effects\": {\r\n                \"happiness\": -5,\r\n                \"satisfaction\": -3,\r\n                \"anxiety\": 10\r\n            },\r\n            \"description\": \"Threw a poop around!\",\r\n            \"is_positive\": False\r\n        }\r\n\r\n        # Update poops thrown counter\r\n        if hasattr(self.squid, 'statistics'):\r\n            self.squid.statistics.total_poops_thrown += 1\r\n        \r\n        # Add with moderate importance\r\n        self.squid.memory_manager.add_short_term_memory(\r\n            'play',\r\n            'poop_throwing',\r\n            memory_details,\r\n            importance=5\r\n        )\r\n        \r\n        # Broadcast to network if multiplayer plugin is available\r\n        if hasattr(self, 'multiplayer_plugin') and self.multiplayer_plugin:\r\n            try:\r\n                self.multiplayer_plugin.throw_poop_network(poop, direction)\r\n            except Exception as e:\r\n                print(f\"[PoopInteraction] Error broadcasting poop throw: {e}\")\r\n        \r\n        self.throw_animation_timer.start(50)\r\n        return True\r\n\r\n    def update_poop_test(self):\r\n        \"\"\"Handle the poop test sequence (approach, carry)\"\"\"\r\n        if not self.target_poop:\r\n            self.cleanup()\r\n            return\r\n        \r\n        # Let the Squid class handle the timing and decision making\r\n        if self.poop_test_phase == 0:  # Approach phase\r\n            poop_center = self.target_poop.sceneBoundingRect().center()\r\n            squid_center = self.squid.squid_item.sceneBoundingRect().center()\r\n            distance = math.hypot(poop_center.x()-squid_center.x(),\r\n                                poop_center.y()-squid_center.y())\r\n\r\n            if distance < 50:  # Close enough to pick up\r\n                if self.attach_poop_to_squid(self.target_poop):\r\n                    self.poop_test_phase = 1  # Move to carry phase\r\n                else:\r\n                    self.cleanup()\r\n            else:\r\n                self.squid.move_toward_position(poop_center)\r\n\r\n    def update_throw_animation(self):\r\n        \"\"\"Handles the physics update for thrown poops\"\"\"\r\n        if not hasattr(self.squid, 'carried_poop') or not self.squid.carried_poop:\r\n            self.throw_animation_timer.stop()\r\n            self.cleanup_after_throw()\r\n            return\r\n        \r\n        poop = self.squid.carried_poop\r\n        new_x = poop.x() + self.throw_velocity_x\r\n        new_y = poop.y() + self.throw_velocity_y\r\n        \r\n        # Apply gravity\r\n        self.throw_velocity_y += 0.3\r\n        \r\n        # Boundary checks\r\n        scene_rect = self.scene.sceneRect()\r\n        poop_rect = poop.boundingRect()\r\n        \r\n        # Left/right boundaries with reduced bounce\r\n        if new_x < scene_rect.left():\r\n            new_x = scene_rect.left()\r\n            self.throw_velocity_x *= -0.5\r\n        elif new_x > scene_rect.right() - poop_rect.width():\r\n            new_x = scene_rect.right() - poop_rect.width()\r\n            self.throw_velocity_x *= -0.5\r\n        \r\n        # Top/bottom boundaries\r\n        if new_y < scene_rect.top():\r\n            new_y = scene_rect.top()\r\n            self.throw_velocity_y *= -0.5\r\n        elif new_y > scene_rect.bottom() - poop_rect.height():\r\n            new_y = scene_rect.bottom() - poop_rect.height()\r\n            self.throw_animation_timer.stop()\r\n            self.cleanup_after_throw()\r\n            return  # Stop updates when hitting bottom\r\n        \r\n        poop.setPos(new_x, new_y)\r\n\r\n    def cleanup(self):\r\n        \"\"\"Reset all poop interaction state\"\"\"\r\n        self.poop_test_timer.stop()\r\n        self.throw_animation_timer.stop()\r\n        \r\n        self.target_poop = None\r\n        self.poop_test_phase = 0\r\n        self.poop_carry_time = 0\r\n        self.poop_carry_duration = 0\r\n        \r\n        if hasattr(self.squid, 'is_carrying_poop') and self.squid.is_carrying_poop:\r\n            self.squid.is_carrying_poop = False\r\n            self.squid.carried_poop = None\r\n\r\n    def cleanup_after_throw(self):\r\n        if hasattr(self.squid, 'carried_poop') and self.squid.carried_poop:\r\n            poop = self.squid.carried_poop\r\n            \r\n            # --- ADDED COOLDOWN APPLICATION ---\r\n            # Set cooldown on the poop so it can't be picked up immediately\r\n            cooldown = self.poop_config.get('cooldown_after_throw', 10.0)\r\n            poop.throw_cooldown_until = time.time() + cooldown\r\n            # ----------------------------------\r\n\r\n            # Make sure to reset all poop-related states\r\n            poop.is_being_carried = False\r\n            self.squid.carried_poop = None\r\n        \r\n        # Reset squid states\r\n        self.squid.is_carrying_poop = False\r\n        \r\n        # Reset status to default if it was set to \"throwing_poop\"\r\n        if hasattr(self.squid, 'status') and self.squid.status == \"throwing_poop\":\r\n            self.squid.status = \"roaming\"\r\n        \r\n        self.throw_velocity_x = 0\r\n        self.throw_velocity_y = 0\r\n        self.cleanup()\r\n\r\n    def setup_timers(self, interval=100):\r\n        \"\"\"Configure timer intervals\"\"\"\r\n        self.poop_test_timer.setInterval(interval)\r\n        self.throw_animation_timer.setInterval(50)\r\n\r\n\r\n\r\n"
  },
  {
    "path": "src/laboratory.py",
    "content": "# 2.4.5.1 | rev3_dec25\r\n#  --------------------------------------------------------------\r\n#  NEURON LABORATORY\r\n#  --------------------------------------------------------------\r\n\r\nfrom PyQt5.QtCore import Qt, QTimer, pyqtSignal\r\nfrom PyQt5 import QtCore\r\nfrom PyQt5.QtGui import (\r\n    QFont, QPixmap, QColor, QPainter, QBrush, QPen, QDoubleValidator\r\n)\r\nfrom PyQt5.QtWidgets import (\r\n    QApplication, QCheckBox, QComboBox, QDialog, QFormLayout, QFrame,\r\n    QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QProgressBar,\r\n    QPushButton, QScrollArea, QSlider, QSpinBox, QTabWidget, QTextEdit,\r\n    QVBoxLayout, QWidget, QMessageBox, QTableWidget, QTableWidgetItem,\r\n    QHeaderView, QSplitter, QToolButton\r\n)\r\n\r\nimport json, math, time, random, datetime as dt\r\nfrom .localisation import loc  # Import localisation\r\n\r\n# ------------------------------------------------------------------\r\n#  Helper: coloured connection badge\r\n# ------------------------------------------------------------------\r\ndef badge(text, color=\"#333\", bg=\"#eee\"):\r\n    return f\"\"\"<span style=\"color:{color};background:{bg};\r\n               padding:2px 6px;border-radius:4px;font-size:10pt;\r\n               font-weight:600;\">{text}</span>\"\"\"\r\n\r\n\r\n# ------------------------------------------------------------------\r\n#  Main Laboratory Dialog\r\n# ------------------------------------------------------------------\r\nclass NeuronLaboratory(QDialog):\r\n    def __init__(self, brain_widget, parent=None):\r\n        super().__init__(parent)\r\n        self.bw = brain_widget\r\n        self.setWindowTitle(loc(\"lab_title\", \"🧠  Neuron Laboratory\"))\r\n        self.resize(900, 750)\r\n        self.setWindowFlag(Qt.WindowMinMaxButtonsHint)\r\n\r\n        # ---- top toolbar ----\r\n        bar = QHBoxLayout()\r\n        self.live_check = QCheckBox(loc(\"lab_live_refresh\", \"Live refresh\"))\r\n        self.live_check.setChecked(True)\r\n        self.live_check.toggled.connect(self._toggle_live)\r\n        bar.addWidget(self.live_check)\r\n\r\n        bar.addStretch()\r\n        self.lock_check = QCheckBox(loc(\"lab_unlock_editing\", \"🔓  Unlock editing\"))\r\n        self.lock_check.toggled.connect(self._unlock_editing)\r\n        bar.addWidget(self.lock_check)\r\n\r\n        # ---- main notebook ----\r\n        self.tabs = QTabWidget()\r\n        \r\n        # --- Apply Card-Based Styling ---\r\n        self.tabs.setStyleSheet(\"\"\"\r\n            QTabWidget::pane {\r\n                border: 2px solid #e1e5eb;\r\n                border-radius: 12px;\r\n                background-color: #f8f9fa;\r\n            }\r\n            QTabBar::tab {\r\n                background: #f8f9fa;\r\n                border: 1px solid #e1e5eb;\r\n                padding: 10px 20px;\r\n                margin-right: 5px;\r\n                border-top-left-radius: 8px;\r\n                border-top-right-radius: 8px;\r\n                font-size: 14px;\r\n                color: #2c3e50;\r\n            }\r\n            QTabBar::tab:selected {\r\n                background: #ffffff;\r\n                border-bottom: none;\r\n                font-weight: 600;\r\n            }\r\n            /* Style all QGroupBoxes to appear as modern cards */\r\n            QGroupBox { \r\n                background-color: #ffffff;\r\n                border: 1px solid #dee2e6;\r\n                border-radius: 10px;\r\n                padding-top: 20px; \r\n                margin-top: 10px; \r\n            }\r\n            QGroupBox::title {\r\n                subcontrol-origin: margin;\r\n                subcontrol-position: top left;\r\n                padding: 0 3px;\r\n                left: 10px;\r\n                color: #1976d2; /* Use a primary color for card titles */\r\n                font-weight: bold;\r\n                font-size: 12pt;\r\n            }\r\n        \"\"\")\r\n        # ------------------------------------------------------------------\r\n        \r\n        # Initialize forced values and timer for absolute override\r\n        self.forced_neurons = {}  # Dictionary to store forced values: name -> value\r\n        self._force_timer = QTimer(self)\r\n        self._force_timer.timeout.connect(self._apply_forced_values)\r\n        self._force_timer.start(100)  # Check 10 times per second for smooth override\r\n        \r\n        self._build_overview_tab()\r\n        self._build_inspector_tab()\r\n        self._build_edit_tab()\r\n\r\n        # ---- footer ----\r\n        self.status_lbl = QLabel(loc(\"lab_status_ready\", \"Ready\"))\r\n        self.status_lbl.setStyleSheet(\"color:#888;font-size:9pt;\")\r\n\r\n        lay = QVBoxLayout(self)\r\n        lay.addLayout(bar)\r\n        lay.addWidget(self.tabs)\r\n        lay.addWidget(self.status_lbl)\r\n\r\n        # ---- refresh timer ----\r\n        self.timer = QTimer(self)\r\n        self.timer.timeout.connect(self._refresh)\r\n        self.timer.start(1000)  # 1 Hz\r\n\r\n        self._refresh()  # first paint\r\n\r\n        # ---- per-neuron manual lock table ----\r\n        self.locked_neurons = {}   # name -> {locked: bool, slider, spin, button}\r\n\r\n    # ================================================================\r\n    #  Construction helpers\r\n    # ================================================================\r\n    def _build_overview_tab(self):\r\n        self.ov_scroll = QScrollArea()\r\n        self.ov_widget = QWidget()\r\n        self.ov_grid = QGridLayout(self.ov_widget)\r\n        self.ov_scroll.setWidget(self.ov_widget)\r\n        self.ov_scroll.setWidgetResizable(True)\r\n        self.tabs.addTab(self.ov_scroll, loc(\"lab_tab_overview\", \"📊  Live Overview\"))\r\n\r\n    def _build_inspector_tab(self):\r\n        w = QWidget()\r\n        lay = QVBoxLayout(w)\r\n        self.pick_neuron = QComboBox()\r\n        self.pick_neuron.currentTextChanged.connect(self._inspect_neuron)\r\n        lay.addWidget(QLabel(loc(\"lab_pick_neuron\", \"Pick a neuron to inspect:\")))\r\n        self.pick_neuron.setStyleSheet(\"\"\"\r\n            QComboBox { font-size: 18px; min-height: 36px; padding: 4px; }\r\n        \"\"\")\r\n        lay.addWidget(self.pick_neuron)\r\n        self.inspector_scroll = QScrollArea()\r\n        self.inspector_cards = QWidget()\r\n        self.inspector_lay = QVBoxLayout(self.inspector_cards)\r\n        self.inspector_scroll.setWidget(self.inspector_cards)\r\n        self.inspector_scroll.setWidgetResizable(True)\r\n        lay.addWidget(self.inspector_scroll, 1)\r\n        self.tabs.addTab(w, loc(\"lab_tab_inspector\", \"🔍  Deep Inspector\"))\r\n\r\n    def _build_edit_tab(self):\r\n        w = QWidget()\r\n        lay = QVBoxLayout(w)\r\n        warn = QLabel(loc(\"lab_edit_locked_msg\", \"⚠️  Editing is locked – check 'Unlock editing' in the toolbar.\"))\r\n        warn.setStyleSheet(\"color:#d9534f;font-weight:bold;\")\r\n        lay.addWidget(warn)\r\n        self.edit_warn = warn\r\n        self.edit_scroll = QScrollArea()\r\n        self.edit_cards = QWidget()\r\n        self.edit_lay = QVBoxLayout(self.edit_cards)\r\n        self.edit_scroll.setWidget(self.edit_cards)\r\n        self.edit_scroll.setWidgetResizable(True)\r\n        lay.addWidget(self.edit_scroll, 1)\r\n        self.tabs.addTab(w, loc(\"lab_tab_edit\", \"🔧  Edit Sandbox\"))\r\n\r\n    # ================================================================\r\n    #  Live refresh\r\n    # ================================================================\r\n    def _refresh(self):\r\n        if not self.live_check.isChecked():\r\n            return\r\n        current = self.pick_neuron.currentText()\r\n        self.pick_neuron.clear()\r\n        \r\n        # Translate neuron names if needed, or use raw keys? \r\n        # Usually keys are used internally, but displayed names might be localized.\r\n        # For this tool, we usually show keys, but let's stick to keys for consistency with other tools.\r\n        self.pick_neuron.addItems(sorted(self.bw.neuron_positions.keys()))\r\n        \r\n        idx = self.pick_neuron.findText(current)\r\n        if idx >= 0:\r\n            self.pick_neuron.setCurrentIndex(idx)\r\n        self._paint_overview()\r\n        self._inspect_neuron(self.pick_neuron.currentText())\r\n        self._paint_edit()\r\n\r\n    def select_neuron_by_name(self, neuron_name: str):\r\n        \"\"\"\r\n        Selects the specified neuron in the pick_neuron dropdown\r\n        and refreshes the Inspector tab content.\r\n        \"\"\"\r\n        if not hasattr(self, 'pick_neuron'):\r\n            return\r\n                \r\n        # 1. Select the neuron in the dropdown\r\n        idx = self.pick_neuron.findText(neuron_name)\r\n        if idx >= 0:\r\n            self.pick_neuron.blockSignals(True)\r\n            self.pick_neuron.setCurrentIndex(idx)\r\n            self.pick_neuron.blockSignals(False)\r\n                \r\n            # 2. Force the inspection of the newly selected neuron\r\n            self._inspect_neuron(neuron_name)\r\n                \r\n            # 3. Switch to the \"Deep Inspector\" tab\r\n            self.tabs.setCurrentIndex(1)\r\n                \r\n            # 4. Update the view to reflect the change\r\n            self.update()\r\n\r\n    # ================================================================\r\n    #  Overview / Inspector / Edit\r\n    # ================================================================\r\n    def _paint_overview(self):\r\n        while self.ov_grid.count():\r\n            item = self.ov_grid.takeAt(0)\r\n            if item and item.widget():\r\n                item.widget().deleteLater()\r\n        nd = getattr(self.bw, 'neurogenesis_data', {})\r\n        cfg = getattr(self.bw, 'neurogenesis_config', {})\r\n        \r\n        # Card 1: Counters\r\n        card1 = QGroupBox(loc(\"lab_ov_counters\", \"Counter progress\"))\r\n        g1 = QGridLayout(card1)\r\n        # We localize the counter labels here using existing keys or raw if simple\r\n        metrics = [(loc(\"novelty\", \"Novelty\"), nd.get('novelty_counter', 0), cfg.get('novelty_threshold', 3)),\r\n                   (loc(\"stress\", \"Stress\"), nd.get('stress_counter', 0), cfg.get('stress_threshold', .7)),\r\n                   (loc(\"reward\", \"Reward\"), nd.get('reward_counter', 0), cfg.get('reward_threshold', .6))]\r\n        for row, (name, cur, thr) in enumerate(metrics):\r\n            pct = min(100, (cur / thr) * 100) if thr else 0\r\n            bar = self._progress_bar(pct)\r\n            g1.addWidget(QLabel(f\"{name} <b>{cur:.2f}</b>/{thr}\"), row, 0)\r\n            g1.addWidget(bar, row, 1)\r\n        self.ov_grid.addWidget(card1, 0, 0)\r\n        \r\n        # Card 2: Newest neurons\r\n        card2 = QGroupBox(loc(\"lab_ov_newest\", \"Newest neurogenesis neurons\"))\r\n        v2 = QVBoxLayout(card2)\r\n        details = nd.get('new_neurons_details', {})\r\n        for name, info in sorted(details.items(), key=lambda x: x[1].get('created_at', 0), reverse=True)[:5]:\r\n            age = int(time.time() - info.get('created_at', 0))\r\n            ago_text = loc(\"lab_ago\", \"{seconds}s ago\", seconds=age)\r\n            v2.addWidget(QLabel(f\"<b>{name}</b>  –  {info.get('trigger_type','?')}  –  {ago_text}\"))\r\n        if not details:\r\n            v2.addWidget(QLabel(loc(\"lab_none_yet\", \"None yet\")))\r\n        self.ov_grid.addWidget(card2, 0, 1)\r\n        \r\n        # Card 3: Limits\r\n        card3 = QGroupBox(loc(\"lab_ov_limits\", \"Limits & pruning\"))\r\n        v3 = QVBoxLayout(card3)\r\n        current = len(self.bw.neuron_positions) - len(self.bw.excluded_neurons)\r\n        max_n = cfg.get('max_neurons', 32)\r\n        v3.addWidget(self._progress_widget(loc(\"neurons\", \"Neurons\"), current, max_n))\r\n        pruning_text = loc(\"lab_pruning_enabled\", \"Pruning enabled:\")\r\n        v3.addWidget(QLabel(f\"{pruning_text} <b>{self.bw.pruning_enabled}</b>\"))\r\n        self.ov_grid.addWidget(card3, 1, 0)\r\n        \r\n        # Card 4: Quick Actions\r\n        card4 = QGroupBox(loc(\"lab_ov_actions\", \"Quick actions\"))\r\n        h = QHBoxLayout(card4)\r\n        btn = QPushButton(loc(\"lab_force_hebbian\", \"Force Hebbian cycle\"))\r\n        btn.clicked.connect(self.bw.perform_hebbian_learning)\r\n        h.addWidget(btn)\r\n        self.ov_grid.addWidget(card4, 1, 1)\r\n        self.ov_grid.setRowStretch(2, 1)\r\n\r\n    def _inspect_neuron(self, name):\r\n        if not name:\r\n            return\r\n        while self.inspector_lay.count():\r\n            item = self.inspector_lay.takeAt(0)\r\n            if item and item.widget():\r\n                item.widget().deleteLater()\r\n        nd = getattr(self.bw, 'neurogenesis_data', {})\r\n        details = nd.get('new_neurons_details', {}).get(name)\r\n\r\n        # Card: Connector Special Details (If applicable)\r\n        if details and details.get('trigger_type') == 'connector':\r\n            card_special = QGroupBox(loc(\"lab_connector_title\", \"Network Bridge Protocol\"))\r\n            v_special = QVBoxLayout(card_special)\r\n            \r\n            info_text = (\r\n                \"<b>Status:</b> <span style='color:blue'>Active Bridge</span><br>\"\r\n                \"This neuron was synthesized to rescue an isolated node.<br><br>\"\r\n                \"<b>Wiring Topology:</b><br>\"\r\n                \"• <b>Primary:</b> Connection to the Orphan<br>\"\r\n                \"• <b>Anchor:</b> Connection to Closest Neighbor<br>\"\r\n                \"• <b>Diversity:</b> Random connection to Network\"\r\n            )\r\n            lbl_special = QLabel(info_text)\r\n            lbl_special.setTextFormat(QtCore.Qt.RichText)\r\n            v_special.addWidget(lbl_special)\r\n            self.inspector_lay.addWidget(card_special)\r\n\r\n        # Card: Connections\r\n        card2 = QGroupBox(loc(\"lab_connections_title\", \"Connections (excitatory vs inhibitory)\"))\r\n        v2 = QVBoxLayout(card2)\r\n        \r\n        h_partner = loc(\"lab_header_partner\", \"Partner\")\r\n        h_weight = loc(\"lab_header_weight\", \"Weight\")\r\n        h_type = loc(\"lab_header_type\", \"Type\")\r\n        h_inf = loc(\"lab_header_inf\", \"Influence\")\r\n        \r\n        html = \"<table width='100%'>\"\r\n        html += f\"<tr><th>{h_partner}</th><th>{h_weight}</th><th>{h_type}</th><th>{h_inf}</th></tr>\"\r\n        \r\n        t_exc = loc(\"lab_type_excitatory\", \"Excitatory\")\r\n        t_inh = loc(\"lab_type_inhibitory\", \"Inhibitory\")\r\n        \r\n        for (src, dst), w in self.bw.weights.items():\r\n            if src == name:\r\n                typ = t_exc if w > 0 else t_inh\r\n                col = \"#d4ffd4\" if w > 0 else \"#ffd4d4\"\r\n                html += f\"<tr bgcolor='{col}'>\"\r\n                html += f\"<td>{dst}</td><td>{w:+.3f}</td><td>{badge(typ,'#000',col)}</td>\"\r\n                html += f\"<td>{self._influence_badge(w)}</td></tr>\"\r\n        for (src, dst), w in self.bw.weights.items():\r\n            if dst == name:\r\n                typ = t_exc if w > 0 else t_inh\r\n                col = \"#d4ffd4\" if w > 0 else \"#ffd4d4\"\r\n                html += f\"<tr bgcolor='{col}'>\"\r\n                html += f\"<td>{src} →</td><td>{w:+.3f}</td><td>{badge(typ,'#000',col)}</td>\"\r\n                html += f\"<td>{self._influence_badge(w, incoming=True)}</td></tr>\"\r\n        html += \"</table>\"\r\n        lbl = QLabel(html)\r\n        lbl.setWordWrap(True)\r\n        lbl.setTextFormat(QtCore.Qt.RichText)\r\n        v2.addWidget(lbl)\r\n        self.inspector_lay.addWidget(card2)\r\n        \r\n        # Card: Impacts\r\n        card3 = QGroupBox(loc(\"lab_impact_title\", \"Functional impact simulation\"))\r\n        v3 = QVBoxLayout(card3)\r\n        impacts = self._compute_impacts(name)\r\n        if impacts:\r\n            h_neuron = loc(\"lab_header_neuron\", \"Neuron\")\r\n            h_delta = loc(\"lab_header_delta\", \"Δ Value\")\r\n            \r\n            html = \"<table width='100%'>\"\r\n            html += f\"<tr><th>{h_neuron}</th><th>{h_delta}</th></tr>\"\r\n            for partner, delta in impacts.items():\r\n                col = \"#d4ffd4\" if delta > 0 else \"#ffd4d4\"\r\n                html += f\"<tr bgcolor='{col}'><td>{partner}</td><td>{delta:+.2f}</td></tr>\"\r\n            html += \"</table>\"\r\n            lbl = QLabel(html)\r\n            lbl.setWordWrap(True)\r\n            lbl.setTextFormat(QtCore.Qt.RichText)\r\n            v3.addWidget(lbl)\r\n        else:\r\n            v3.addWidget(QLabel(loc(\"lab_no_connections\", \"No active connections at the moment\")))\r\n        self.inspector_lay.addWidget(card3)\r\n        \r\n        # Card: Educational\r\n        card4 = QGroupBox(loc(\"lab_did_you_know\", \"Did you know?\"))\r\n        v4 = QVBoxLayout(card4)\r\n        v4.addWidget(QLabel(self._educational_tip(name)))\r\n        self.inspector_lay.addWidget(card4)\r\n        self.inspector_lay.addStretch(1)\r\n\r\n    # ================================================================\r\n    #  EDIT tab\r\n    # ================================================================\r\n    def _paint_edit(self):\r\n        while self.edit_lay.count():\r\n            item = self.edit_lay.takeAt(0)\r\n            if item and item.widget():\r\n                item.widget().deleteLater()\r\n        if not self.lock_check.isChecked():\r\n            return\r\n\r\n        card = QGroupBox(loc(\"lab_edit_header\", \"Neuron values (drag to change)  –  click 🔒 to lock\"))\r\n        grid = QGridLayout(card)\r\n\r\n        for row, name in enumerate(sorted(self.bw.neuron_positions.keys())):\r\n            val = self.forced_neurons.get(name, self.bw.state.get(name, 50))\r\n            if isinstance(val, bool):\r\n                continue\r\n\r\n            # Preserve existing lock state\r\n            was_locked = self.locked_neurons.get(name, {}).get(\"locked\", False)\r\n\r\n            # label\r\n            grid.addWidget(QLabel(name), row, 0)\r\n\r\n            # slider\r\n            slider = QSlider(QtCore.Qt.Horizontal)\r\n            slider.setRange(0, 100)\r\n            slider.setValue(int(val))\r\n            slider.valueChanged.connect(lambda v, n=name: self._set_neuron(n, v))\r\n            grid.addWidget(slider, row, 1)\r\n\r\n            # spin-box\r\n            spin = QSpinBox()\r\n            spin.setRange(0, 100)\r\n            spin.setValue(int(val))\r\n            spin.valueChanged.connect(lambda v, n=name: self._set_neuron(n, v))\r\n            grid.addWidget(spin, row, 2)\r\n\r\n            # pad-lock button\r\n            btn = QToolButton()\r\n            btn.setCheckable(True)\r\n            btn.setChecked(was_locked)\r\n            btn.setText(\"🔒\" if was_locked else \"🔓\")\r\n            btn.setFixedSize(24, 24)\r\n            btn.setStyleSheet(\"QToolButton:checked { color: red; }\")\r\n            btn.toggled.connect(lambda checked, n=name, b=btn: self._toggle_lock(n, b))\r\n            grid.addWidget(btn, row, 3)\r\n\r\n            # store references\r\n            self.locked_neurons[name] = {\r\n                \"locked\": was_locked,\r\n                \"slider\": slider,\r\n                \"spin\": spin,\r\n                \"button\": btn\r\n            }\r\n\r\n        self.edit_lay.addWidget(card)\r\n        self.edit_lay.addStretch(1)\r\n\r\n    # -----------  lock / set slots  ---------------------------------\r\n    def update_debug_info(self):\r\n        self._refresh()\r\n\r\n    def _toggle_lock(self, name, button):\r\n        is_locked = button.isChecked()\r\n        self.locked_neurons[name][\"locked\"] = is_locked\r\n        button.setText(\"🔒\" if is_locked else \"🔓\")\r\n        \r\n        if is_locked:\r\n            current_value = self.bw.state.get(name, 50)\r\n            self.forced_neurons[name] = int(current_value)\r\n            self.status_lbl.setText(loc(\"lab_status_locked\", \"🔒 {name} locked at {value}\", name=name, value=current_value))\r\n        else:\r\n            if name in self.forced_neurons:\r\n                del self.forced_neurons[name]\r\n            self.status_lbl.setText(loc(\"lab_status_unlocked\", \"🔓 {name} unlocked\", name=name))\r\n\r\n    def _set_neuron(self, name, value):\r\n        self.forced_neurons[name] = value\r\n        self.bw.state[name] = value\r\n        self.bw.update()\r\n\r\n    def _apply_forced_values(self):\r\n        if not self.isVisible():\r\n            return\r\n            \r\n        for name, value in self.forced_neurons.items():\r\n            if name in self.bw.state:\r\n                self.bw.state[name] = value\r\n                \r\n                # NEW: Sync to squid if it's a core statistic neuron\r\n                if hasattr(self.bw, 'tamagotchi_logic') and hasattr(self.bw.tamagotchi_logic, 'squid'):\r\n                    squid = self.bw.tamagotchi_logic.squid\r\n                    if name in ['hunger', 'happiness', 'cleanliness', 'sleepiness', \r\n                            'health', 'satisfaction', 'curiosity', 'anxiety']:\r\n                        setattr(squid, name, value)\r\n                \r\n                if name in self.locked_neurons and self.locked_neurons[name][\"locked\"]:\r\n                    slider = self.locked_neurons[name][\"slider\"]\r\n                    spin = self.locked_neurons[name][\"spin\"]\r\n                    \r\n                    int_value = int(value)\r\n                    if slider.value() != int_value:\r\n                        slider.blockSignals(True)\r\n                        slider.setValue(int_value)\r\n                        slider.blockSignals(False)\r\n                    \r\n                    if spin.value() != int_value:\r\n                        spin.blockSignals(True)\r\n                        spin.setValue(int_value)\r\n                    spin.blockSignals(False)\r\n\r\n    # ================================================================\r\n    #  Slots\r\n    # ================================================================\r\n    def _toggle_live(self, on):\r\n        self.timer.setInterval(1000 if on else 10000)\r\n\r\n    def _unlock_editing(self, on):\r\n        if on:\r\n            title = loc(\"lab_unlock_title\", \"Unlock editing?\")\r\n            msg = loc(\"lab_unlock_msg\", \"You can now change neuron values and force creation events. Use responsibly!\")\r\n            ans = QMessageBox.question(self, title, msg)\r\n            if ans != QMessageBox.Yes:\r\n                self.lock_check.setChecked(False)\r\n                return\r\n        self.edit_warn.setVisible(not on)\r\n        self._paint_edit()\r\n\r\n    def _force_neurogenesis(self, typ):\r\n        fake_state = {\"_debug_forced_neurogenesis\": True,\r\n                      f\"{typ}_exposure\": 999}\r\n        self.bw.update_state(fake_state)\r\n\r\n    # ================================================================\r\n    #  Pretty helpers\r\n    # ================================================================\r\n    def _progress_bar(self, pct):\r\n        bar = QProgressBar()\r\n        bar.setRange(0, 100)\r\n        bar.setValue(int(pct))\r\n        bar.setTextVisible(True)\r\n        bar.setStyleSheet(\"QProgressBar::chunk{background:#4CAF50;}\")\r\n        return bar\r\n\r\n    def _progress_widget(self, title, cur, maxi):\r\n        w = QWidget()\r\n        h = QHBoxLayout(w)\r\n        h.setContentsMargins(0, 0, 0, 0)\r\n        h.addWidget(QLabel(f\"{title}  {cur}/{maxi}\"))\r\n        bar = self._progress_bar((cur / maxi) * 100)\r\n        bar.setMaximumHeight(12)\r\n        h.addWidget(bar)\r\n        return w\r\n\r\n    def _influence_badge(self, w, incoming=False):\r\n        mag = abs(w)\r\n        if mag < 0.1:\r\n            return badge(loc(\"lab_inf_tiny\", \"tiny\"), \"#666\", \"#fff\")\r\n        if mag < 0.3:\r\n            return badge(loc(\"lab_inf_mild\", \"mild\"), \"#fff\", \"#555\")\r\n        if mag < 0.6:\r\n            return badge(loc(\"lab_inf_mod\", \"moderate\"), \"#fff\", \"#000\")\r\n        return badge(loc(\"lab_inf_strong\", \"STRONG\"), \"#fff\", \"#d9534f\")\r\n\r\n    def _compute_impacts(self, name):\r\n        impacts = {}\r\n        val = self.bw.state.get(name, 50)\r\n        if abs(val - 50) < 5:\r\n            return impacts\r\n        # outgoing\r\n        for (src, dst), w in self.bw.weights.items():\r\n            if src == name and dst not in self.bw.excluded_neurons:\r\n                impacts[dst] = (val - 50) * w * 0.5\r\n        return impacts\r\n\r\n    def _educational_tip(self, name):\r\n        # 1. Try specific key first\r\n        specific_key = f\"lab_tip_{name}\"\r\n        text = loc(specific_key)\r\n        \r\n        # Check if translation was found (if loc returns key when missing)\r\n        if text != specific_key:\r\n            return text\r\n\r\n        # 2. Core neurons\r\n        core_keys = [\"hunger\", \"happiness\", \"anxiety\", \"curiosity\"]\r\n        if name in core_keys:\r\n            return loc(specific_key)\r\n                \r\n        if name in self.bw.original_neuron_positions:\r\n            return loc(\"lab_tip_core\", \"Core neuron – fundamental to survival.\")\r\n                \r\n        nd = getattr(self.bw, 'neurogenesis_data', {})\r\n        det = nd.get('new_neurons_details', {}).get(name)\r\n            \r\n        if not det:\r\n            return loc(\"lab_tip_neuro_default\", \"Neurogenesis neuron – purpose inferred from birth context.\")\r\n\r\n        # Special handling for connectors\r\n        if det.get('trigger_type') == 'connector':\r\n            return loc(\"lab_tip_connector\", \"Generated by the network to connect orphaned neurons. Has 3 connections.\")\r\n                \r\n        return loc(\"lab_tip_neuro_fmt\", \r\n                \"Created by <b>{trigger}</b> – specialises in <b>{spec}</b>. Its job is to turn experiences into long-term behaviour.\",\r\n                trigger=det.get('trigger_type'),\r\n                spec=det.get('specialisation','?'))\r\n\r\n\r\nNeurogenesisDebugDialog = NeuronLaboratory  #  Old name alias – Backwards compatibility\r\n\r\n\r\n# ------------------------------------------------------------------\r\n#  Quick test when run standalone\r\n# ------------------------------------------------------------------\r\nif __name__ == \"__main__\":\r\n    import sys\r\n    app = QApplication(sys.argv)\r\n    # dummy brain-widget for test\r\n    class DummyBW:\r\n        neuron_positions = {\"hunger\": (100, 100), \"happiness\": (200, 100)}\r\n        excluded_neurons = []\r\n        original_neuron_positions = {\"hunger\": (100, 100), \"happiness\": (200, 100)}\r\n        state = {\"hunger\": 60, \"happiness\": 40}\r\n        pruning_enabled = True\r\n        weights = {(\"hunger\", \"happiness\"): 0.75}\r\n        neurogenesis_data = {\r\n            \"novelty_counter\": 2.3,\r\n            \"stress_counter\": 0.4,\r\n            \"reward_counter\": 1.1,\r\n            \"new_neurons_details\": {\r\n                \"novelty_0\": {\"trigger_type\": \"novelty\", \"created_at\": time.time() - 120,\r\n                              \"specialisation\": \"object_investigation\", \"trigger_value_at_creation\": 3.2,\r\n                              \"associated_state_snapshot\": {\"curiosity\": 80}}\r\n            },\r\n            \"last_neuron_time\": time.time() - 300\r\n        }\r\n        neurogenesis_config = {\"novelty_threshold\": 3, \"stress_threshold\": 0.7, \"reward_threshold\": 0.6,\r\n                               \"max_neurons\": 32, \"cooldown\": 180}\r\n\r\n        def perform_hebbian_learning(self):\r\n            print(\"Hebbian cycle triggered\")\r\n            \r\n        def update(self):\r\n            # dummy update for slider/spinbox changes\r\n            pass\r\n\r\n    dlg = NeuronLaboratory(DummyBW())\r\n    dlg.show()\r\n\r\n    sys.exit(app.exec_())\r\n"
  },
  {
    "path": "src/learning.py",
    "content": "import random\r\nfrom PyQt5 import QtCore\r\nimport csv\r\nimport time\r\nimport json\r\nfrom datetime import datetime\r\nfrom .personality import Personality\r\n\r\nclass HebbianLearning:\r\n    def __init__(self, squid, brain_window, config=None):\r\n        self.squid = squid\r\n        self.brain_window = brain_window\r\n        self.config = config if config else LearningConfig()\r\n\r\n        \r\n        # Learning data and tracking\r\n        self.squid_personality = squid.personality if squid else None\r\n        self.learning_data = []\r\n        self.threshold = 0.7\r\n        self.goal_weights = {\r\n            'organize_decorations': 0.5,\r\n            'interact_with_rocks': 0.7,\r\n            'move_to_plants': 0.4\r\n        }\r\n\r\n        self.excluded_neurons = ['is_sick', 'is_eating', 'is_sleeping', 'pursuing_food', 'direction']\r\n\r\n        self.learning_rate = self.config.hebbian['base_learning_rate']\r\n        self.threshold = self.config.hebbian['threshold']\r\n        self.goal_weights = self.config.hebbian['goal_weights']\r\n        \r\n        # Neurogenesis tracking\r\n        self.last_neurogenesis_time = time.time()\r\n        self.neurogenesis_active = False\r\n\r\n        # Learning event logging\r\n        self.learning_event_log = []\r\n        self.learning_log_file = 'learning_events.json'\r\n\r\n        # Network state history\r\n        self.network_state_history = []\r\n        self.max_history_length = 100\r\n\r\n        # Personality-specific learning modifiers\r\n        self.personality_learning_modifiers = {\r\n            Personality.TIMID: {\r\n                'learning_rate_reduction': 0.5,\r\n                'novelty_sensitivity': 0.3,\r\n                'connection_stability': 0.8\r\n            },\r\n            Personality.ADVENTUROUS: {\r\n                'learning_rate_boost': 1.5,\r\n                'novelty_sensitivity': 1.2,\r\n                'connection_plasticity': 1.2\r\n            },\r\n            Personality.GREEDY: {\r\n                'reward_learning_boost': 1.3,\r\n                'exploration_penalty': 0.7,\r\n                'connection_prioritization': ['satisfaction', 'hunger']\r\n            },\r\n            Personality.STUBBORN: {\r\n                'unlearning_resistance': 0.9,\r\n                'new_connection_threshold': 0.6,\r\n                'preference_reinforcement': 1.2\r\n            }\r\n        }\r\n\r\n    def get_learning_data(self):\r\n        return self.learning_data\r\n    \r\n    def export_learning_data(self, file_name):\r\n        with open(file_name, 'w', newline='') as file:\r\n            writer = csv.writer(file)\r\n            writer.writerow([\"Timestamp\", \"Neuron 1\", \"Neuron 2\", \"Weight Change\", \"Goal Type\"])\r\n            writer.writerows(self.learning_data)\r\n\r\n    def learn_from_eating(self):\r\n        \"\"\"\r\n        Modify eating-related learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        self.strengthen_connection('hunger', 'satisfaction', self.learning_rate * 2.0)\r\n        self.strengthen_connection('hunger', 'happiness', self.learning_rate * 1.5)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.GREEDY:\r\n            # Greedy squids form stronger connections related to food and satisfaction\r\n            self.strengthen_connection('hunger', 'satisfaction', self.learning_rate * 3.0)\r\n            self.strengthen_connection('satisfaction', 'happiness', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids only strengthen connections for favorite food (sushi)\r\n            if getattr(self.squid, 'last_food_type', None) == 'sushi':\r\n                self.strengthen_connection('hunger', 'satisfaction', self.learning_rate * 2.5)\r\n        \r\n        elif personality == Personality.TIMID:\r\n            # Timid squids form weaker, more cautious connections\r\n            self.strengthen_connection('hunger', 'satisfaction', self.learning_rate * 1.5)\r\n            self.strengthen_connection('hunger', 'anxiety', self.learning_rate * 0.5)\r\n        \r\n        elif personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids form more varied and dynamic connections\r\n            self.strengthen_connection('hunger', 'curiosity', self.learning_rate * 1.8)\r\n            self.strengthen_connection('satisfaction', 'happiness', self.learning_rate * 2.0)\r\n\r\n    def learn_from_decoration_interaction(self, decoration_category):\r\n        \"\"\"\r\n        Modify decoration interaction learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        if decoration_category == 'plant':\r\n            self.strengthen_connection('curiosity', 'cleanliness', self.learning_rate)\r\n        elif decoration_category == 'rock':\r\n            self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 1.5)\r\n            self.strengthen_connection('curiosity', 'happiness', self.learning_rate)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids learn more from new decorations\r\n            if decoration_category == 'rock':\r\n                self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 2.5)\r\n                self.strengthen_connection('curiosity', 'happiness', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.TIMID:\r\n            # Timid squids are more cautious about new decorations\r\n            if decoration_category == 'rock':\r\n                self.strengthen_connection('curiosity', 'anxiety', self.learning_rate * 0.5)\r\n        \r\n        elif personality == Personality.GREEDY:\r\n            # Greedy squids focus on decorations that might provide rewards\r\n            if decoration_category == 'rock':\r\n                self.strengthen_connection('satisfaction', 'happiness', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids resist learning from new decorations\r\n            if decoration_category == 'rock':\r\n                self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 0.5)\r\n\r\n    def learn_from_organization(self):\r\n        \"\"\"\r\n        Modify organization-related learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 2)\r\n        self.strengthen_connection('cleanliness', 'satisfaction', self.learning_rate)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids learn more from organizing\r\n            self.strengthen_connection('curiosity', 'happiness', self.learning_rate * 2.5)\r\n        \r\n        elif personality == Personality.TIMID:\r\n            # Timid squids have a more cautious approach to organization\r\n            self.strengthen_connection('curiosity', 'anxiety', self.learning_rate * 0.5)\r\n        \r\n        elif personality == Personality.GREEDY:\r\n            # Greedy squids organize for potential rewards\r\n            self.strengthen_connection('satisfaction', 'happiness', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids resist changing their environment\r\n            self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 0.5)\r\n\r\n    def learn_from_sickness(self):\r\n        \"\"\"\r\n        Modify sickness-related learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        self.strengthen_connection('is_sick', 'cleanliness', self.learning_rate)\r\n        self.strengthen_connection('is_sick', 'anxiety', self.learning_rate)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.TIMID:\r\n            # Timid squids become more anxious when sick\r\n            self.strengthen_connection('is_sick', 'anxiety', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids learn to overcome sickness\r\n            self.strengthen_connection('is_sick', 'happiness', self.learning_rate * 1.5)\r\n        \r\n        elif personality == Personality.GREEDY:\r\n            # Greedy squids focus on recovering quickly\r\n            self.strengthen_connection('is_sick', 'satisfaction', self.learning_rate * 1.8)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids resist the impact of sickness\r\n            self.strengthen_connection('is_sick', 'anxiety', self.learning_rate * 0.5)\r\n\r\n    def update_personality(self, new_personality):\r\n        \"\"\"\r\n        Update the personality dynamically during learning\r\n        \r\n        Args:\r\n            new_personality (Personality): New personality type\r\n        \"\"\"\r\n        self.squid_personality = new_personality\r\n        \r\n        # Optional: Log personality change\r\n        self.log_learning_event({\r\n            'event_type': 'personality_change',\r\n            'old_personality': self.squid_personality,\r\n            'new_personality': new_personality\r\n        })\r\n\r\n    def learn_from_curiosity(self):\r\n        \"\"\"\r\n        Modify curiosity-related learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        self.strengthen_connection('curiosity', 'happiness', self.learning_rate)\r\n        self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids learn more from curiosity\r\n            self.strengthen_connection('curiosity', 'happiness', self.learning_rate * 2.5)\r\n            self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.TIMID:\r\n            # Timid squids have a more reserved curiosity\r\n            self.strengthen_connection('curiosity', 'anxiety', self.learning_rate * 0.5)\r\n        \r\n        elif personality == Personality.GREEDY:\r\n            # Greedy squids are curious about potential rewards\r\n            self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 2.0)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids resist learning from curiosity\r\n            self.strengthen_connection('curiosity', 'satisfaction', self.learning_rate * 0.5)\r\n\r\n    def learn_from_anxiety(self):\r\n        \"\"\"\r\n        Modify anxiety-related learning based on personality\r\n        \"\"\"\r\n        # Base learning connections\r\n        self.strengthen_connection('anxiety', 'is_sick', self.learning_rate)\r\n        self.strengthen_connection('anxiety', 'cleanliness', self.learning_rate)\r\n        \r\n        # Personality-specific modifications\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        if personality == Personality.TIMID:\r\n            # Timid squids are more affected by anxiety\r\n            self.strengthen_connection('anxiety', 'is_sick', self.learning_rate * 2.0)\r\n            self.strengthen_connection('anxiety', 'happiness', self.learning_rate * 0.5)\r\n        \r\n        elif personality == Personality.ADVENTUROUS:\r\n            # Adventurous squids learn to overcome anxiety\r\n            self.strengthen_connection('anxiety', 'happiness', self.learning_rate * 1.5)\r\n        \r\n        elif personality == Personality.GREEDY:\r\n            # Greedy squids link anxiety to potential loss\r\n            self.strengthen_connection('anxiety', 'satisfaction', self.learning_rate * 1.8)\r\n        \r\n        elif personality == Personality.STUBBORN:\r\n            # Stubborn squids resist the impact of anxiety\r\n            self.strengthen_connection('anxiety', 'is_sick', self.learning_rate * 0.5)\r\n\r\n    def strengthen_connection(self, neuron1, neuron2, base_learning_rate):\r\n        \"\"\"Enhanced version that considers neurogenesis state and goal weights\"\"\"\r\n        # Skip if either neuron is in the excluded list\r\n        if neuron1 in self.excluded_neurons or neuron2 in self.excluded_neurons:\r\n            return\r\n        \r\n        # Determine personality (with a default)\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        \r\n        # Apply personality modifiers\r\n        learning_rate = self.apply_personality_learning_modifiers(neuron1, neuron2, base_learning_rate)\r\n        \r\n        if getattr(self.squid, neuron1) > self.threshold and getattr(self.squid, neuron2) > self.threshold:\r\n            # Check if this is a goal-oriented connection\r\n            is_goal = (neuron1, neuron2) in self.goal_weights or (neuron2, neuron1) in self.goal_weights\r\n            \r\n            # Calculate weight change\r\n            if is_goal:\r\n                base_change = learning_rate * self.config.combined['goal_reinforcement_factor']\r\n            else:\r\n                base_change = learning_rate\r\n            \r\n            if self.neurogenesis_active:\r\n                base_change *= self.config.combined['neurogenesis_learning_boost']\r\n            \r\n            # Apply weight change\r\n            pair = (neuron1, neuron2)\r\n            reverse_pair = (neuron2, neuron1)\r\n            \r\n            prev_weight = self.brain_window.brain_widget.weights.get(pair, 0)\r\n            new_weight = prev_weight + base_change\r\n            \r\n            # Apply weight bounds\r\n            new_weight = max(self.config.hebbian['min_weight'], \r\n                            min(self.config.hebbian['max_weight'], new_weight))\r\n            \r\n            self.brain_window.brain_widget.weights[pair] = new_weight\r\n            self.brain_window.brain_widget.weights[reverse_pair] = new_weight\r\n            \r\n            # Prepare learning event details\r\n            learning_event = {\r\n                'neuron1': neuron1,\r\n                'neuron2': neuron2,\r\n                'learning_rate': learning_rate,\r\n                'weight_change': new_weight - prev_weight,\r\n                'previous_weight': prev_weight,\r\n                'new_weight': new_weight,\r\n                'personality': personality.value,\r\n                'is_goal_oriented': is_goal,\r\n                'neurogenesis_active': self.neurogenesis_active,\r\n                'explanation': self.generate_learning_explanation(neuron1, neuron2, learning_rate)\r\n            }\r\n            \r\n            # Log the learning event\r\n            self.log_learning_event(learning_event)\r\n            \r\n            # Periodically capture network state\r\n            if len(self.learning_event_log) % 10 == 0:\r\n                self.capture_network_state()\r\n\r\n    def apply_personality_learning_modifiers(self, neuron1, neuron2, learning_rate):\r\n        \"\"\"\r\n        Apply personality-specific modifiers to learning process\r\n        \"\"\"\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        modifiers = self.personality_learning_modifiers.get(personality, {})\r\n        \r\n        # Base learning rate modification\r\n        if personality == Personality.TIMID:\r\n            learning_rate *= modifiers.get('learning_rate_reduction', 1.0)\r\n        elif personality == Personality.ADVENTUROUS:\r\n            learning_rate *= modifiers.get('learning_rate_boost', 1.0)\r\n        \r\n        # Connection prioritization for greedy personality\r\n        if personality == Personality.GREEDY:\r\n            priority_neurons = modifiers.get('connection_prioritization', [])\r\n            if any(n in priority_neurons for n in [neuron1, neuron2]):\r\n                learning_rate *= 1.3\r\n        \r\n        # Stubborn personality resistance to change\r\n        if personality == Personality.STUBBORN:\r\n            learning_rate *= modifiers.get('unlearning_resistance', 1.0)\r\n        \r\n        return learning_rate\r\n    \r\n    def generate_learning_explanation(self, neuron1, neuron2, learning_rate):\r\n        \"\"\"\r\n        Generate a human-readable explanation of learning process\r\n        \"\"\"\r\n        personality = self.squid_personality or Personality.ADVENTUROUS\r\n        explanations = {\r\n            Personality.TIMID: f\"Cautiously adjusting connection between {neuron1} and {neuron2}\",\r\n            Personality.ADVENTUROUS: f\"Rapidly strengthening connection between {neuron1} and {neuron2}\",\r\n            Personality.GREEDY: f\"Prioritizing connection between {neuron1} and {neuron2} for potential reward\",\r\n            Personality.STUBBORN: f\"Maintaining existing connection pattern between {neuron1} and {neuron2}\"\r\n        }\r\n        \r\n        return explanations.get(personality, \"Neutral learning process\")\r\n\r\n    def log_learning_event(self, event_details):\r\n        \"\"\"\r\n        Log a detailed learning event with comprehensive details\r\n        \r\n        Args:\r\n            event_details (dict): Dictionary containing learning event information\r\n        \"\"\"\r\n        try:\r\n            # Ensure timestamp is added\r\n            event_details['timestamp'] = datetime.now().isoformat()\r\n            \r\n            # Add context information if not present\r\n            if 'personality' not in event_details and self.squid_personality:\r\n                event_details['personality'] = self.squid_personality.value\r\n            \r\n            # Validate and sanitize event details\r\n            sanitized_event = {\r\n                k: v for k, v in event_details.items() \r\n                if v is not None and v != ''\r\n            }\r\n            \r\n            # Add to in-memory log\r\n            self.learning_event_log.append(sanitized_event)\r\n            \r\n            # Optionally save to file (every 10 events or based on a condition)\r\n            if len(self.learning_event_log) % 10 == 0:\r\n                self.save_learning_log()\r\n            \r\n            # Optional: print event for debugging (can be removed in production)\r\n            if self.debug_mode:\r\n                print(f\"Learning Event: {sanitized_event}\")\r\n            \r\n        except Exception as e:\r\n            print(f\"Error logging learning event: {e}\")\r\n            # Optionally log to a separate error log\r\n    \r\n    def save_learning_log(self):\r\n        \"\"\"Save learning events to a JSON file\"\"\"\r\n        try:\r\n            with open(self.learning_log_file, 'w') as f:\r\n                json.dump(self.learning_event_log, f, indent=2)\r\n        except Exception as e:\r\n            print(f\"Error saving learning log: {e}\")\r\n    \r\n    def export_learning_log(self, filename=None):\r\n        \"\"\"\r\n        Export learning log to CSV or specified format\r\n        \"\"\"\r\n        if not filename:\r\n            filename = f\"learning_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv\"\r\n        \r\n        try:\r\n            with open(filename, 'w', newline='') as csvfile:\r\n                # Determine fieldnames dynamically based on first event\r\n                if self.learning_event_log:\r\n                    fieldnames = list(self.learning_event_log[0].keys())\r\n                    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\r\n                    \r\n                    writer.writeheader()\r\n                    for event in self.learning_event_log:\r\n                        writer.writerow(event)\r\n            \r\n            print(f\"Learning log exported to {filename}\")\r\n        except Exception as e:\r\n            print(f\"Error exporting learning log: {e}\")\r\n\r\n    def capture_network_state(self):\r\n        \"\"\"\r\n        Capture the current state of the neural network\r\n        \"\"\"\r\n        current_state = {\r\n            'timestamp': datetime.now().isoformat(),\r\n            'neurons': list(self.brain_window.brain_widget.neuron_positions.keys()),\r\n            'weights': {str(k): v for k, v in self.brain_window.brain_widget.weights.items()},\r\n            'neuron_positions': {\r\n                str(name): list(pos) \r\n                for name, pos in self.brain_window.brain_widget.neuron_positions.items()\r\n            },\r\n            'personality': self.squid.personality.value,\r\n            'learning_rate': self.learning_rate\r\n        }\r\n        \r\n        # Add to history\r\n        self.network_state_history.append(current_state)\r\n        \r\n        # Trim history if it exceeds max length\r\n        if len(self.network_state_history) > self.max_history_length:\r\n            self.network_state_history.pop(0)\r\n        \r\n        return current_state\r\n    \r\n    def analyze_network_evolution(self):\r\n        \"\"\"\r\n        Analyze how the network has evolved over time\r\n        \"\"\"\r\n        if len(self.network_state_history) < 2:\r\n            return {\"error\": \"Not enough history to analyze\"}\r\n        \r\n        analysis = {\r\n            'total_neurons_added': 0,\r\n            'total_weight_changes': 0,\r\n            'personality_impact': {},\r\n            'learning_rate_trend': []\r\n        }\r\n        \r\n        # Analyze changes between consecutive states\r\n        for i in range(1, len(self.network_state_history)):\r\n            prev_state = self.network_state_history[i-1]\r\n            current_state = self.network_state_history[i]\r\n            \r\n            # Count new neurons\r\n            new_neurons = set(current_state['neurons']) - set(prev_state['neurons'])\r\n            analysis['total_neurons_added'] += len(new_neurons)\r\n            \r\n            # Track weight changes\r\n            weight_changes = 0\r\n            for (k, v) in current_state['weights'].items():\r\n                if k in prev_state['weights']:\r\n                    if abs(v - prev_state['weights'][k]) > 0.01:\r\n                        weight_changes += 1\r\n            \r\n            analysis['total_weight_changes'] += weight_changes\r\n            \r\n            # Personality impact tracking\r\n            personality = current_state['personality']\r\n            analysis['personality_impact'][personality] = analysis['personality_impact'].get(personality, 0) + 1\r\n            \r\n            # Learning rate trend\r\n            analysis['learning_rate_trend'].append(current_state['learning_rate'])\r\n        \r\n        return analysis\r\n    \r\n    def export_network_evolution(self, filename=None):\r\n        \"\"\"\r\n        Export network evolution history\r\n        \"\"\"\r\n        if not filename:\r\n            filename = f\"network_evolution_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\r\n        \r\n        try:\r\n            with open(filename, 'w') as f:\r\n                json.dump(self.network_state_history, f, indent=2)\r\n            \r\n            print(f\"Network evolution history exported to {filename}\")\r\n        except Exception as e:\r\n            print(f\"Error exporting network evolution: {e}\")\r\n\r\n    def update_learning_rate(self, novelty_factor):\r\n        \"\"\"Update learning rate based on novelty and neurogenesis state\"\"\"\r\n        base_rate = self.config.hebbian['base_learning_rate']\r\n        if self.neurogenesis_active:\r\n            self.learning_rate = base_rate * novelty_factor * self.config.combined['neurogenesis_learning_boost']\r\n        else:\r\n            self.learning_rate = base_rate * novelty_factor\r\n            \r\n        # Update goal weights with the same factor\r\n        for goal in self.goal_weights:\r\n            self.goal_weights[goal] = min(1.0, self.config.hebbian['goal_weights'][goal] * novelty_factor)\r\n\r\n    def update_weights(self):\r\n        # Apply goal-oriented reinforcement\r\n        if self.squid.status == \"organizing decorations\":\r\n            self.learn_from_organization()\r\n        elif self.squid.status == \"interacting with rocks\":\r\n            self.learn_from_decoration_interaction('rock')\r\n        elif self.squid.status == \"moving to plant\":\r\n            self.learn_from_decoration_interaction('plant')\r\n\r\n    def check_neurogenesis_conditions(self, brain_state):\r\n        \"\"\"Check if conditions for neurogenesis are met\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # Check cooldown first\r\n        if current_time - self.last_neurogenesis_time < self.config.neurogenesis['cooldown']:\r\n            return False\r\n            \r\n        # Check triggers\r\n        triggers = {\r\n            'novelty': brain_state.get('novelty_exposure', 0) > self.config.neurogenesis['novelty_threshold'],\r\n            'stress': brain_state.get('sustained_stress', 0) > self.config.neurogenesis['stress_threshold'],\r\n            'reward': brain_state.get('recent_rewards', 0) > self.config.neurogenesis['reward_threshold']\r\n        }\r\n        \r\n        return any(triggers.values())\r\n\r\n    def create_new_neuron(self, neuron_type, trigger_data):\r\n        \"\"\"Create a new neuron and connect it to existing ones\"\"\"\r\n        base_name = {\r\n            'novelty': 'novel',\r\n            'stress': 'defense',\r\n            'reward': 'reward'\r\n        }.get(neuron_type, 'new')\r\n        \r\n        new_name = f\"{base_name}_{len(self.brain_window.brain_widget.neurogenesis_data['new_neurons'])}\"\r\n        \r\n        # Add to brain widget\r\n        self.brain_window.brain_widget.create_neuron(neuron_type, trigger_data)\r\n        \r\n        # Initialize connections with existing neurons\r\n        for existing_neuron in self.brain_window.brain_widget.neuron_positions:\r\n            if existing_neuron != new_name:\r\n                # Stronger initial connection if related\r\n                if (neuron_type == 'novelty' and existing_neuron == 'curiosity') or \\\r\n                   (neuron_type == 'stress' and existing_neuron == 'anxiety') or \\\r\n                   (neuron_type == 'reward' and existing_neuron == 'satisfaction'):\r\n                    weight = self.config.combined['new_neuron_connection_strength'] * 1.5\r\n                else:\r\n                    weight = self.config.combined['new_neuron_connection_strength']\r\n                \r\n                self.brain_window.brain_widget.weights[(new_name, existing_neuron)] = weight\r\n                self.brain_window.brain_widget.weights[(existing_neuron, new_name)] = weight * 0.5\r\n        \r\n        # Activate neurogenesis boost\r\n        self.neurogenesis_active = True\r\n        self.last_neurogenesis_time = time.time()\r\n        QtCore.QTimer.singleShot(10000, self.end_neurogenesis_boost)  # 10 second boost\r\n        \r\n        return new_name\r\n    \r\n    def end_neurogenesis_boost(self):\r\n        self.neurogenesis_active = False\r\n\r\nclass LearningConfig:\r\n    def __init__(self):\r\n        # Initialize default values\r\n        self.hebbian = {\r\n            'base_learning_rate': 0.1,\r\n            'threshold': 0.7,\r\n            'weight_decay': 0.01,\r\n            'max_weight': 1.0,\r\n            'min_weight': -1.0,\r\n            'learning_interval': 30000,\r\n            'goal_weights': {\r\n                'organize_decorations': 0.5,\r\n                'interact_with_rocks': 0.7,\r\n                'move_to_plants': 0.4\r\n            }\r\n        }\r\n        \r\n        # Initialize neurogenesis with values matching config.ini\r\n        self.neurogenesis = {\r\n            'novelty_threshold': 2.5,  # Match config.ini value\r\n            'stress_threshold': 2.0,   # Match config.ini value\r\n            'reward_threshold': 1.8,   # Match config.ini value\r\n            'cooldown': 120,           # Match config.ini value\r\n            'decay_rate': 0.95,        # Match config.ini value\r\n            'new_neuron_initial_weight': 0.5,\r\n            'max_new_neurons': 5\r\n        }\r\n        \r\n        # Try loading from config file if available\r\n        self.load_from_config()\r\n        \r\n    def load_from_config(self):\r\n        \"\"\"Load values from config.ini if available\"\"\"\r\n        try:\r\n            from .config_manager import ConfigManager\r\n            config_manager = ConfigManager()\r\n            \r\n            # Get neurogenesis configuration\r\n            neuro_config = config_manager.get_neurogenesis_config()\r\n            \r\n            # Update our neurogenesis config with values from file\r\n            if neuro_config:\r\n                # Update the thresholds with values from config.ini\r\n                self.neurogenesis['novelty_threshold'] = neuro_config['triggers']['novelty']['threshold']\r\n                self.neurogenesis['stress_threshold'] = neuro_config['triggers']['stress']['threshold']\r\n                self.neurogenesis['reward_threshold'] = neuro_config['triggers']['reward']['threshold']\r\n                self.neurogenesis['cooldown'] = neuro_config['general']['cooldown']\r\n                self.neurogenesis['decay_rate'] = neuro_config['triggers']['novelty']['decay_rate']\r\n            \r\n            print(\"Configuration loaded from config.ini\")\r\n        except Exception as e:\r\n            print(f\"Error loading from config.ini: {e}\")\r\n            print(\"Using default configuration values\")"
  },
  {
    "path": "src/localisation.py",
    "content": "# localisation.py\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\n\r\nclass Localisation:\r\n    _instance = None\r\n    \r\n    @classmethod\r\n    def instance(cls):\r\n        \"\"\"Return the singleton instance\"\"\"\r\n        if cls._instance is None:\r\n            cls._instance = cls()\r\n        return cls._instance\r\n    \r\n    def __init__(self):\r\n        \"\"\"Initialize with default language\"\"\"\r\n        self.translations = {}\r\n        self.current_language = 'en'\r\n        self.load_language('en')\r\n    \r\n    def load_language(self, lang_code):\r\n        \"\"\"Load translations from language module\"\"\"\r\n        # Attempt 1: Try import from src.translations (Standard Project Structure)\r\n        try:\r\n            module_name = f'src.translations.{lang_code}'\r\n            module = __import__(module_name, fromlist=['translations'])\r\n            self.translations = getattr(module, 'translations', {})\r\n            self.current_language = lang_code\r\n            print(f\"[Localisation] Successfully loaded '{lang_code}' from {module_name}\")\r\n            return\r\n        except ImportError:\r\n            pass\r\n\r\n        # Attempt 2: Try import from translations (Flat/Root Structure)\r\n        try:\r\n            module_name = f'translations.{lang_code}'\r\n            module = __import__(module_name, fromlist=['translations'])\r\n            self.translations = getattr(module, 'translations', {})\r\n            self.current_language = lang_code\r\n            print(f\"[Localisation] Successfully loaded '{lang_code}' from {module_name}\")\r\n            return\r\n        except ImportError as e:\r\n            # Fallback: No translations found\r\n            print(f\"[Localisation] WARNING: Could not load translations for '{lang_code}'. Error: {e}\")\r\n            self.translations = {}\r\n    \r\n    def get(self, key, default=None, **kwargs):\r\n        \"\"\"\r\n        Get a translation string with optional formatting.\r\n        \r\n        Args:\r\n            key: Translation key to look up\r\n            default: Default value if key not found\r\n            **kwargs: Formatting arguments for the translation string\r\n        \r\n        Returns:\r\n            Translated and formatted string\r\n        \"\"\"\r\n        if not key:\r\n            return default or \"\"\r\n        \r\n        # Look up the translation\r\n        value = self.translations.get(key, default)\r\n        \r\n        # If no value found, use the key itself as default\r\n        if value is None:\r\n            value = str(key)\r\n        \r\n        # Format with kwargs if provided\r\n        if kwargs and isinstance(value, str):\r\n            try:\r\n                return value.format(**kwargs)\r\n            except (KeyError, ValueError):\r\n                # Return unformatted string if formatting fails\r\n                return value\r\n        \r\n        return value\r\n    \r\n    def set_language(self, lang_code):\r\n        \"\"\"Change the current language\"\"\"\r\n        self.load_language(lang_code)\r\n    \r\n    def get_available_languages(self):\r\n        \"\"\"Scan the translations folder and return sorted list of language codes\"\"\"\r\n        languages = set()\r\n        \r\n        # Scan root/translations directory\r\n        try:\r\n            # Go up one level from src/ to root/, then into translations/\r\n            root_dir = Path(__file__).parent.parent\r\n            trans_dir = root_dir / \"translations\"\r\n            \r\n            if trans_dir.exists():\r\n                for file_path in trans_dir.glob(\"*.py\"):\r\n                    lang_code = file_path.stem.lower()\r\n                    if lang_code not in ('__init__',) and not lang_code.startswith('_'):\r\n                        languages.add(lang_code)\r\n        except Exception as e:\r\n            print(f\"[Localisation] Error scanning translations directory: {e}\")\r\n        \r\n        return sorted(list(languages))\r\n\r\n    def get_language_name(self, lang_code, fallback=True):\r\n        \"\"\"Get the display name for a language code\"\"\"\r\n        if not lang_code:\r\n            return \"Unknown\"\r\n        \r\n        # Try to get native name from translation file\r\n        try:\r\n            original_lang = self.current_language\r\n            self.load_language(lang_code)\r\n            name = self.translations.get('language_name_native') or \\\r\n                   self.translations.get('language_name')\r\n            self.load_language(original_lang)\r\n            \r\n            if name:\r\n                return name\r\n        except Exception as e:\r\n            print(f\"[Localisation] Error getting name for '{lang_code}': {e}\")\r\n        \r\n        # Fallback to hardcoded map\r\n        if fallback:\r\n            fallback_names = {\r\n                'en': 'English', 'es': 'Spanish (Español)', 'fr': 'French (Français)',\r\n                'de': 'German (Deutsch)', 'pl': 'Polish (Polski)', 'uk': 'Ukrainian (Українська)',\r\n                'zh': 'Chinese (中文)', 'ja': 'Japanese (日本語)', 'pt': 'Portuguese (Português)',\r\n                'cy': 'Welsh (Cymraeg)', 'ga': 'Irish (Gaeilge)', 'gen_z': 'Gen Z Slang',\r\n            }\r\n            return fallback_names.get(lang_code, lang_code.upper())\r\n        \r\n        return lang_code\r\n    \r\n    # =====================================================================\r\n    # PERSONALITY TRANSLATION METHODS\r\n    # =====================================================================\r\n    \r\n    def get_personality_name(self, personality):\r\n        \"\"\"Get translated personality display name.\"\"\"\r\n        key = personality.value if hasattr(personality, 'value') else str(personality).lower()\r\n        return self.get(f'personality_{key}', key.capitalize())\r\n    \r\n    def get_personality_description(self, personality):\r\n        \"\"\"Get translated personality description text.\"\"\"\r\n        key = personality.value if hasattr(personality, 'value') else str(personality).lower()\r\n        return self.get(f'desc_{key}', '')\r\n    \r\n    def get_personality_modifier_text(self, personality):\r\n        \"\"\"Get translated personality modifier summary.\"\"\"\r\n        key = personality.value if hasattr(personality, 'value') else str(personality).lower()\r\n        return self.get(f'mod_{key}', 'Balanced')\r\n    \r\n    def get_personality_modifiers(self, personality):\r\n        \"\"\"Get translated detailed personality modifiers.\"\"\"\r\n        key = personality.value if hasattr(personality, 'value') else str(personality).lower()\r\n        return self.get(f'modifiers_{key}', '')\r\n    \r\n    def get_care_tips(self, personality):\r\n        \"\"\"Get translated care tips for personality.\"\"\"\r\n        key = personality.value if hasattr(personality, 'value') else str(personality).lower()\r\n        return self.get(f'tips_{key}', '')\r\n\r\n\r\n# Robust alias for direct access to the get method\r\ndef loc(key, default=None, **kwargs):\r\n    \"\"\"Convenient alias for Localisation.instance().get()\"\"\"\r\n    return Localisation.instance().get(key, default, **kwargs)\r\n\r\ndef set_language(lang_code):\r\n    \"\"\"\r\n    Set the global language for the Localisation singleton.\r\n    Args:\r\n        lang_code: Language code (e.g., 'en', 'zh')\r\n    \"\"\"\r\n    Localisation.instance().set_language(lang_code)\r\n\r\n# Legacy alias for American spelling compatibility\r\nLocalization = Localisation"
  },
  {
    "path": "src/main.py",
    "content": "# Dosidicus - a digital pet with a neural network\n# main.py Entrypoint \nfrom PyQt5 import QtWidgets, QtCore, QtGui\nimport sys\nimport os\n\n# BOOTSTRAP: Add the current directory and src directory to sys.path\n# This ensures \"from src.ui import Ui\" works even if launched from elsewhere.\nBASE_DIR = os.path.dirname(os.path.abspath(__file__))\nif BASE_DIR not in sys.path:\n    sys.path.insert(0, BASE_DIR)\n\nimport time\nimport sys\nimport json\nimport os\nimport shutil\nimport traceback\nimport multiprocessing\nimport logging\nfrom PyQt5 import QtWidgets, QtCore\nimport random\nimport argparse\nfrom src.ui import Ui\nfrom src.tamagotchi_logic import TamagotchiLogic\nfrom src.squid import Squid, Personality\nfrom src.splash_screen import SplashScreen\nfrom src.save_manager import SaveManager\nfrom src.brain_tool import SquidBrainWindow\nfrom src.learning import LearningConfig\nfrom src.plugin_manager import PluginManager\nfrom src.brain_worker import BrainWorker\nfrom src.config_manager import ConfigManager\nfrom src.localisation import Localisation\n\ndef launch_brain_designer_process():\n    \"\"\"Entry point for Brain Designer in a separate process\"\"\"\n    import sys\n    from PyQt5.QtWidgets import QApplication\n    from src.brain_designer import BrainDesignerWindow\n    \n    app = QApplication(sys.argv)\n    window = BrainDesignerWindow()\n    window.show()\n    sys.exit(app.exec_())\n\ndef setup_logging_configuration():\n    \"\"\"Initialize logging configuration\"\"\"\n    os.environ['QT_LOGGING_RULES'] = '*.debug=false;qt.qpa.*=false;qt.style.*=false'\n    os.makedirs('logs', exist_ok=True)\n\n    logging.basicConfig(\n        filename='logs/dosidicus_log.txt',\n        level=logging.ERROR,\n        format='%(asctime)s - %(levelname)s - %(message)s'\n    )\n\ndef perform_cleanup_and_exit():\n    \"\"\"Recursively delete __pycache__ and logs directories.\"\"\"\n    print(\"🧹 Cleaning environment...\")\n    root_dir = os.path.dirname(os.path.abspath(__file__))\n    \n    deleted_count = 0\n    \n    for root, dirs, files in os.walk(root_dir, topdown=True):\n        # Filter and remove specific directories\n        # We iterate over a copy of dirs so we can modify the original list safely\n        for name in list(dirs):\n            if name in ['__pycache__', 'logs']:\n                path = os.path.join(root, name)\n                try:\n                    shutil.rmtree(path)\n                    print(f\"   Deleted: {path}\")\n                    dirs.remove(name)  # Prevent os.walk from trying to enter this dir\n                    deleted_count += 1\n                except Exception as e:\n                    print(f\"   ❌ Failed to delete {path}: {e}\")\n                    \n    print(f\"✨ Cleanup complete. Removed {deleted_count} directories.\")\n\ndef global_exception_handler(exctype, value, tb):\n    \"\"\"Global exception handler to log unhandled exceptions\"\"\"\n    error_message = ''.join(traceback.format_exception(exctype, value, tb))\n    logging.error(\"Unhandled exception:\\n%s\", error_message)\n    QtWidgets.QMessageBox.critical(None, \"Error\", \n                                 \"An unexpected error occurred. Please check dosidicus_log.txt for details.\")\n\n\nclass TeeStream:\n    \"\"\"Duplicate output to both console and file\"\"\"\n    def __init__(self, original_stream, file_stream):\n        self.original_stream = original_stream\n        self.file_stream = file_stream\n\n    def write(self, data):\n        self.original_stream.write(data)\n        self.file_stream.write(data)\n        self.file_stream.flush()\n\n    def flush(self):\n        self.original_stream.flush()\n        self.file_stream.flush()\n\nclass TimedMessageBox(QtWidgets.QDialog):\n    \"\"\"A message box that auto-closes after a timeout with a default choice\"\"\"\n    def __init__(self, parent, title, message, timeout_seconds=5):\n        super().__init__(parent)\n        self.setWindowTitle(title)\n        self.timeout_seconds = timeout_seconds\n        self.remaining_seconds = timeout_seconds\n        self.result_value = QtWidgets.QMessageBox.No  # Default to No\n        self.loc = Localisation.instance()\n        \n        # Setup UI\n        layout = QtWidgets.QVBoxLayout()\n        \n        self.message_label = QtWidgets.QLabel(message)\n        self.message_label.setStyleSheet(\"font-size: 15px;\")\n        layout.addWidget(self.message_label)\n        \n        # Auto-decline message\n        self.timer_label = QtWidgets.QLabel(self.loc.get(\"auto_decline\", seconds=self.remaining_seconds))\n        self.timer_label.setStyleSheet(\"color: gray; font-size: 14px;\")\n        layout.addWidget(self.timer_label)\n        \n        # Buttons\n        self.button_box = QtWidgets.QDialogButtonBox(\n            QtWidgets.QDialogButtonBox.Yes | QtWidgets.QDialogButtonBox.No\n        )\n        # Localise buttons manually since we use a custom dict system\n        self.button_box.button(QtWidgets.QDialogButtonBox.Yes).setText(self.loc.get(\"yes\"))\n        self.button_box.button(QtWidgets.QDialogButtonBox.No).setText(self.loc.get(\"no\"))\n\n        self.button_box.accepted.connect(self.accept_yes)\n        self.button_box.rejected.connect(self.reject_no)\n        layout.addWidget(self.button_box)\n        \n        self.setLayout(layout)\n        \n        # Setup timer\n        self.timer = QtCore.QTimer(self)\n        self.timer.timeout.connect(self.update_countdown)\n        self.timer.start(1000)  # Update every second\n\n\n        \n    def update_countdown(self):\n        \"\"\"Update the countdown and auto-close when time runs out\"\"\"\n        self.remaining_seconds -= 1\n        self.timer_label.setText(self.loc.get(\"auto_decline\", seconds=self.remaining_seconds))\n        \n        if self.remaining_seconds <= 0:\n            self.timer.stop()\n            self.reject_no()  # Auto-close with No\n    \n    def accept_yes(self):\n        \"\"\"User clicked Yes\"\"\"\n        self.timer.stop()\n        self.result_value = QtWidgets.QMessageBox.Yes\n        self.accept()\n    \n    def reject_no(self):\n        \"\"\"User clicked No or timeout occurred\"\"\"\n        self.timer.stop()\n        self.result_value = QtWidgets.QMessageBox.No\n        self.reject()\n    \n    def get_result(self):\n        \"\"\"Get the result after dialog closes\"\"\"\n        return self.result_value\n    \n\n\nclass MainWindow(QtWidgets.QMainWindow):\n    def __init__(self, specified_personality=None, debug_mode=False, neuro_cooldown=None):\n        super().__init__()\n        \n        # Apply configured language from config.ini\n        config_manager = ConfigManager()\n        language = config_manager.get_language()\n        Localisation.instance().set_language(language)\n        print(f\"📄 Applied language from config: {language}\")\n        \n        # Initialize configuration\n        self.config = LearningConfig()\n        if neuro_cooldown is not None:\n            self.config.neurogenesis['cooldown'] = neuro_cooldown\n        \n        # Add initialization tracking flag\n        self._initialization_complete = False\n        \n        # Set up debugging\n        self.debug_mode = debug_mode\n        if self.debug_mode:\n            self.setup_logging()\n        \n        # Initialize UI first\n        logging.debug(\"Initializing UI\")\n        self.user_interface = Ui(self, debug_mode=self.debug_mode)\n\n        # Initialize SquidBrainWindow with config\n        logging.debug(\"Initializing SquidBrainWindow\")\n        self.brain_window = SquidBrainWindow(None, self.debug_mode, self.config)\n        \n        # Store the original window reference to prevent garbage collection\n        self._brain_window_ref = self.brain_window\n        \n        # Explicitly force creation of all tab contents\n        QtCore.QTimer.singleShot(100, self.preload_brain_window_tabs)\n        \n        # Continue with normal initialization\n        self.brain_window.set_tamagotchi_logic(None)  # Placeholder to ensure initialization\n        self.user_interface.squid_brain_window = self.brain_window\n        \n        # Initialize plugin manager after UI and brain window\n        logging.debug(\"Initializing PluginManager\")\n        self.plugin_manager = PluginManager()\n        print(f\"> Plugin manager initialized: {self.plugin_manager}\")\n        \n        self.specified_personality = specified_personality\n        self.neuro_cooldown = neuro_cooldown\n        self.squid = None\n        \n        # Check for existing save data\n        self.save_manager = SaveManager(\"saves\")\n        \n        # Track whether we want to show tutorial\n        self.show_tutorial = False\n\n        # ===== PERFORMANCE FIX: Single BrainWorker managed by brain_tool =====\n        # Don't create another worker here - SquidBrainWindow creates and shares it\n        # Access via self.brain_window.brain_worker if needed\n        self.brain_worker = None\n        print(\"ℹ️ BrainWorker managed by SquidBrainWindow\")\n        \n        # Initialize the game\n        logging.debug(\"Initializing game\")\n        self.initialize_game()\n        \n        # Now that tamagotchi_logic is created, set it in plugin_manager and brain_window\n        logging.debug(\"Setting tamagotchi_logic references\")\n        self.plugin_manager.tamagotchi_logic = self.tamagotchi_logic\n        self.tamagotchi_logic.plugin_manager = self.plugin_manager\n        self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n        # New in 2.4.5.0 : Create a unique personality starter neuron\n        squid = self.tamagotchi_logic.squid\n        brain_widget = self.brain_window.brain_widget\n        if (squid and squid.personality and\n            brain_widget and hasattr(brain_widget, 'enhanced_neurogenesis')):\n\n            if not squid._has_personality_starter_neuron():\n                neuron = brain_widget.enhanced_neurogenesis.create_personality_starter_neuron(\n                    squid.personality.value,\n                    brain_widget.state\n                )\n                if neuron:\n                    print(f\"🧬 Personality starter neuron created: {neuron}\")\n        \n        # Load and initialize plugins after core components\n        logging.debug(\"Loading plugins\")\n        plugin_results = self.plugin_manager.load_all_plugins()\n        \n        # Setup plugins with tamagotchi_logic reference\n        for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n            instance = plugin_data.get('instance')\n            if instance and hasattr(instance, 'setup') and not plugin_data.get('is_setup', False):\n                try:\n                    instance.setup(self.plugin_manager, self.tamagotchi_logic)\n                    plugin_data['is_setup'] = True\n                except Exception as e:\n                    print(f\"Error setting up plugin {plugin_name}: {e}\")\n        \n        # CRITICAL FIX: Re-load achievement data since plugin instances were replaced\n        # during load_all_plugins(), discarding any data loaded earlier\n        if self.save_manager.save_exists():\n            save_data = self.save_manager.load_game()\n            if save_data and 'achievements' in save_data:\n                self._restore_achievements_data(save_data['achievements'])\n        \n        # Update status bar with plugin information\n        if hasattr(self.user_interface, 'status_bar'):\n            self.user_interface.status_bar.update_plugins_status(self.plugin_manager)\n        \n        # Connect signals\n        self.user_interface.new_game_action.triggered.connect(self.start_new_game)\n        self.user_interface.load_action.triggered.connect(self.load_game)\n        self.user_interface.save_action.triggered.connect(self.save_game)\n        self.user_interface.decorations_action.triggered.connect(self.user_interface.toggle_decoration_window)\n        \n        # Initialize plugin menu - do this AFTER loading plugins\n        self.user_interface.apply_plugin_menu_registrations(self.plugin_manager)\n    \n        # Position window 300 pixels to the left of default position\n        desktop = QtWidgets.QApplication.desktop()\n        screen_rect = desktop.screenGeometry()\n        window_rect = self.geometry()\n        center_x = screen_rect.center().x()\n        window_x = center_x - (window_rect.width() // 2)  # Default centered X position\n        \n        # Move 300 pixels to the left\n        self.move(window_x - 300, self.y())\n        \n        if self.debug_mode:\n            print(f\"DEBUG MODE ENABLED: Console output is being logged to console.txt\")\n\n        self.setup_facts_timer()\n\n    def preload_brain_window_tabs(self):\n        \"\"\"Force creation of all tab contents to prevent crashes during tutorial\"\"\"\n        print(\"Pre-loading brain window tabs...\")\n        if not hasattr(self, 'brain_window') or not self.brain_window:\n            print(\"⚠️  Brain window not initialized, cannot preload\")\n            return\n            \n        try:\n            # Force the window to process events and initialize all tabs\n            if hasattr(self.brain_window, 'tabs'):\n                # Visit each tab to ensure it's loaded\n                tab_count = self.brain_window.tabs.count()\n                \n                # Initialize tabs array to prevent garbage collection\n                if not hasattr(self, '_preloaded_tabs'):\n                    self._preloaded_tabs = []\n                \n                # Remember if window was visible before we started\n                was_visible = self.brain_window.isVisible()\n                #print(f\"📋 Brain window was_visible before preload: {was_visible}\")\n                    \n                # Temporarily show the window off-screen to force loading\n                original_pos = self.brain_window.pos()\n                self.brain_window.move(-10000, -10000)  # Move off-screen\n                self.brain_window.show()\n                \n                # Force each tab to be displayed at least once\n                for i in range(tab_count):\n                    self.brain_window.tabs.setCurrentIndex(i)\n                    QtWidgets.QApplication.processEvents()\n                    \n                    # Get and store references to tab widgets\n                    widget = self.brain_window.tabs.widget(i)\n                    if widget:\n                        self._preloaded_tabs.append(widget)\n                        #print(f\"  ✓ Preloaded tab {i}: {self.brain_window.tabs.tabText(i)}\")\n                \n                # Return to first tab (Network/Brain tab)\n                self.brain_window.tabs.setCurrentIndex(0)\n                QtWidgets.QApplication.processEvents()\n                #print(f\"📋 Reset to first tab: {self.brain_window.tabs.tabText(0)}\")\n                \n                # Restore original position\n                self.brain_window.move(original_pos)\n                \n                # Only hide if it wasn't visible before (don't hide if user is viewing it)\n                if not was_visible:\n                    self.brain_window.hide()\n                    print(\"📋 Brain window hidden after preload (was not visible before)\")\n                else:\n                    print(\"📋 Brain window kept visible after preload (was visible before)\")\n                \n                print(f\"✅ Successfully preloaded {len(self._preloaded_tabs)} tabs\")\n        except Exception as e:\n            print(f\"❌ Error preloading tabs: {e}\")\n            import traceback\n            traceback.print_exc()\n\n    def setup_logging(self):\n        \"\"\"Set up console logging to file\"\"\"\n        if not hasattr(sys, '_original_stdout'):\n            sys._original_stdout = sys.stdout\n            sys._original_stderr = sys.stderr\n            \n        console_log = open('console.txt', 'w', encoding='utf-8')\n        sys.stdout = TeeStream(sys._original_stdout, console_log)\n        sys.stderr = TeeStream(sys._original_stderr, console_log)\n\n    def setup_facts_timer(self):\n        \"\"\"Rare ocean-blue Humboldt squid facts every 5 minutes\"\"\"\n        config_manager = ConfigManager()\n        if not config_manager.get_facts_enabled():\n            return\n        self.fact_timer = QtCore.QTimer(self)\n        self.fact_timer.timeout.connect(self.show_random_squid_fact)\n        self.fact_timer.start(config_manager.get_fact_interval_ms())\n\n    def show_random_squid_fact(self):\n        \"\"\"Show one short Humboldt squid fact – big, bright blue, always visible\"\"\"\n        try:\n            from src.squid_facts import get_random_fact\n            fact = get_random_fact()\n            if not fact:\n                return\n\n            msg = f\"🌊 Humboldt Fact: {fact}\"\n\n            # Preferred: status bar (works everywhere, supports color)\n            if hasattr(self.user_interface, 'status_bar') and self.user_interface.status_bar:\n                colored_msg = f'<span style=\"color:#00BFFF; font-weight:bold; font-size:14px;\">{msg}</span>'\n                self.user_interface.status_bar.showMessage(colored_msg, 8000)  # 8 seconds\n            else:\n                # Fallback: plain message\n                self.user_interface.show_message(msg)\n\n            print(f\"[Facts] DISPLAYED: {fact[:80]}...\")  # helpful console confirmation\n\n        except Exception as e:\n            print(f\"[Facts] Error showing fact: {e}\")\n\n    def initialize_game(self):\n        if hasattr(self.save_manager, 'cleanup_duplicate_saves'):\n            self.save_manager.cleanup_duplicate_saves()\n        \n        if self.save_manager.save_exists() and self.specified_personality is None:\n            print(\"\\x1b[32mExisting save data found and will be loaded\\x1b[0m\")\n            self.squid = Squid(self.user_interface, None, None)\n            self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n\n            self.squid.tamagotchi_logic = self.tamagotchi_logic\n            self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n            self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n            if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n                self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n            self.load_game()\n\n            if hasattr(self.tamagotchi_logic, 'statistics_window'):\n                self.tamagotchi_logic.statistics_window.update_statistics()\n\n                brain_widget = self.brain_window.brain_widget\n\n                for name in brain_widget.original_neurons:\n                    brain_widget.visible_neurons.add(name)\n                if hasattr(brain_widget, 'neurogenesis_data'):\n                    for name in brain_widget.neurogenesis_data.get('new_neurons_details', {}):\n                        brain_widget.visible_neurons.add(name)\n\n                core = brain_widget.original_neurons\n                for idx, name in enumerate(core):\n                    QtCore.QTimer.singleShot(idx * 500, lambda n=name: brain_widget.reveal_neuron(n))\n\n                self.brain_window.show()\n                self.user_interface.brain_action.setChecked(True)\n        else:\n            print(\"\\x1b[92m--------------  STARTING A NEW SIMULATION --------------\\x1b[0m\")\n\n            self.create_new_game(self.specified_personality)\n            self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n\n            self.squid.tamagotchi_logic = self.tamagotchi_logic\n            self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n            self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n            if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n                self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n\n            if not self.save_manager.save_exists():\n                QtCore.QTimer.singleShot(500, self.delayed_tutorial_check)\n\n        self._initialization_complete = True\n\n    def delayed_tutorial_check(self):\n        \"\"\"Check if the user wants to see the tutorial after UI is responsive\"\"\"\n        # Process pending events to ensure UI is responsive\n        QtWidgets.QApplication.processEvents()\n        \n        # Now check tutorial preference\n        self.check_tutorial_preference()\n        \n        # If tutorial was chosen, schedule it for later\n        if self.show_tutorial:\n            # We'll show tutorial when the game starts\n            pass\n        else:\n            # Just open initial windows if no tutorial\n            QtCore.QTimer.singleShot(500, self.open_initial_windows)\n\n    def create_new_game(self, specified_personality=None):\n        \"\"\"Create a new game instance\"\"\"\n        # Delete any existing save to ensure clean start\n        if self.save_manager.save_exists():\n            self.save_manager.delete_save()\n        \n        # Choose personality randomly if not specified\n        if specified_personality is None:\n            personality = random.choice(list(Personality))\n        else:\n            personality = specified_personality\n        \n        # Create new squid with chosen personality\n        self.squid = Squid(\n            user_interface=self.user_interface,\n            tamagotchi_logic=None,\n            personality=personality,\n            neuro_cooldown=self.neuro_cooldown\n        )\n        \n        print(f\"    \")\n        print(f\">> Generated squid personality: {self.squid.personality.value}\")\n        print(f\"    \")\n        if self.neuro_cooldown:\n            print(f\"\\x1b[43m Neurogenesis cooldown:\\033[0m {self.neuro_cooldown}\")\n        \n        self.squid.memory_manager.clear_all_memories()\n        self.show_splash_screen()\n\n    def check_tutorial_preference(self):\n        \"\"\"Show a dialog asking if the user wants to see the tutorial with 5-second timeout\"\"\"\n        # Don't ask about tutorial if save data exists\n        if self.save_manager.save_exists():\n            self.show_tutorial = False\n            return\n            \n        # Show timed dialog\n        dialog = TimedMessageBox(\n            self,\n            Localisation.instance().get(\"startup\"),\n            Localisation.instance().get(\"show_tutorial_q\"),\n            timeout_seconds=5\n        )\n        dialog.exec_()\n        \n        # Set flag based on user's choice (defaults to No if timeout)\n        self.show_tutorial = (dialog.get_result() == QtWidgets.QMessageBox.Yes)\n    \n    def position_and_show_decoration_window(self):\n        \"\"\"Position the decoration window in the bottom right and show it\"\"\"\n        if hasattr(self.user_interface, 'decoration_window') and self.user_interface.decoration_window:\n            # Get screen geometry\n            screen_geometry = QtWidgets.QApplication.desktop().availableGeometry()\n            \n            # Position window in bottom right\n            decoration_window = self.user_interface.decoration_window\n            decoration_window.move(\n                screen_geometry.right() - decoration_window.width() - 20,\n                screen_geometry.bottom() - decoration_window.height() - 20\n            )\n            decoration_window.show()\n            self.user_interface.decorations_action.setChecked(True)\n\n    def start_new_game(self):\n        \"\"\"Start a new game, deleting any existing save\"\"\"\n        # First, ask for confirmation with a timed dialog\n        confirm_dialog = TimedMessageBox(\n            self,\n            \"Confirm New Game\",\n            \"Are you sure you want to start a new game? This will delete all current progress and save data.\",\n            timeout_seconds=10\n        )\n        confirm_dialog.exec_()\n        \n        # If user declined or let it timeout, abort\n        if confirm_dialog.get_result() != QtWidgets.QMessageBox.Yes:\n            print(\"New game cancelled by user\")\n            return\n        \n        print(\"Starting new game...\")\n        \n        # Ask about tutorial\n        tutorial_dialog = TimedMessageBox(\n            self,\n            Localisation.instance().get(\"tutorial_title\"),\n            Localisation.instance().get(\"tutorial_query\"),\n            timeout_seconds=5\n        )\n        tutorial_dialog.exec_()\n        self.show_tutorial = (tutorial_dialog.get_result() == QtWidgets.QMessageBox.Yes)\n        \n        # Stop current simulation if running\n        if hasattr(self, 'tamagotchi_logic'):\n            self.tamagotchi_logic.stop()\n            # Stop autosave timer if it exists\n            if hasattr(self.tamagotchi_logic, 'autosave_timer'):\n                self.tamagotchi_logic.autosave_timer.stop()\n        \n        # Delete all save files (both autosave and manual save)\n        if self.save_manager.save_exists():\n            self.save_manager.delete_save(is_autosave=True)  # Delete autosave\n            self.save_manager.delete_save(is_autosave=False)  # Delete manual save\n            print(\"All save files deleted\")\n        \n        # Clear memory files\n        memory_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '_memory')\n        if os.path.exists(memory_dir):\n            import shutil\n            shutil.rmtree(memory_dir)\n            print(\"Memory directory cleared\")\n        \n        # Clear all neurons and state from brain window\n        if hasattr(self, 'brain_window') and hasattr(self.brain_window, 'brain_widget'):\n            brain_widget = self.brain_window.brain_widget\n            \n            # Clear visible neurons\n            brain_widget.visible_neurons = set()\n            \n            # Clear neurogenesis data\n            if hasattr(brain_widget, 'neurogenesis_data'):\n                brain_widget.neurogenesis_data = {\n                    'new_neurons': [],\n                    'new_neurons_details': {},\n                    'new_synapses': []\n                }\n            \n            # Clear enhanced neurogenesis tracking\n            if hasattr(brain_widget, 'enhanced_neurogenesis'):\n                brain_widget.enhanced_neurogenesis.reset_state()\n            \n            # Reset brain widget state\n            if hasattr(brain_widget, 'state'):\n                brain_widget.state = brain_widget.create_initial_state()\n            \n            # Clear hebbian learning state\n            if hasattr(brain_widget, 'hebbian'):\n                brain_widget.hebbian.reset()\n            \n            print(\"Brain state cleared\")\n        \n        # Clear all decorations and items from the scene\n        if hasattr(self, 'user_interface') and hasattr(self.user_interface, 'scene'):\n            # Remove all items except the background (if it exists)\n            items_to_remove = []\n            background_item = getattr(self.user_interface, 'background', None)\n            \n            for item in self.user_interface.scene.items():\n                # Keep the background (if it exists) and remove everything else\n                if background_item is None or item != background_item:\n                    items_to_remove.append(item)\n            \n            for item in items_to_remove:\n                self.user_interface.scene.removeItem(item)\n            \n            # Clear decoration tracking\n            if hasattr(self.user_interface, 'awarded_decorations'):\n                self.user_interface.awarded_decorations = set()\n            \n            print(\"Scene cleared\")\n        \n        # Create new game (creates squid but not tamagotchi_logic)\n        self.create_new_game(self.specified_personality)\n        \n        # Create TamagotchiLogic\n        self.tamagotchi_logic = TamagotchiLogic(self.user_interface, self.squid, self.brain_window)\n        \n        # Update references\n        self.squid.tamagotchi_logic = self.tamagotchi_logic\n        self.user_interface.tamagotchi_logic = self.tamagotchi_logic\n        self.brain_window.tamagotchi_logic = self.tamagotchi_logic\n        if hasattr(self.brain_window, 'set_tamagotchi_logic'):\n            self.brain_window.set_tamagotchi_logic(self.tamagotchi_logic)\n        \n        self.plugin_manager.tamagotchi_logic = self.tamagotchi_logic\n        self.tamagotchi_logic.plugin_manager = self.plugin_manager\n        \n        # Create personality starter neuron if needed\n        squid = self.tamagotchi_logic.squid\n        brain_widget = self.brain_window.brain_widget\n        if (squid and squid.personality and\n            brain_widget and hasattr(brain_widget, 'enhanced_neurogenesis')):\n            if not squid._has_personality_starter_neuron():\n                neuron = brain_widget.enhanced_neurogenesis.create_personality_starter_neuron(\n                    squid.personality.value,\n                    brain_widget.state\n                )\n                if neuron:\n                    print(f\"🧬 Personality starter neuron created: {neuron}\")\n        \n        # Reload plugins to ensure they get the new tamagotchi_logic\n        self.plugin_manager.reload_all_plugins()\n        \n        print(\"New game created successfully!\")\n\n    def load_game(self):\n        \"\"\"Delegate to tamagotchi_logic\"\"\"\n        self.tamagotchi_logic.load_game()\n\n    def save_game(self):\n        \"\"\"Delegate to tamagotchi_logic\"\"\"\n        if self.squid and self.tamagotchi_logic:\n            self.tamagotchi_logic.save_game()\n\n    def _restore_achievements_data(self, achievements_data):\n        \"\"\"Restore achievement data to the achievements plugin after plugin reload.\n        \n        This is needed because plugin instances get replaced during initialization,\n        discarding any previously loaded save data.\n        \"\"\"\n        if not achievements_data:\n            return\n        try:\n            if 'achievements' in self.plugin_manager.plugins:\n                plugin_info = self.plugin_manager.plugins['achievements']\n                instance = plugin_info.get('instance')\n                if instance and hasattr(instance, 'load_save_data'):\n                    instance.load_save_data(achievements_data)\n                    unlocked_count = len(achievements_data.get('unlocked', {}))\n                    print(f\"✓ Restored {unlocked_count} achievements\")\n        except Exception as e:\n            print(f\"[Warning] Could not restore achievements: {e}\")\n\n    def closeEvent(self, event):\n        \"\"\"Handle window close event\"\"\"\n        # Save game before closing\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            self.save_game()\n        \n        # Stop the tamagotchi logic if it has a stop method\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'stop'):\n                self.tamagotchi_logic.stop()\n            # Stop the timer if it exists\n            elif hasattr(self.tamagotchi_logic, 'timer') and self.tamagotchi_logic.timer:\n                self.tamagotchi_logic.timer.stop()\n        \n        # Clean up brain state bridge for designer sync\n        if hasattr(self, 'brain_window') and self.brain_window:\n            if hasattr(self.brain_window, 'brain_widget') and self.brain_window.brain_widget:\n                if hasattr(self.brain_window.brain_widget, 'cleanup_brain_bridge'):\n                    self.brain_window.brain_widget.cleanup_brain_bridge()\n        \n        # Close brain window\n        if hasattr(self, 'brain_window') and self.brain_window:\n            self.brain_window.close()\n        \n        event.accept()\n\n    def show_splash_screen(self):\n        \"\"\"Display splash screen animation with synchronized neuron reveal\"\"\"\n        self.splash = SplashScreen(self)\n        self.splash.finished.connect(self.start_simulation)\n        self.splash.finished.connect(lambda: self.tamagotchi_logic.statistics_window.award(1000))\n        self.splash.second_frame.connect(self.show_hatching_notification)\n\n        # NEW: award 1000 points the instant the splash ends\n        self.splash.finished.connect(\n            lambda: self.tamagotchi_logic.statistics_window.award(1000)\n        )\n\n       # After splash ends, wait 3 s then show the normal feeding hint\n        self.splash.finished.connect(lambda: QtCore.QTimer.singleShot(3000, self.show_feeding_hint))\n\n        # Check if this is a brand new game (no save exists)\n        is_new_game = not self.save_manager.save_exists()\n        print(f\"🎮 show_splash_screen: is_new_game={is_new_game}, save_exists={self.save_manager.save_exists()}\")\n\n        if is_new_game:\n            # Ensure brain widget starts empty\n            if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'visible_neurons'):\n                self.brain_window.brain_widget.visible_neurons = set()\n            \n            # Show brain window first\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n            \n            # Force immediate processing to ensure brain window is painted\n            QtWidgets.QApplication.processEvents()\n            \n            # Give the brain window time to fully render (longer delay)\n            QtCore.QTimer.singleShot(1500, lambda: self._start_splash_with_reveals())\n        else:\n            # For loaded games, show brain window with all neurons visible\n            if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'visible_neurons'):\n                brain_widget = self.brain_window.brain_widget\n                # Add all core neurons to visible set\n                for neuron_name in brain_widget.original_neurons:\n                    brain_widget.visible_neurons.add(neuron_name)\n                \n                # Also add any neurogenesis neurons that exist\n                if hasattr(brain_widget, 'neurogenesis_data') and 'new_neurons_details' in brain_widget.neurogenesis_data:\n                    for neuron_name in brain_widget.neurogenesis_data['new_neurons_details'].keys():\n                        brain_widget.visible_neurons.add(neuron_name)\n            \n            # Show brain window immediately for loaded games\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n            \n            # Force immediate processing to ensure brain window is painted\n            QtWidgets.QApplication.processEvents()\n            \n            # Show splash normally (no animated reveals needed for loaded games)\n            self.splash.show()\n            QtCore.QTimer.singleShot(1000, self.splash.start_animation)\n\n\n    def show_feeding_hint(self):\n        \"\"\"Use the same strip as every other message.\"\"\"\n        self.user_interface.show_message(\"Press D to open the Decorations window\")\n    \n    def _start_splash_with_reveals(self):\n        \"\"\"Start splash screen with neuron reveal synchronization (called after brain window is ready)\"\"\"\n        print(\"     🥚 A squid is hatching...\")\n        \n        # Connect frame changes to neuron reveals\n        self.splash.frame_changed.connect(self._reveal_neuron_for_frame)\n        \n        # Show and start the splash screen animation\n        self.splash.show()\n        QtCore.QTimer.singleShot(500, self.splash.start_animation)  # Small delay for splash to show\n\n    def _reveal_neuron_for_frame(self, frame_index):\n        \"\"\"Reveal core neurons in sequence with animation frames\"\"\"\n        if not hasattr(self.brain_window, 'brain_widget'):\n            return\n            \n        brain_widget = self.brain_window.brain_widget\n        core_neurons = brain_widget.original_neurons\n        \n        # Distribution: 1-2 neurons per frame. Now revised for 8 core neurons (indices 0-7)\n        reveal_map = {\n            0: [0],       # First frame\n            1: [1],       # Second frame\n            2: [2],       # Third frame\n            3: [3],       # Fourth frame\n            4: [4, 5],    # Fifth frame\n            5: [6, 7]     # Sixth frame\n        }\n        \n        # Reveal mapped neurons for this frame\n        for neuron_idx in reveal_map.get(frame_index, []):\n            if neuron_idx < len(core_neurons):\n                neuron_name = core_neurons[neuron_idx]\n                brain_widget.reveal_neuron(neuron_name)\n                #print(f\"🧠 Revealed neuron: {neuron_name} (frame {frame_index})\")\n\n    def show_hatching_notification(self):\n        \"\"\"Display hatching message\"\"\"\n        self.user_interface.show_message(\"Squid is hatching!\")\n\n    def start_simulation(self):\n        \"\"\"Begin the simulation - brain window is already visible for new games\"\"\"\n        self.cleanup_duplicate_squids()\n        self.tamagotchi_logic.set_simulation_speed(1)\n        self.tamagotchi_logic.start_autosave()\n\n        # Get brain widget reference\n        brain_widget = self.brain_window.brain_widget\n\n        # Show tutorial if enabled\n        if self.show_tutorial:\n            QtCore.QTimer.singleShot(1000, self.user_interface.show_tutorial_overlay)\n        else:\n            # === FIX START: Manual cleanup if tutorial is skipped ===\n            if hasattr(brain_widget, 'is_tutorial_mode'):\n                # Set the flag to False, which allows connections to draw\n                brain_widget.is_tutorial_mode = False \n                \n                # OPTIONAL: If setting the flag doesn't immediately refresh the links,\n                # you may need to force a repaint. If the links are set to show,\n                # a simple repaint will usually draw them once the block is gone.\n                brain_widget.update() # Force repaint\n            # === FIX END ===\n\n            # Only open decoration window automatically (brain window already visible for new games)\n            QtCore.QTimer.singleShot(500, self.position_and_show_decoration_window)\n\n    def show_tutorial_overlay(self):\n        \"\"\"Delegate to UI layer and ensure no duplicates remain\"\"\"\n        # First do one more duplicate cleanup\n        self.cleanup_duplicate_squids()\n        \n        # Then show the tutorial via the UI\n        if hasattr(self, 'user_interface') and self.user_interface:\n            self.user_interface.show_tutorial_overlay()\n\n    def open_initial_windows(self):\n        \"\"\"Open brain window and decorations window\"\"\"\n        # Open brain window\n        if hasattr(self, 'brain_window'):\n            self.brain_window.show()\n            self.user_interface.brain_action.setChecked(True)\n\n        # Open decorations window\n        if hasattr(self.user_interface, 'decoration_window'):\n            self.position_and_show_decoration_window()\n            self.user_interface.decorations_action.setChecked(True)\n\n    def cleanup_duplicate_squids(self):\n        \"\"\"Remove any duplicate squid items from the scene\"\"\"\n        if not hasattr(self, 'user_interface') or not self.user_interface:\n            return\n            \n        if not hasattr(self, 'squid') or not self.squid:\n            return\n            \n        try:\n            # Get the reference to our genuine squid item\n            main_squid_item = self.squid.squid_item\n            \n            # Get all items in the scene\n            all_items = self.user_interface.scene.items()\n            \n            # Track how many items we find and remove\n            found_count = 0\n            \n            # Look for graphics items that could be duplicate squids\n            for item in all_items:\n                # Skip our genuine squid item\n                if item == main_squid_item:\n                    continue\n                    \n                # Only check QGraphicsPixmapItems\n                if isinstance(item, QtWidgets.QGraphicsPixmapItem):\n                    # Check if it has the same pixmap dimensions as our squid\n                    if (hasattr(item, 'pixmap') and item.pixmap() and main_squid_item.pixmap() and\n                        item.pixmap().width() == main_squid_item.pixmap().width() and\n                        item.pixmap().height() == main_squid_item.pixmap().height()):\n                        print(f\"Found potential duplicate squid item - removing\")\n                        self.user_interface.scene.removeItem(item)\n                        found_count += 1\n            \n            if found_count > 0:\n                print(f\"Cleaned up {found_count} duplicate squid items\")\n                # Force scene update\n                self.user_interface.scene.update()\n        \n        except Exception as e:\n            print(f\"Error during cleanup: {str(e)}\")\n\n    def initialize_multiplayer_manually(self):\n        \"\"\"Manually initialize multiplayer plugin if needed\"\"\"\n        try:\n            # Import the plugin module directly\n            import sys\n            import os\n            plugin_path = os.path.join(os.path.dirname(__file__), 'plugins', 'multiplayer')\n            if plugin_path not in sys.path:\n                sys.path.insert(0, plugin_path)\n                \n            import main as multiplayer_main\n            \n            # Create plugin instance\n            multiplayer_plugin = multiplayer_main.MultiplayerPlugin()\n            \n            # Find it in plugin_manager and add the instance\n            for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n                if plugin_name.lower() == \"multiplayer\":\n                    plugin_data['instance'] = multiplayer_plugin\n                    print(f\"Manually added multiplayer plugin instance to {plugin_name}\")\n                    \n                    # Initialize the plugin\n                    if hasattr(multiplayer_plugin, 'setup'):\n                        multiplayer_plugin.setup(self.plugin_manager)\n                    \n                    # Register menu actions\n                    if hasattr(multiplayer_plugin, 'register_menu_actions'):\n                        multiplayer_plugin.register_menu_actions()\n                    \n                    break\n                    \n            # Force the UI to refresh plugin menu\n            self.user_interface.setup_plugin_menu(self.plugin_manager)\n            \n            #print(\"Manual multiplayer initialization complete\")\n            return True\n            \n        except Exception as e:\n            print(f\"Error in manual multiplayer initialization: {e}\")\n            import traceback\n            traceback.print_exc()\n            return False\n\ndef main():\n    \"\"\"Main entry point\"\"\"\n    # CRITICAL for PyInstaller + multiprocessing on Windows\n    multiprocessing.freeze_support()\n    \n    sys.excepthook = global_exception_handler\n\n    parser = argparse.ArgumentParser(description=\"Dosidicus digital squid with a neural network\")\n    parser.add_argument('-p', '--personality', type=str, \n                       choices=[p.value for p in Personality], \n                       help='Specify squid personality')\n    parser.add_argument('-d', '--debug', action='store_true', \n                       help='Enable debug mode with console logging')\n    parser.add_argument('-nc', '--neurocooldown', type=int, \n                       help='Set neurogenesis cooldown in seconds')\n    parser.add_argument('-c', '--clean', action='store_true',\n                       help='Clean __pycache__ and logs folders before starting')\n    parser.add_argument('-designer', '--designer', action='store_true',\n                       help='Launch Brain Designer standalone')\n    args = parser.parse_args()\n\n    # Perform cleanup if requested before logging setup\n    if args.clean:\n        perform_cleanup_and_exit()\n\n    # Launch designer if flag is set\n    if args.designer:\n        print(\"Launching Brain Designer standalone...\")\n        try:\n            # Import and run designer's main function\n            try:\n                from src import brain_designer\n            except ImportError:\n                import brain_designer\n            \n            # brain_designer.main() will parse sys.argv and handle -d and -c flags automatically\n            brain_designer.main()\n        except ImportError as e:\n            print(f\"Error: Could not import brain_designer module: {e}\")\n            sys.exit(1)\n        except Exception as e:\n            print(f\"Error launching designer: {e}\")\n            sys.exit(1)\n        return  # Exit after designer closes\n\n    # Initialize logging (replaces previous global setup)\n    setup_logging_configuration()\n    \n    print(f\"    Personality: {args.personality}\")\n    print(f\"    Debug mode: {args.debug}\")\n    print(f\"    Cooldown {args.neurocooldown or 'will be loaded from config'}\")\n\n    app = QtWidgets.QApplication(sys.argv)\n    \n    try:\n        personality = Personality(args.personality) if args.personality else None\n        main_window = MainWindow(personality, args.debug, args.neurocooldown)\n        main_window.show()\n        sys.exit(app.exec_())\n    except Exception as e:\n        logging.exception(\"Fatal error in main\")\n        QtWidgets.QMessageBox.critical(None, \"Error\", \n                                     f\"Critical error: {str(e)}\\nSee dosidicus_log.txt for details.\")\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "src/memory_manager.py",
    "content": "import json\nimport os\nfrom datetime import datetime\nimport time\n\nclass MemoryManager:\n    def __init__(self):\n        self.memory_dir = '_memory'\n        self.short_term_file = os.path.join(self.memory_dir, 'ShortTerm.json')\n        self.long_term_file = os.path.join(self.memory_dir, 'LongTerm.json')\n        \n        # Load memory and ensure all timestamps are converted to floats\n        self.short_term_memory = self._load_and_convert_timestamps(self.short_term_file)\n        self.long_term_memory = self._load_and_convert_timestamps(self.long_term_file)\n        \n        self.short_term_limit = 50\n        self.short_term_duration = 300  # 5 minutes in seconds\n        self.last_cleanup_time = time.time()\n        self.plant_interaction_count = {}\n\n    def _load_and_convert_timestamps(self, file_path):\n        \"\"\"Loads memory from JSON and converts all timestamps to floats.\"\"\"\n        if not os.path.exists(file_path):\n            return []\n        try:\n            with open(file_path, 'r') as file:\n                content = file.read()\n                if not content.strip():\n                    return []\n                memory_list = json.loads(content)\n                if not isinstance(memory_list, list):\n                    return []\n\n                for item in memory_list:\n                    if 'timestamp' in item:\n                        ts = item['timestamp']\n                        if isinstance(ts, str):\n                            try:\n                                # Convert ISO string to float timestamp\n                                item['timestamp'] = datetime.fromisoformat(ts).timestamp()\n                            except (ValueError, TypeError):\n                                # If conversion fails, set a default invalid timestamp\n                                item['timestamp'] = 0\n                        elif not isinstance(ts, (int, float)):\n                            item['timestamp'] = 0 # Mark other invalid types\n                return memory_list\n        except (json.JSONDecodeError, Exception) as e:\n            print(f\"Error processing memory file {file_path}: {e}\")\n            return []\n\n    def save_memory(self, memory, file_path):\n        \"\"\"Saves memory to JSON, converting float timestamps to ISO strings.\"\"\"\n        if not memory:\n            # To clear a file, we can write an empty list\n            memory_to_save = []\n        else:\n            os.makedirs(os.path.dirname(file_path), exist_ok=True)\n            serializable_memory = []\n            for item in memory:\n                new_item = item.copy()\n                if 'timestamp' in new_item and isinstance(new_item['timestamp'], float):\n                    new_item['timestamp'] = datetime.fromtimestamp(new_item['timestamp']).isoformat()\n                serializable_memory.append(new_item)\n            memory_to_save = serializable_memory\n\n        try:\n            with open(file_path, 'w') as file:\n                json.dump(memory_to_save, file, indent=4)\n        except Exception as e:\n            print(f\"Error saving memory to {file_path}: {e}\")\n\n\n    def add_short_term_memory(self, category, key, value, importance=1.0, related_neurons=None):\n        \"\"\"Adds a memory, using float timestamps.\"\"\"\n        for memory in self.short_term_memory:\n            if memory.get('key') == key and memory.get('category') == category:\n                memory['importance'] = memory.get('importance', 1.0) + 0.5\n                memory['timestamp'] = time.time()\n                if memory['importance'] >= 3.0:\n                    self.transfer_to_long_term_memory(category, key)\n                return\n\n        memory_item = {\n            \"timestamp\": time.time(),\n            \"category\": category,\n            \"key\": key,\n            \"value\": value,\n            \"importance\": importance,\n            \"related_neurons\": related_neurons or [],\n            \"access_count\": 1\n        }\n        self.short_term_memory.append(memory_item)\n        if len(self.short_term_memory) > self.short_term_limit:\n            self.short_term_memory.pop(0)\n        self.save_memory(self.short_term_memory, self.short_term_file)\n\n    def cleanup_short_term_memory(self):\n        current_time = time.time()\n        self.short_term_memory = [m for m in self.short_term_memory if isinstance(m.get('timestamp'), (int, float)) and (current_time - m.get('timestamp', 0)) <= self.short_term_duration]\n        if len(self.short_term_memory) > self.short_term_limit:\n            self.short_term_memory.sort(key=lambda x: (x.get('importance', 1), x.get('access_count', 0)), reverse=True)\n            self.short_term_memory = self.short_term_memory[:self.short_term_limit]\n\n    def add_long_term_memory(self, category, key, value):\n        \"\"\"Adds a memory to long-term storage, preventing duplicates.\"\"\"\n        for memory in self.long_term_memory:\n            if memory.get('key') == key and memory.get('category') == category:\n                # Memory already exists, so we don't add it again.\n                # Optional: update timestamp to reflect it's a reinforced memory\n                memory['timestamp'] = time.time()\n                self.save_memory(self.long_term_memory, self.long_term_file)\n                return\n\n        memory = {'category': category, 'key': key, 'value': value, 'timestamp': time.time()}\n        self.long_term_memory.append(memory)\n        self.save_memory(self.long_term_memory, self.long_term_file)\n\n    def get_short_term_memory(self, category, key, default=None):\n        current_time = time.time()\n        for memory in self.short_term_memory:\n            if memory.get('category') == category and memory.get('key') == key:\n                ts = memory.get('timestamp', 0)\n                if isinstance(ts, (int, float)) and (current_time - ts) <= self.short_term_duration:\n                    memory['access_count'] = memory.get('access_count', 0) + 1\n                    return memory.get('value')\n        return default\n\n    def get_all_short_term_memories(self, raw=False):\n        \"\"\"Retrieves all valid short-term memories.\"\"\"\n        current_time = time.time()\n        valid_memories = [\n            m for m in self.short_term_memory\n            if isinstance(m.get('timestamp'), (int, float)) and (current_time - m.get('timestamp', 0)) <= self.short_term_duration\n        ]\n        sorted_memories = sorted(valid_memories, key=lambda x: x.get('timestamp', 0), reverse=True)\n        return sorted_memories if raw else [self._format_memory_for_display(mem) for mem in sorted_memories]\n\n    def get_all_long_term_memories(self, category=None):\n        filtered = [m for m in self.long_term_memory if not (isinstance(m.get('key'), str) and m['key'].isdigit())]\n        if category:\n            return [m for m in filtered if m.get('category') == category]\n        return filtered\n\n    def get_active_memories_data(self, count=None):\n        \"\"\"Return memories with human-readable time string.\"\"\"\n        current_time = time.time()\n        active = []\n        for mem in self.short_term_memory:\n            ts = mem.get('timestamp', 0)\n            if isinstance(ts, (int, float)) and (current_time - ts) <= self.short_term_duration:\n                active.append({\n                    'category': mem.get('category'),\n                    'key'     : mem.get('key'),\n                    'formatted_value': mem.get('value'),\n                    'raw_value': mem.get('value'),\n                    'time_str' : datetime.fromtimestamp(ts).strftime('%H:%M:%S'),  # ← HH:MM:SS\n                    'importance': mem.get('importance', 1),\n                    'access_count': mem.get('access_count', 0)\n                })\n        active.sort(key=lambda x: (x['importance'], x['access_count']), reverse=True)\n        return active[:count] if count else active\n\n    def review_and_transfer_memories(self):\n        current_time = time.time()\n        # Iterate over a copy as we may modify the list\n        for memory in list(self.short_term_memory):\n            timestamp = memory.get('timestamp', 0)\n            if isinstance(timestamp, (int, float)) and (current_time - timestamp) > self.short_term_duration:\n                if self.should_transfer_to_long_term(memory):\n                    self.transfer_to_long_term_memory(memory['category'], memory['key'])\n                else:\n                    self.short_term_memory.remove(memory)\n        self.cleanup_short_term_memory()\n        \n    def periodic_memory_management(self):\n        if (time.time() - self.last_cleanup_time) > 30:\n            self.last_cleanup_time = time.time()\n            self.review_and_transfer_memories()\n\n    def transfer_to_long_term_memory(self, category, key):\n        \"\"\"Transfers an important memory from short-term to long-term storage.\"\"\"\n        memory_to_transfer = None\n        for mem in self.short_term_memory:\n             if mem.get('category') == category and mem.get('key') == key:\n                  memory_to_transfer = mem\n                  break\n        if memory_to_transfer:\n            # Use the new add_long_term_memory to handle duplicates\n            self.add_long_term_memory(\n                memory_to_transfer['category'],\n                memory_to_transfer['key'],\n                memory_to_transfer['value']\n            )\n            # Remove from short-term memory to prevent re-transfer\n            self.short_term_memory.remove(memory_to_transfer)\n            self.save_memory(self.short_term_memory, self.short_term_file)\n\n    def should_transfer_to_long_term(self, memory):\n        return (memory.get('importance', 1) >= 7 or\n                memory.get('access_count', 0) >= 3 or\n                (memory.get('importance', 1) >= 5 and memory.get('access_count', 0) >= 2))\n                \n    # Other methods (clear_all_memories, etc.) remain largely the same but benefit from consistent data\n    def clear_short_term_memory(self):\n        self.short_term_memory = []\n        self.save_memory(self.short_term_memory, self.short_term_file)\n\n    def update_memory_importance(self, category, key, importance_change):\n        for memory in self.short_term_memory:\n            if memory.get('category') == category and memory.get('key') == key:\n                memory['importance'] = max(1, min(10, memory.get('importance', 1) + importance_change))\n                self.save_memory(self.short_term_memory, self.short_term_file)\n                break\n\n    def clear_all_memories(self):\n        self.short_term_memory = []\n        self.save_memory([], self.short_term_file)\n        self.long_term_memory = []\n        self.save_memory([], self.long_term_file)\n        print(\"All memory files have been cleared.\")\n\n    def _format_memory_for_display(self, memory):\n        return memory\n\n    def format_memory(self, memory):\n        timestamp = memory.get('timestamp', 0)\n        timestamp_str = datetime.fromtimestamp(timestamp).strftime(\"%H:%M:%S\") if isinstance(timestamp, (int,float)) and timestamp > 0 else \"N/A\"\n        formatted_memory = f\"[{timestamp_str}] {memory.get('value', '')}\"\n\n        # --- Neurogenesis special card ---\n        if memory.get('category') == 'neurogenesis':\n            return (\n                f\"<div style='background-color: #4169E1; color: white; padding: 5px; margin: 5px; \"\n                f\"border-radius: 5px; display: flex; align-items: center; gap: 6px;'>\"\n                f\"<img src='images/ng.png' style='width:20px; height:20px; filter: invert(1); vertical-align: middle;' />\"\n                f\"<span>{formatted_memory}</span>\"\n                f\"<br><font color='#ADD8E6'><b>Neurogenesis</b></font><hr style='border-color:#6699FF;'></div>\"\n            )\n\n        interaction_type = \"<b>Neutral</b>\"\n        background_color = \"#FFFACD\"  # Pastel yellow\n\n        raw_value = memory.get('raw_value')\n        if isinstance(raw_value, dict):\n            total_effect = sum(float(val) for val in raw_value.values() if isinstance(val, (int, float)))\n            if total_effect > 0:\n                interaction_type = \"<font color='green'><b>Positive</b></font>\"\n                background_color = \"#D1FFD1\"\n            elif total_effect < 0:\n                interaction_type = \"<font color='red'><b>Negative</b></font>\"\n                background_color = \"#FFD1DC\"\n        elif memory.get('category') == 'mental_state' and memory.get('key') == 'startled':\n            interaction_type = \"<font color='red'><b>Negative</b></font>\"\n            background_color = \"#FFD1DC\"\n\n        return f\"<div style='background-color: {background_color}; padding: 5px; margin: 5px; border-radius: 5px;'>{formatted_memory}<br>{interaction_type}<hr></div>\""
  },
  {
    "path": "src/mental_states.py",
    "content": "# Mental states\r\n\r\nfrom PyQt5 import QtCore, QtGui, QtWidgets\r\nimport os\r\n\r\nclass MentalState:\r\n    def __init__(self, name, icon_filename):\r\n        self.name = name\r\n        self.icon_filename = icon_filename\r\n        self.is_active = False\r\n        self.icon_item = None\r\n\r\nclass MentalStateManager:\r\n    def __init__(self, squid, scene):\r\n        self.squid = squid\r\n        self.scene = scene\r\n        self.icon_offset = QtCore.QPointF(0, -100)  # Offset for all icons above the squid\r\n        self.mental_states_enabled = True  \r\n\r\n        self.mental_states = {                                      #   List of possible mental states:\r\n            \"sick\": MentalState(\"sick\", \"sick.png\"),                #   SICK\r\n            \"thinking\": MentalState(\"thinking\", \"think.png\"),       #   THINKING - CURRENTLY UNUSED\r\n            \"startled\": MentalState(\"startled\", \"startled.png\"),    #   STARTLED\r\n            \"curious\": MentalState(\"curious\", \"curious.png\")        #   CURIOUS\r\n        }\r\n\r\n    def set_mental_states_enabled(self, enabled):\r\n        self.mental_states_enabled = enabled\r\n        if not enabled:\r\n            self.clear_optional_states()\r\n\r\n    def set_state(self, state_name, is_active):\r\n        if state_name in self.mental_states:\r\n            if state_name == \"sick\" or self.mental_states_enabled:\r\n                self.mental_states[state_name].is_active = is_active\r\n                self.update_mental_state_icons()\r\n\r\n    def update_mental_state_icons(self):\r\n        for state in self.mental_states.values():\r\n            if state.name == \"sick\" or self.mental_states_enabled:\r\n                self.update_icon_state(state)\r\n\r\n    def update_icon_state(self, state):\r\n        if state.is_active:\r\n            if state.icon_item is None:\r\n                icon_pixmap = QtGui.QPixmap(os.path.join(\"images\", state.icon_filename))\r\n                state.icon_item = QtWidgets.QGraphicsPixmapItem(icon_pixmap)\r\n                state.icon_item.setZValue(500)  # Below DIRTY text (z=1000) but above decorations\r\n                self.scene.addItem(state.icon_item)\r\n            self.update_icon_position(state.icon_item)\r\n        else:\r\n            if state.icon_item is not None:\r\n                self.scene.removeItem(state.icon_item)\r\n                state.icon_item = None\r\n\r\n    def update_icon_position(self, icon_item):\r\n        icon_item.setPos(self.squid.squid_x + self.squid.squid_width // 2 - icon_item.pixmap().width() // 2 + self.icon_offset.x(),\r\n                         self.squid.squid_y + self.icon_offset.y())\r\n\r\n    def update_positions(self):\r\n        self.update_mental_state_icons()\r\n\r\n    def is_state_active(self, state_name):\r\n        if state_name == \"sick\" or self.mental_states_enabled:\r\n            return self.mental_states.get(state_name, MentalState(state_name, \"\")).is_active\r\n        return False\r\n\r\n    def clear_optional_states(self):\r\n        for state in self.mental_states.values():\r\n            if state.name != \"sick\":\r\n                if state.icon_item is not None:\r\n                    self.scene.removeItem(state.icon_item)\r\n                    state.icon_item = None\r\n                state.is_active = False\r\n"
  },
  {
    "path": "src/network_adapter.py",
    "content": "\"\"\"\r\nnetwork_adapter.py - Adapter bridging Project 1's Network to Project 2's brain systems\r\n\r\nThis adapter wraps a Network object and exposes it through the\r\nBrainProtocol interface, allowing custom networks with different topologies\r\nto plug into the brain visualization and learning systems.\r\n\r\nUsage:\r\n    from NeuralNetwork.core import Network\r\n    from network_adapter import NetworkAdapter\r\n    \r\n    # Create or load a custom network\r\n    custom_network = Network()\r\n    custom_network.add_neuron(\"input1\", 50.0, (100, 100), 'input')\r\n    ...\r\n    \r\n    # Wrap it for use with Dosidicus\r\n    adapted_brain = NetworkAdapter(custom_network)\r\n    \r\n    # Now it can be used with BrainWidget, BrainWorker, etc.\r\n\"\"\"\r\n\r\nimport json\r\nimport random\r\nimport time\r\nfrom typing import Dict, List, Tuple, Set, Any, Optional, TYPE_CHECKING\r\nfrom dataclasses import dataclass, field\r\n\r\nfrom network_protocol import (\r\n    BrainProtocol, BrainConfig, NeuronData, ConnectionData, LayerDefinition\r\n)\r\n\r\nif TYPE_CHECKING:\r\n    # Import Project 1's classes for type hints only\r\n    from core import Network, Neuron, Connection, Config\r\n\r\n\r\nclass NetworkAdapter:\r\n    \"\"\"\r\n    Adapter that wraps a Project 1 Network to implement the BrainProtocol\r\n    interface required by Project 2's brain systems.\r\n    \r\n    Features:\r\n    - Exposes network data in the format expected by BrainWidget\r\n    - Supports dynamic neurogenesis (adding neurons at runtime)\r\n    - Maintains layer structure for visualization\r\n    - Handles serialization compatible with Dosidicus-2 save system\r\n    \r\n    Args:\r\n        network: A Project 1 Network instance (or None to create fresh)\r\n        layer_structure: Optional layer definitions for visualization\r\n        config: Optional BrainConfig (created with defaults if not provided)\r\n    \"\"\"\r\n    \r\n    def __init__(self, \r\n                 network: Optional['Network'] = None,\r\n                 layer_structure: Optional[List[LayerDefinition]] = None,\r\n                 config: Optional[BrainConfig] = None):\r\n        \r\n        # Import Project 1's Network if we need to create a new one\r\n        if network is None:\r\n            from core import Network as P1Network\r\n            network = P1Network()\r\n        \r\n        self._network = network\r\n        self._layer_structure = layer_structure or []\r\n        self._config = config or BrainConfig()\r\n        \r\n        # Project 2 compatibility: excluded neurons (status indicators)\r\n        self._excluded_neurons: Set[str] = {\r\n            'is_sick', 'is_eating', 'pursuing_food', 'direction', 'is_sleeping'\r\n        }\r\n        \r\n        # Track neurogenesis data (matches BrainWidget.neurogenesis_data structure)\r\n        self._neurogenesis_data = {\r\n            'new_neurons': [],\r\n            'last_neuron_time': time.time(),\r\n            'new_neurons_details': {}\r\n        }\r\n        \r\n        # Sync config with underlying network\r\n        self._sync_config_to_network()\r\n    \r\n    # =========================================================================\r\n    # BRAINPROTOCOL REQUIRED PROPERTIES\r\n    # =========================================================================\r\n    \r\n    @property\r\n    def state(self) -> Dict[str, float]:\r\n        \"\"\"Current activation state of each neuron (0-100 scale).\"\"\"\r\n        return self._network.state\r\n    \r\n    @state.setter\r\n    def state(self, value: Dict[str, float]):\r\n        \"\"\"Allow direct state assignment for compatibility.\"\"\"\r\n        self._network.state = value\r\n    \r\n    @property\r\n    def neuron_positions(self) -> Dict[str, Tuple[float, float]]:\r\n        \"\"\"Position of each neuron for visualization.\"\"\"\r\n        return {\r\n            name: neuron.get_position() \r\n            for name, neuron in self._network.neurons.items()\r\n        }\r\n    \r\n    @neuron_positions.setter  \r\n    def neuron_positions(self, value: Dict[str, Tuple[float, float]]):\r\n        \"\"\"Allow position updates for drag-and-drop support.\"\"\"\r\n        for name, pos in value.items():\r\n            if name in self._network.neurons:\r\n                self._network.neurons[name].set_position(*pos)\r\n    \r\n    @property\r\n    def weights(self) -> Dict[Tuple[str, str], float]:\r\n        \"\"\"Connection weights as (source, target) -> weight mapping.\"\"\"\r\n        return {\r\n            (conn.source, conn.target): conn.get_weight()\r\n            for conn in self._network.connections.values()\r\n        }\r\n    \r\n    @weights.setter\r\n    def weights(self, value: Dict[Tuple[str, str], float]):\r\n        \"\"\"Allow weight updates for Hebbian learning.\"\"\"\r\n        for (source, target), weight in value.items():\r\n            key = (source, target)\r\n            if key in self._network.connections:\r\n                self._network.connections[key].set_weight(weight)\r\n            elif source in self._network.neurons and target in self._network.neurons:\r\n                # Create new connection\r\n                self._network.connect(source, target, weight)\r\n    \r\n    @property\r\n    def excluded_neurons(self) -> Set[str]:\r\n        \"\"\"Neurons excluded from Hebbian learning.\"\"\"\r\n        return self._excluded_neurons\r\n    \r\n    @excluded_neurons.setter\r\n    def excluded_neurons(self, value: Set[str]):\r\n        \"\"\"Allow updating excluded neurons.\"\"\"\r\n        self._excluded_neurons = set(value)\r\n    \r\n    @property\r\n    def config(self) -> BrainConfig:\r\n        \"\"\"Configuration object with hebbian and neurogenesis settings.\"\"\"\r\n        return self._config\r\n    \r\n    @property\r\n    def neurogenesis_data(self) -> Dict[str, Any]:\r\n        \"\"\"Neurogenesis tracking data (BrainWidget compatibility).\"\"\"\r\n        return self._neurogenesis_data\r\n    \r\n    @neurogenesis_data.setter\r\n    def neurogenesis_data(self, value: Dict[str, Any]):\r\n        \"\"\"Allow neurogenesis data updates.\"\"\"\r\n        self._neurogenesis_data = value\r\n    \r\n    # =========================================================================\r\n    # BRAINPROTOCOL REQUIRED METHODS\r\n    # =========================================================================\r\n    \r\n    def get_neuron(self, name: str) -> Optional[NeuronData]:\r\n        \"\"\"Get neuron data by name.\"\"\"\r\n        if name not in self._network.neurons:\r\n            return None\r\n        \r\n        neuron = self._network.neurons[name]\r\n        return NeuronData(\r\n            name=neuron.name,\r\n            neuron_type=neuron.type,\r\n            position=neuron.get_position(),\r\n            attributes=neuron.attributes or {}\r\n        )\r\n    \r\n    def get_all_neurons(self) -> List[NeuronData]:\r\n        \"\"\"Get list of all neurons.\"\"\"\r\n        return [\r\n            NeuronData(\r\n                name=n.name,\r\n                neuron_type=n.type,\r\n                position=n.get_position(),\r\n                attributes=n.attributes or {}\r\n            )\r\n            for n in self._network.neurons.values()\r\n        ]\r\n    \r\n    def get_connections_for_neuron(self, name: str) -> List[ConnectionData]:\r\n        \"\"\"Get all connections involving a neuron (incoming and outgoing).\"\"\"\r\n        connections = []\r\n        for (source, target), conn in self._network.connections.items():\r\n            if source == name or target == name:\r\n                connections.append(ConnectionData(\r\n                    source=source,\r\n                    target=target,\r\n                    weight=conn.get_weight()\r\n                ))\r\n        return connections\r\n    \r\n    def add_neuron(self, neuron: NeuronData, initial_activation: float = 50.0) -> bool:\r\n        \"\"\"Add a new neuron to the network.\"\"\"\r\n        success = self._network.add_neuron(\r\n            name=neuron.name,\r\n            value=initial_activation,\r\n            position=neuron.position,\r\n            n_type=neuron.neuron_type,\r\n            attributes=neuron.attributes\r\n        )\r\n        \r\n        if success:\r\n            # Track for neurogenesis data\r\n            self._neurogenesis_data['new_neurons'].append(neuron.name)\r\n            self._neurogenesis_data['last_neuron_time'] = time.time()\r\n            \r\n        return success\r\n    \r\n    def add_connection(self, source: str, target: str, weight: float) -> bool:\r\n        \"\"\"Add or update a connection.\"\"\"\r\n        # Update existing connection\r\n        if (source, target) in self._network.connections:\r\n            self._network.connections[(source, target)].set_weight(weight)\r\n            return True\r\n        \r\n        # Create new connection\r\n        return self._network.connect(source, target, weight)\r\n    \r\n    def set_neuron_activation(self, name: str, value: float) -> None:\r\n        \"\"\"Set the activation value of a neuron.\"\"\"\r\n        if name in self._network.state:\r\n            self._network.state[name] = max(0.0, min(100.0, value))\r\n    \r\n    def get_layer_structure(self) -> List[LayerDefinition]:\r\n        \"\"\"Get the layer structure of the network.\"\"\"\r\n        return self._layer_structure\r\n    \r\n    # =========================================================================\r\n    # ADDITIONAL COMPATIBILITY METHODS (for BrainWidget)\r\n    # =========================================================================\r\n    \r\n    def initialize_connections(self) -> List[Tuple[str, str]]:\r\n        \"\"\"Return list of connection pairs (BrainWidget compatibility).\"\"\"\r\n        return list(self._network.connections.keys())\r\n    \r\n    def initialize_weights(self) -> None:\r\n        \"\"\"Initialize weights (no-op, weights already exist in network).\"\"\"\r\n        pass\r\n    \r\n    @property\r\n    def connections(self) -> List[Tuple[str, str]]:\r\n        \"\"\"Connection list for BrainWidget compatibility.\"\"\"\r\n        return list(self._network.connections.keys())\r\n    \r\n    @connections.setter\r\n    def connections(self, value: List[Tuple[str, str]]):\r\n        \"\"\"Allow setting connections list.\"\"\"\r\n        # This is typically read-only, but some code might try to set it\r\n        pass\r\n    \r\n    @property\r\n    def original_neuron_positions(self) -> Dict[str, Tuple[float, float]]:\r\n        \"\"\"Original positions for reset functionality.\"\"\"\r\n        return {\r\n            name: neuron.get_position()\r\n            for name, neuron in self._network.neurons.items()\r\n        }\r\n    \r\n    @property\r\n    def original_neurons(self) -> List[str]:\r\n        \"\"\"List of original neuron names (non-neurogenesis).\"\"\"\r\n        new_neurons = set(self._neurogenesis_data.get('new_neurons', []))\r\n        return [n for n in self._network.neurons.keys() if n not in new_neurons]\r\n    \r\n    @property\r\n    def visible_neurons(self) -> Set[str]:\r\n        \"\"\"All neurons are visible by default.\"\"\"\r\n        return set(self._network.neurons.keys())\r\n    \r\n    @visible_neurons.setter\r\n    def visible_neurons(self, value: Set[str]):\r\n        \"\"\"Visibility is typically managed elsewhere, but allow setting.\"\"\"\r\n        pass\r\n    \r\n    @property\r\n    def neuron_shapes(self) -> Dict[str, str]:\r\n        \"\"\"Get neuron shapes from attributes.\"\"\"\r\n        shapes = {}\r\n        for name, neuron in self._network.neurons.items():\r\n            if neuron.attributes and 'shape' in neuron.attributes:\r\n                shapes[name] = neuron.attributes['shape']\r\n            else:\r\n                # Default shape based on type\r\n                shape_map = self._config.neurogenesis['appearance']['shapes']\r\n                shapes[name] = shape_map.get(neuron.type, 'circle')\r\n        return shapes\r\n    \r\n    # =========================================================================\r\n    # SERIALIZATION\r\n    # =========================================================================\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        \"\"\"Serialize the entire network to a dictionary.\"\"\"\r\n        return {\r\n            'version': '2.0',\r\n            'adapter_type': 'NetworkAdapter',\r\n            'neurons': {\r\n                name: {\r\n                    'type': n.type,\r\n                    'position': n.get_position(),\r\n                    'attributes': n.attributes\r\n                }\r\n                for name, n in self._network.neurons.items()\r\n            },\r\n            'connections': {\r\n                f\"{s}->{t}\": c.get_weight()\r\n                for (s, t), c in self._network.connections.items()\r\n            },\r\n            'state': dict(self._network.state),\r\n            'config': self._config.to_dict(),\r\n            'layer_structure': [l.to_dict() for l in self._layer_structure],\r\n            'excluded_neurons': list(self._excluded_neurons),\r\n            'neurogenesis_data': {\r\n                'new_neurons': self._neurogenesis_data.get('new_neurons', []),\r\n                'last_neuron_time': self._neurogenesis_data.get('last_neuron_time', 0),\r\n                'new_neurons_details': self._neurogenesis_data.get('new_neurons_details', {})\r\n            }\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: Dict[str, Any]) -> 'NetworkAdapter':\r\n        \"\"\"Deserialize a network from a dictionary.\"\"\"\r\n        from core import Network as P1Network\r\n        \r\n        network = P1Network()\r\n        \r\n        # Load neurons\r\n        for name, n_data in data.get('neurons', {}).items():\r\n            network.add_neuron(\r\n                name=name,\r\n                value=data.get('state', {}).get(name, 50.0),\r\n                position=tuple(n_data.get('position', (0, 0))),\r\n                n_type=n_data.get('type', 'hidden'),\r\n                attributes=n_data.get('attributes', {})\r\n            )\r\n        \r\n        # Load connections\r\n        for key, weight in data.get('connections', {}).items():\r\n            source, target = key.split('->')\r\n            network.connect(source, target, weight)\r\n        \r\n        # Load state\r\n        network.state = data.get('state', {n: 50.0 for n in network.neurons})\r\n        \r\n        # Load config\r\n        config = BrainConfig.from_dict(data.get('config', {}))\r\n        \r\n        # Load layer structure\r\n        layers = [\r\n            LayerDefinition(\r\n                name=l['name'],\r\n                neuron_names=l['neuron_names'],\r\n                layer_type=l['layer_type'],\r\n                y_position=l['y_position']\r\n            )\r\n            for l in data.get('layer_structure', [])\r\n        ]\r\n        \r\n        # Create adapter\r\n        adapter = cls(network, layers, config)\r\n        \r\n        # Load excluded neurons\r\n        adapter._excluded_neurons = set(data.get('excluded_neurons', []))\r\n        \r\n        # Load neurogenesis data\r\n        adapter._neurogenesis_data = data.get('neurogenesis_data', {\r\n            'new_neurons': [],\r\n            'last_neuron_time': time.time(),\r\n            'new_neurons_details': {}\r\n        })\r\n        \r\n        return adapter\r\n    \r\n    def save(self, filepath: str) -> bool:\r\n        \"\"\"Save the network to a file.\"\"\"\r\n        try:\r\n            with open(filepath, 'w') as f:\r\n                json.dump(self.to_dict(), f, indent=2)\r\n            return True\r\n        except Exception as e:\r\n            print(f\"Error saving network: {e}\")\r\n            return False\r\n    \r\n    @classmethod\r\n    def load(cls, filepath: str) -> Optional['NetworkAdapter']:\r\n        \"\"\"Load a network from a file.\"\"\"\r\n        try:\r\n            with open(filepath, 'r') as f:\r\n                data = json.load(f)\r\n            return cls.from_dict(data)\r\n        except Exception as e:\r\n            print(f\"Error loading network: {e}\")\r\n            return None\r\n    \r\n    # =========================================================================\r\n    # INTERNAL HELPERS\r\n    # =========================================================================\r\n    \r\n    def _sync_config_to_network(self):\r\n        \"\"\"Sync adapter config with underlying network config.\"\"\"\r\n        if hasattr(self._network, 'config'):\r\n            # Copy relevant settings to network's config\r\n            self._network.config.hebbian.update(self._config.hebbian)\r\n            self._network.config.neurogenesis.update(self._config.neurogenesis)\r\n    \r\n    def set_layer_structure(self, layers: List[LayerDefinition]):\r\n        \"\"\"Update the layer structure (and optionally reposition neurons).\"\"\"\r\n        self._layer_structure = layers\r\n    \r\n    def get_underlying_network(self) -> 'Network':\r\n        \"\"\"Get the underlying Project 1 Network (for advanced access).\"\"\"\r\n        return self._network\r\n\r\n\r\nclass DosidictusDefaultBrain(NetworkAdapter):\r\n    \"\"\"\r\n    Pre-configured brain matching the default Dosidicus-2 structure.\r\n    \r\n    Creates the 7 core neurons (hunger, happiness, cleanliness, sleepiness,\r\n    satisfaction, anxiety, curiosity) with standard positions and connections.\r\n    \r\n    Use this as a reference for custom brain designs.\r\n    \"\"\"\r\n    \r\n    def __init__(self):\r\n        from core import Network as P1Network\r\n        \r\n        network = P1Network()\r\n        \r\n        # Core neurons with standard positions\r\n        core_neurons = {\r\n            \"hunger\": ((127, 81), 'input'),\r\n            \"happiness\": ((361, 81), 'hidden'),\r\n            \"cleanliness\": ((627, 81), 'input'),\r\n            \"sleepiness\": ((840, 81), 'input'),\r\n            \"satisfaction\": ((271, 380), 'output'),\r\n            \"anxiety\": ((491, 389), 'output'),\r\n            \"curiosity\": ((701, 386), 'output'),\r\n        }\r\n        \r\n        # Add neurons\r\n        for name, (pos, n_type) in core_neurons.items():\r\n            network.add_neuron(name, 50.0, pos, n_type)\r\n        \r\n        # Standard connections (simplified - customize as needed)\r\n        connections = [\r\n            (\"hunger\", \"satisfaction\", -0.5),\r\n            (\"hunger\", \"anxiety\", 0.3),\r\n            (\"happiness\", \"satisfaction\", 0.6),\r\n            (\"happiness\", \"anxiety\", -0.4),\r\n            (\"cleanliness\", \"happiness\", 0.3),\r\n            (\"cleanliness\", \"anxiety\", -0.2),\r\n            (\"sleepiness\", \"curiosity\", -0.3),\r\n            (\"satisfaction\", \"happiness\", 0.4),\r\n            (\"anxiety\", \"curiosity\", -0.2),\r\n            (\"anxiety\", \"happiness\", -0.3),\r\n            (\"curiosity\", \"satisfaction\", 0.2),\r\n        ]\r\n        \r\n        for source, target, weight in connections:\r\n            network.connect(source, target, weight)\r\n        \r\n        # Define layer structure\r\n        layers = [\r\n            LayerDefinition(\"Inputs\", [\"hunger\", \"cleanliness\", \"sleepiness\"], \"input\", 81),\r\n            LayerDefinition(\"Processing\", [\"happiness\"], \"hidden\", 200),\r\n            LayerDefinition(\"Outputs\", [\"satisfaction\", \"anxiety\", \"curiosity\"], \"output\", 385),\r\n        ]\r\n        \r\n        super().__init__(network, layers)\r\n        \r\n        # Add status neurons as excluded\r\n        self._excluded_neurons = {\r\n            'is_sick', 'is_eating', 'pursuing_food', 'direction', 'is_sleeping'\r\n        }\r\n"
  },
  {
    "path": "src/network_protocol.py",
    "content": "\"\"\"\r\nnetwork_protocol.py - Protocol definition for pluggable brain networks\r\n\r\nDefines the interface that any brain implementation must follow to work\r\nwith the brain_tool, brain_widget, and neurogenesis systems.\r\n\r\nThis allows custom networks to be swapped in while maintaining full \r\ncompatibility with all visualization and learning features.\r\n\"\"\"\r\n\r\nfrom typing import Protocol, Dict, Tuple, List, Set, Any, Optional, runtime_checkable\r\nfrom dataclasses import dataclass\r\n\r\n\r\n@dataclass\r\nclass NeuronData:\r\n    \"\"\"Standard data structure for neuron information.\"\"\"\r\n    name: str\r\n    neuron_type: str  # 'input', 'hidden', 'output', 'novelty', 'stress', 'reward'\r\n    position: Tuple[float, float]\r\n    attributes: Dict[str, Any]\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        return {\r\n            'name': self.name,\r\n            'type': self.neuron_type,\r\n            'position': self.position,\r\n            'attributes': self.attributes\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: Dict[str, Any]) -> 'NeuronData':\r\n        return cls(\r\n            name=data['name'],\r\n            neuron_type=data.get('type', 'hidden'),\r\n            position=tuple(data.get('position', (0, 0))),\r\n            attributes=data.get('attributes', {})\r\n        )\r\n\r\n\r\n@dataclass  \r\nclass ConnectionData:\r\n    \"\"\"Standard data structure for connection information.\"\"\"\r\n    source: str\r\n    target: str\r\n    weight: float\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        return {\r\n            'source': self.source,\r\n            'target': self.target,\r\n            'weight': self.weight\r\n        }\r\n\r\n\r\n@dataclass\r\nclass LayerDefinition:\r\n    \"\"\"Defines a layer in the network topology.\"\"\"\r\n    name: str\r\n    neuron_names: List[str]\r\n    layer_type: str  # 'input', 'hidden', 'output'\r\n    y_position: float  # Base Y position for visualization\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        return {\r\n            'name': self.name,\r\n            'neuron_names': self.neuron_names,\r\n            'layer_type': self.layer_type,\r\n            'y_position': self.y_position\r\n        }\r\n\r\n\r\n@runtime_checkable\r\nclass BrainProtocol(Protocol):\r\n    \"\"\"\r\n    Protocol defining the interface a brain must implement to work with\r\n    the Dosidicus-2 visualization and learning systems.\r\n    \r\n    Implementations should provide these properties and methods to be\r\n    fully compatible with BrainWidget, BrainWorker, and EnhancedNeurogenesis.\r\n    \"\"\"\r\n    \r\n    # =========================================================================\r\n    # REQUIRED PROPERTIES - Must be Dict-like with these exact names\r\n    # =========================================================================\r\n    \r\n    @property\r\n    def state(self) -> Dict[str, float]:\r\n        \"\"\"Current activation state of each neuron (0-100 scale).\"\"\"\r\n        ...\r\n    \r\n    @property\r\n    def neuron_positions(self) -> Dict[str, Tuple[float, float]]:\r\n        \"\"\"Position of each neuron for visualization.\"\"\"\r\n        ...\r\n    \r\n    @property\r\n    def weights(self) -> Dict[Tuple[str, str], float]:\r\n        \"\"\"Connection weights as (source, target) -> weight mapping.\"\"\"\r\n        ...\r\n    \r\n    @property\r\n    def excluded_neurons(self) -> Set[str]:\r\n        \"\"\"Neurons excluded from Hebbian learning (status neurons, etc.).\"\"\"\r\n        ...\r\n    \r\n    @property\r\n    def config(self) -> Any:\r\n        \"\"\"Configuration object with hebbian and neurogenesis settings.\"\"\"\r\n        ...\r\n    \r\n    # =========================================================================\r\n    # REQUIRED METHODS\r\n    # =========================================================================\r\n    \r\n    def get_neuron(self, name: str) -> Optional[NeuronData]:\r\n        \"\"\"Get neuron data by name.\"\"\"\r\n        ...\r\n    \r\n    def get_all_neurons(self) -> List[NeuronData]:\r\n        \"\"\"Get list of all neurons.\"\"\"\r\n        ...\r\n    \r\n    def get_connections_for_neuron(self, name: str) -> List[ConnectionData]:\r\n        \"\"\"Get all connections involving a neuron (incoming and outgoing).\"\"\"\r\n        ...\r\n    \r\n    def add_neuron(self, neuron: NeuronData, initial_activation: float = 50.0) -> bool:\r\n        \"\"\"Add a new neuron to the network. Returns True on success.\"\"\"\r\n        ...\r\n    \r\n    def add_connection(self, source: str, target: str, weight: float) -> bool:\r\n        \"\"\"Add or update a connection. Returns True on success.\"\"\"\r\n        ...\r\n    \r\n    def set_neuron_activation(self, name: str, value: float) -> None:\r\n        \"\"\"Set the activation value of a neuron.\"\"\"\r\n        ...\r\n    \r\n    def get_layer_structure(self) -> List[LayerDefinition]:\r\n        \"\"\"Get the layer structure of the network (for visualization).\"\"\"\r\n        ...\r\n    \r\n    # =========================================================================\r\n    # SERIALIZATION\r\n    # =========================================================================\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        \"\"\"Serialize the entire network to a dictionary.\"\"\"\r\n        ...\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: Dict[str, Any]) -> 'BrainProtocol':\r\n        \"\"\"Deserialize a network from a dictionary.\"\"\"\r\n        ...\r\n    \r\n    def save(self, filepath: str) -> bool:\r\n        \"\"\"Save the network to a file.\"\"\"\r\n        ...\r\n    \r\n    @classmethod\r\n    def load(cls, filepath: str) -> Optional['BrainProtocol']:\r\n        \"\"\"Load a network from a file.\"\"\"\r\n        ...\r\n\r\n\r\nclass BrainConfig:\r\n    \"\"\"\r\n    Configuration class compatible with Dosidicus-2's config expectations.\r\n    \r\n    Provides both hebbian learning and neurogenesis configuration.\r\n    \"\"\"\r\n    \r\n    def __init__(self):\r\n        self.hebbian = {\r\n            'base_learning_rate': 0.1,\r\n            'active_threshold': 50,\r\n            'learning_interval': 30000,  # milliseconds\r\n            'weight_decay': 0.01,\r\n            'min_weight': -1.0,\r\n            'max_weight': 1.0,\r\n        }\r\n        \r\n        self.neurogenesis = {\r\n            'enabled_globally': True,\r\n            'novelty_threshold': 3.0,\r\n            'stress_threshold': 1.2,\r\n            'reward_threshold': 0.6,\r\n            'cooldown': 180,  # seconds\r\n            'max_neurons': 50,\r\n            'max_per_type': {\r\n                'stress': 3,\r\n                'novelty': 5,\r\n                'reward': 4\r\n            },\r\n            'max_per_specialization': 3,\r\n            'highlight_duration': 5.0,\r\n            'decay_rate': 0.75,\r\n            'appearance': {\r\n                'colors': {\r\n                    'default': (200, 200, 200),\r\n                    'input': (150, 220, 150),\r\n                    'hidden': (150, 150, 220),\r\n                    'output': (220, 150, 150),\r\n                    'novelty': (255, 255, 150),\r\n                    'stress': (255, 150, 150),\r\n                    'reward': (173, 216, 230),\r\n                },\r\n                'shapes': {\r\n                    'default': 'circle',\r\n                    'input': 'square',\r\n                    'hidden': 'circle',\r\n                    'output': 'diamond',\r\n                    'novelty': 'diamond',\r\n                    'stress': 'square',\r\n                    'reward': 'triangle',\r\n                }\r\n            }\r\n        }\r\n    \r\n    def to_dict(self) -> Dict[str, Any]:\r\n        return {\r\n            'hebbian': dict(self.hebbian),\r\n            'neurogenesis': dict(self.neurogenesis)\r\n        }\r\n    \r\n    @classmethod\r\n    def from_dict(cls, data: Dict[str, Any]) -> 'BrainConfig':\r\n        config = cls()\r\n        if 'hebbian' in data:\r\n            config.hebbian.update(data['hebbian'])\r\n        if 'neurogenesis' in data:\r\n            config.neurogenesis.update(data['neurogenesis'])\r\n        return config\r\n"
  },
  {
    "path": "src/neurogenesis.py",
    "content": "\"\"\"\nNeurogenesis ver3.3_stress_cap\n\n- STRICT 5-neuron cap for 'stress' type.\n- Emergency triggers STRENGTHEN existing neurons if cap is reached.\n- Anxiety reduction logic based on multipliers.\n\"\"\"\n\nimport time\nimport math\nimport random\nfrom collections import deque\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Tuple, Optional, Set, Any\nfrom PyQt5.QtCore import QTimer\n\n# Import localisation with robust fallback\ntry:\n    from localisation import loc\nexcept ImportError:\n    def loc(key, default=None, **kwargs):\n        return default if default is not None else key.replace('_', ' ').title()\n\n\n@dataclass\nclass ExperienceContext:\n    trigger_type: str\n    active_neurons: Dict[str, float]\n    recent_actions: List[str]\n    environmental_state: Dict[str, any]\n    outcome: str\n    timestamp: float\n    \n    def get_pattern_signature(self) -> str:\n        motivational_neurons = {\n            k: v for k, v in self.active_neurons.items() \n            if k in ['hunger', 'happiness', 'satisfaction', 'anxiety', 'curiosity', 'cleanliness', 'sleepiness']\n        }\n        if not motivational_neurons:\n            return f\"{self.trigger_type}_{self.outcome}\"\n        primary_neuron, primary_value = max(motivational_neurons.items(), key=lambda x: abs(x[1] - 50))\n        def get_range(value):\n            if value < 35: return \"low\"\n            elif value > 65: return \"high\"\n            else: return \"mid\"\n        pattern_parts = [self.trigger_type, self.outcome, primary_neuron, get_range(primary_value)]\n        meaningful_actions = [a for a in self.recent_actions[-3:] if a and a != 'none' and a != 'idle']\n        if meaningful_actions:\n            last_action = meaningful_actions[-1].lower()\n            if 'rock' in last_action: pattern_parts.append(\"rock\")\n            elif 'poop' in last_action: pattern_parts.append(\"poop\")\n            elif 'food' in last_action or 'eat' in last_action: pattern_parts.append(\"food\")\n            elif 'sleep' in last_action: pattern_parts.append(\"sleep\")\n        return \"_\".join(pattern_parts)\n    \n    def get_core_pattern(self) -> str:\n        motivational_neurons = {\n            k: v for k, v in self.active_neurons.items() \n            if k in ['hunger', 'happiness', 'satisfaction', 'anxiety', 'curiosity', 'cleanliness', 'sleepiness']\n        }\n        if not motivational_neurons: return f\"{self.trigger_type}_{self.outcome}\"\n        primary_neuron, primary_value = max(motivational_neurons.items(), key=lambda x: abs(x[1] - 50))\n        intensity = \"high\" if primary_value > 60 or primary_value < 40 else \"mid\"\n        return f\"{self.trigger_type}_{self.outcome}_{primary_neuron}_{intensity}\"\n    \n    def get_parent_pattern(self) -> str:\n        motivational_neurons = {k: v for k, v in self.active_neurons.items() \n                                if not k.startswith('is_') and k not in [\n                                    'position', 'direction', 'status', 'pursuing_food',\n                                    'novelty_exposure', 'sustained_stress', 'recent_rewards', \n                                    'personality', 'neurogenesis_active'\n                                ]}\n        top_neurons = sorted(motivational_neurons.items(), key=lambda x: abs(x[1] - 50), reverse=True)[:2]\n        pattern = f\"{self.trigger_type}_{self.outcome}\"\n        for neuron, activation in top_neurons:\n            if activation < 40: pattern += f\"_{neuron}_low\"\n            elif activation > 60: pattern += f\"_{neuron}_high\"\n        return pattern\n\nclass ExperienceBuffer:\n    def __init__(self, max_size=50):\n        self.buffer = deque(maxlen=max_size)\n        self.pattern_counts = {}\n        self.parent_pattern_counts = {}\n        self.core_pattern_counts = {}\n        self._max_pattern_entries = 500\n        \n    def add_experience(self, context: ExperienceContext):\n        self.buffer.append(context)\n        pattern = context.get_pattern_signature()\n        self.pattern_counts[pattern] = self.pattern_counts.get(pattern, 0) + 1\n        parent = context.get_parent_pattern()\n        self.parent_pattern_counts[parent] = self.parent_pattern_counts.get(parent, 0) + 1\n        core = context.get_core_pattern()\n        self.core_pattern_counts[core] = self.core_pattern_counts.get(core, 0) + 1\n        self._prune_pattern_counts_if_needed()\n        \n    def _prune_pattern_counts_if_needed(self):\n        if len(self.pattern_counts) > self._max_pattern_entries:\n            recent_patterns = {exp.get_pattern_signature() for exp in self.buffer}\n            self.pattern_counts = {k: v for k, v in self.pattern_counts.items() if k in recent_patterns}\n        if len(self.parent_pattern_counts) > self._max_pattern_entries // 2:\n            recent_parents = {exp.get_parent_pattern() for exp in self.buffer}\n            self.parent_pattern_counts = {k: v for k, v in self.parent_pattern_counts.items() if k in recent_parents}\n        if len(self.core_pattern_counts) > self._max_pattern_entries // 4:\n            recent_cores = {exp.get_core_pattern() for exp in self.buffer}\n            self.core_pattern_counts = {k: v for k, v in self.core_pattern_counts.items() if k in recent_cores}\n    \n    def get_pattern_recurrence(self, context: ExperienceContext) -> Tuple[str, int, str]:\n        specific = context.get_pattern_signature()\n        parent = context.get_parent_pattern()\n        core = context.get_core_pattern()\n        specific_count = self.pattern_counts.get(specific, 0)\n        parent_count = self.parent_pattern_counts.get(parent, 0)\n        core_count = self.core_pattern_counts.get(core, 0)\n        \n        if specific_count >= 2: return ('specific', specific_count, specific)\n        if parent_count >= 3: return ('parent', parent_count, parent)\n        if core_count >= 5: return ('core', core_count, core)\n        if specific_count >= parent_count and specific_count >= core_count: return ('specific', specific_count, specific)\n        elif parent_count >= core_count: return ('parent', parent_count, parent)\n        else: return ('core', core_count, core)\n    \n    def to_dict(self):\n        return {\n            'pattern_counts': dict(self.pattern_counts),\n            'parent_pattern_counts': dict(self.parent_pattern_counts),\n            'core_pattern_counts': dict(self.core_pattern_counts),\n            'buffer_size': len(self.buffer),\n            'recent_experiences': [\n                {\n                    'trigger_type': exp.trigger_type,\n                    'active_neurons': exp.active_neurons,\n                    'recent_actions': exp.recent_actions,\n                    'environmental_state': exp.environmental_state,\n                    'outcome': exp.outcome,\n                    'timestamp': exp.timestamp\n                } for exp in list(self.buffer)\n            ]\n        }\n\n    @classmethod\n    def from_dict(cls, data):\n        buf = cls(max_size=data.get('buffer_size', 50))\n        buf.pattern_counts = dict(data.get('pattern_counts', {}))\n        buf.parent_pattern_counts = dict(data.get('parent_pattern_counts', {}))\n        buf.core_pattern_counts = dict(data.get('core_pattern_counts', {}))\n        for exp in data.get('recent_experiences', []):\n            ctx = ExperienceContext(\n                trigger_type=exp['trigger_type'],\n                active_neurons={k: float(v) if isinstance(v, (int, float)) else 50.0 for k, v in exp.get('active_neurons', {}).items()},\n                recent_actions=exp.get('recent_actions', []),\n                environmental_state=exp.get('environmental_state', {}),\n                outcome=exp.get('outcome', 'neutral'),\n                timestamp=exp.get('timestamp', time.time())\n            )\n            buf.buffer.append(ctx)\n        return buf\n\nclass FunctionalNeuron:\n    def __init__(self, name: str, neuron_type: str, creation_context: ExperienceContext):\n        self.name = name\n        self.neuron_type = neuron_type\n        self.creation_context = creation_context\n        self.specialization = self._determine_specialization()\n        self.activation_count = 0\n        self.last_activated = 0\n        self.utility_score = 0.0\n        self.strength_multiplier = 1.0\n\n    @property\n    def display_name(self) -> str:\n        type_key = f\"neuron_type_{self.neuron_type}\"\n        type_default = self.neuron_type.capitalize()\n        type_str = loc(type_key, default=type_default)\n        spec_key = f\"spec_{self.specialization}\"\n        spec_default = self.specialization.replace('_', ' ').title()\n        spec_str = loc(spec_key, default=spec_default)\n        parts = self.name.split('_')\n        suffix = \"\"\n        if parts[-1].isdigit(): suffix = f\" {parts[-1]}\"\n        return loc(\"neuron_name_format\", default=\"{type}: {spec}{suffix}\", type=type_str, spec=spec_str, suffix=suffix)\n\n    @classmethod\n    def from_dict(cls, data):\n        creation_ctx = data['creation_context']\n        active_neurons_data = creation_ctx.get('active_neurons') or creation_ctx.get('brain_state', {})\n        ctx = ExperienceContext(\n            trigger_type=creation_ctx['trigger_type'],\n            active_neurons=active_neurons_data,\n            recent_actions=creation_ctx['recent_actions'],\n            environmental_state=creation_ctx['environmental_state'],\n            outcome=creation_ctx['outcome'],\n            timestamp=creation_ctx['timestamp'],\n        )\n        neuron = cls.__new__(cls)\n        neuron.name = data['name']\n        neuron.neuron_type = data['neuron_type']\n        neuron.creation_context = ctx\n        neuron.specialization = data['specialization']\n        neuron.activation_count = data['activation_count']\n        neuron.last_activated = data['last_activated']\n        neuron.utility_score = data['utility_score']\n        neuron.strength_multiplier = data['strength_multiplier']\n        return neuron\n\n    def to_dict(self):\n        return {\n            'name': self.name,\n            'neuron_type': self.neuron_type,\n            'specialization': self.specialization,\n            'activation_count': self.activation_count,\n            'last_activated': self.last_activated,\n            'utility_score': self.utility_score,\n            'strength_multiplier': self.strength_multiplier,\n            'creation_context': {\n                'trigger_type': self.creation_context.trigger_type,\n                'timestamp': self.creation_context.timestamp,\n                'active_neurons': self.creation_context.active_neurons,\n                'recent_actions': self.creation_context.recent_actions,\n                'environmental_state': self.creation_context.environmental_state,\n                'outcome': self.creation_context.outcome,\n            }\n        }\n\n    def _determine_specialization(self):\n        ctx = self.creation_context\n        if ctx.trigger_type == 'reward':\n            if ctx.environmental_state.get('is_eating', False): return 'feeding_satisfaction'\n            elif ctx.active_neurons.get('cleanliness', 50) > 70 and ctx.outcome == 'positive': return 'cleanliness_reward'\n            elif ctx.active_neurons.get('sleepiness', 50) < 30 and ctx.outcome == 'positive': return 'rest_reward'\n            else: return 'general_reward'\n        elif ctx.trigger_type == 'stress':\n            if ctx.active_neurons.get('hunger', 50) > 70: return 'hunger_stress_response'\n            elif ctx.active_neurons.get('cleanliness', 50) < 30: return 'filth_avoidance'\n            elif ctx.active_neurons.get('anxiety', 50) > 70: return 'anxiety_regulation'\n            else: return 'general_stress_coping'\n        elif ctx.trigger_type == 'novelty':\n            if ctx.environmental_state.get('has_rock', False) or 'rock' in str(ctx.environmental_state): return 'object_investigation'\n            elif 'new_location' in ctx.recent_actions: return 'exploration_memory'\n            else: return 'general_novelty_processing'\n        return 'undefined'\n    \n    def get_functional_connections(self, all_neurons: List[str]) -> Dict[str, float]:\n        connections = {}\n        ctx = self.creation_context\n        for neuron, activation in ctx.active_neurons.items():\n            if neuron in all_neurons:\n                try: activation_value = float(activation) if isinstance(activation, (int, float, str)) else 50.0\n                except (ValueError, TypeError): activation_value = 50.0\n                deviation = abs(activation_value - 50)\n                if deviation > 20:\n                    weight = (deviation / 50) * 0.8\n                    if activation_value < 50: weight = -weight\n                    connections[neuron] = weight\n        spec_connections = self._get_specialization_connections(all_neurons)\n        connections.update(spec_connections)\n        return connections\n    \n    def _get_specialization_connections(self, all_neurons: List[str]) -> Dict[str, float]:\n        connections = {}\n        \n        # === STRESS SPECIFIC LOGIC FOR INHIBITING ANXIETY ===\n        if self.neuron_type == 'stress':\n             # All stress neurons should naturally inhibit anxiety to simulate coping\n             if 'anxiety' in all_neurons: connections['anxiety'] = -1.0\n             \n        if self.specialization == 'feeding_satisfaction':\n            if 'hunger' in all_neurons: connections['hunger'] = -0.7\n            if 'happiness' in all_neurons: connections['happiness'] = 0.6\n            if 'satisfaction' in all_neurons: connections['satisfaction'] = 0.8\n        elif self.specialization == 'hunger_stress_response':\n            if 'hunger' in all_neurons: connections['hunger'] = 0.7\n            if 'anxiety' in all_neurons: connections['anxiety'] = -0.8 # Inhibition\n            if 'curiosity' in all_neurons: connections['curiosity'] = 0.4\n        elif self.specialization == 'filth_avoidance':\n            if 'cleanliness' in all_neurons: connections['cleanliness'] = -0.8\n            if 'anxiety' in all_neurons: connections['anxiety'] = 0.6 # This causes anxiety\n        elif self.specialization == 'anxiety_regulation':\n            if 'anxiety' in all_neurons: connections['anxiety'] = -1.0 # Strong inhibition\n            if 'happiness' in all_neurons: connections['happiness'] = 0.4\n            if 'satisfaction' in all_neurons: connections['satisfaction'] = 0.3\n        elif self.specialization == 'general_stress_coping':\n             if 'anxiety' in all_neurons: connections['anxiety'] = -0.9\n        elif self.specialization == 'object_investigation':\n            if 'curiosity' in all_neurons: connections['curiosity'] = 0.7\n            if 'anxiety' in all_neurons: connections['anxiety'] = -0.4\n        elif self.specialization == 'rest_reward':\n            if 'sleepiness' in all_neurons: connections['sleepiness'] = -0.6\n            if 'satisfaction' in all_neurons: connections['satisfaction'] = 0.5\n            if 'happiness' in all_neurons: connections['happiness'] = 0.4\n        elif self.specialization == 'cleanliness_reward':\n            if 'cleanliness' in all_neurons: connections['cleanliness'] = 0.6\n            if 'satisfaction' in all_neurons: connections['satisfaction'] = 0.5\n            if 'anxiety' in all_neurons: connections['anxiety'] = -0.3\n        return connections\n    \n    def calculate_activation(self, brain_state: Dict[str, float], weights: Dict[Tuple[str, str], float]) -> float:\n        activation = 50.0\n        for (source, target), weight in weights.items():\n            if target == self.name and source in brain_state:\n                source_activation = float(brain_state[source])\n                influence = (source_activation - 50.0) * weight\n                activation += influence\n        activation = 50.0 + (activation - 50.0) * self.strength_multiplier\n        activation = max(0.0, min(100.0, activation))\n        if abs(activation - 50.0) > 15.0:\n            self.activation_count += 1\n            self.last_activated = time.time()\n        return activation\n    \n    def update_utility_score(self, outcome_value: float):\n        alpha = 0.3\n        self.utility_score = alpha * outcome_value + (1 - alpha) * self.utility_score\n\nclass EnhancedNeurogenesis:\n    def __init__(self, brain_widget, config):\n        self.brain_widget = brain_widget\n        self.config = config\n        self.experience_buffer = ExperienceBuffer()\n        self.functional_neurons: Dict[str, FunctionalNeuron] = {}\n        self.novelty_neuron_count = 0\n        self._awarded_neurons = set()\n        self.last_neurogenesis_time = 0\n        self.neurons_created_this_session = 0\n        self.last_creation_by_type = {'novelty': 0, 'stress': 0, 'reward': 0}\n        self._first_real_tick = None\n        self.recent_actions = deque(maxlen=10)\n        self.last_states = deque(maxlen=5)\n        self._on_neuron_created_callback = None\n        self._on_neuron_leveled_callback = None\n\n    def _get_stress_neuron_count(self) -> int:\n        \"\"\"Count current stress neurons.\"\"\"\n        return len([n for n, fn in self.functional_neurons.items() \n                    if getattr(fn, 'neuron_type', '') == 'stress'])\n    \n    def _get_anxiety_cap(self) -> float:\n        \"\"\"\n        Calculate the maximum anxiety allowed based on stress neuron count.\n        Each stress neuron reduces the cap by 10, representing growing resilience.\n        \n        0 neurons: cap = 100 (no protection)\n        1 neuron:  cap = 90\n        2 neurons: cap = 80\n        3 neurons: cap = 70\n        4 neurons: cap = 60\n        5 neurons: cap = 50 (maximum resilience - anxiety can never exceed 50!)\n        \"\"\"\n        stress_count = self._get_stress_neuron_count()\n        cap = 100.0 - (stress_count * 10.0)\n        return max(50.0, cap)  # Floor at 50 even if somehow more than 5 neurons\n    \n    def _get_scaled_relief(self, base_amount: float, is_emergency: bool = False) -> float:\n        \"\"\"\n        Calculate anxiety relief that scales with existing stress neuron count.\n        More neurons = stronger relief (cumulative coping mechanisms).\n        \n        Base formula: base_amount * (1 + 0.5 * stress_count)\n        - 0 neurons: 1.0x multiplier\n        - 1 neuron:  1.5x multiplier  \n        - 2 neurons: 2.0x multiplier\n        - 3 neurons: 2.5x multiplier\n        - 4 neurons: 3.0x multiplier\n        - 5 neurons: 3.5x multiplier\n        \n        Emergency adds an additional 1.5x multiplier.\n        \"\"\"\n        stress_count = self._get_stress_neuron_count()\n        multiplier = 1.0 + (0.5 * stress_count)\n        \n        if is_emergency:\n            multiplier *= 1.5\n            \n        return base_amount * multiplier\n\n    def _apply_anxiety_relief(self, base_drop: float, source: str = \"stress_neuron\", \n                               is_emergency: bool = False) -> float:\n        \"\"\"\n        Apply scaled anxiety relief to BOTH the brain_widget state AND the actual squid.\n        Relief scales with stress neuron count, and respects the anxiety cap.\n        \n        Args:\n            base_drop: Base amount to reduce anxiety by (will be scaled)\n            source: Description for logging\n            is_emergency: Whether this is an emergency situation (extra multiplier)\n            \n        Returns:\n            The new anxiety value\n        \"\"\"\n        old_anxiety = self.brain_widget.state.get('anxiety', 50)\n        stress_count = self._get_stress_neuron_count()\n        anxiety_cap = self._get_anxiety_cap()\n        scaled_drop = self._get_scaled_relief(base_drop, is_emergency)\n        \n        # Apply the drop\n        new_anxiety = max(0.0, old_anxiety - scaled_drop)\n        \n        # Also enforce the cap (in case anxiety was already above it)\n        new_anxiety = min(new_anxiety, anxiety_cap)\n        \n        # 1. Update brain_widget state (for visualization)\n        self.brain_widget.state['anxiety'] = new_anxiety\n        \n        # 2. CRITICAL: Also update the actual squid's anxiety!\n        if (hasattr(self.brain_widget, 'tamagotchi_logic') and \n            self.brain_widget.tamagotchi_logic and\n            hasattr(self.brain_widget.tamagotchi_logic, 'squid') and\n            self.brain_widget.tamagotchi_logic.squid):\n            squid = self.brain_widget.tamagotchi_logic.squid\n            squid.anxiety = new_anxiety\n            emergency_str = \" [EMERGENCY]\" if is_emergency else \"\"\n            print(f\"   ✅ {source}{emergency_str}: Anxiety {old_anxiety:.1f} → {new_anxiety:.1f} \"\n                  f\"(drop: -{scaled_drop:.1f}, cap: {anxiety_cap:.0f}, neurons: {stress_count})\")\n        else:\n            print(f\"   ⚠️ {source}: Could not access squid - relief only applied to brain_widget!\")\n        \n        return new_anxiety\n    \n    def enforce_anxiety_cap(self) -> None:\n        \"\"\"\n        Enforce the anxiety cap based on current stress neuron count.\n        Call this periodically to ensure anxiety never exceeds the cap.\n        \"\"\"\n        anxiety_cap = self._get_anxiety_cap()\n        \n        # Enforce on brain_widget state\n        if 'anxiety' in self.brain_widget.state:\n            current = self.brain_widget.state['anxiety']\n            if current > anxiety_cap:\n                self.brain_widget.state['anxiety'] = anxiety_cap\n        \n        # Enforce on actual squid\n        if (hasattr(self.brain_widget, 'tamagotchi_logic') and \n            self.brain_widget.tamagotchi_logic and\n            hasattr(self.brain_widget.tamagotchi_logic, 'squid') and\n            self.brain_widget.tamagotchi_logic.squid):\n            squid = self.brain_widget.tamagotchi_logic.squid\n            if squid.anxiety > anxiety_cap:\n                old = squid.anxiety\n                squid.anxiety = anxiety_cap\n                print(f\"   🛡️ Anxiety cap enforced: {old:.1f} → {anxiety_cap:.1f} (stress neurons: {self._get_stress_neuron_count()})\")\n\n    def create_neuron(self, neuron_type: str, context: Optional[ExperienceContext] = None, brain_state: Optional[Dict[str, float]] = None, environment: Optional[Dict[str, Any]] = None, trigger_value: Optional[float] = None, is_emergency: bool = False) -> Optional[str]:\n        if context is None:\n            if brain_state is None: brain_state = dict(self.brain_widget.state)\n            if environment is None: environment = {}\n            context = self._build_context(neuron_type, brain_state, environment)\n        return self._create_neuron_internal(context, trigger_value, is_emergency)\n    \n    def _make_reciprocal_connections(self, new_neuron: str):\n        bw = self.brain_widget\n        created = []                       \n        MIN_RECIPROCAL = 0.2               \n        outgoing = [(tgt, w) for (src, tgt), w in bw.weights.items() if src == new_neuron and abs(w) >= MIN_RECIPROCAL]\n        for target, w in outgoing:\n            if (target, new_neuron) in bw.weights: continue\n            bw.weights[(target, new_neuron)] = w\n            created.append(f\"{target}→{new_neuron}:{w:+.2f}\")\n        if created: print(f\"   🔗 {loc('log_reciprocal_links', default='Reciprocal links added')}: {', '.join(created)}\")\n    \n    def create_functional_neuron(self, ctx: ExperienceContext, is_emergency: bool = False) -> Optional[str]:\n        return self._create_neuron_internal(ctx, is_emergency=is_emergency)\n    \n    def _build_context(self, trigger_type: str, brain_state: Dict[str, float], environment: Dict[str, Any]) -> ExperienceContext:\n        clean_neurons = {k: float(v) if isinstance(v, (int, float)) else 50.0 for k, v in brain_state.items() if not k.startswith('is_') and k not in ['novelty_exposure', 'sustained_stress', 'recent_rewards', 'neurogenesis_active', 'personality', 'pursuing_food', 'position', 'direction', 'status']}\n        happiness = brain_state.get('happiness', 50)\n        anxiety = brain_state.get('anxiety', 50)\n        if happiness > 60: outcome = 'positive'\n        elif anxiety > 70: outcome = 'negative'\n        else: outcome = 'neutral'\n        return ExperienceContext(trigger_type=trigger_type, active_neurons=clean_neurons, recent_actions=list(self.recent_actions)[-5:], environmental_state=environment, outcome=outcome, timestamp=time.time())\n    \n    def _create_neuron_internal(self, ctx: ExperienceContext, trigger_value_for_log: Optional[float] = None, is_emergency: bool = False) -> Optional[str]:\n        trigger_type = ctx.trigger_type\n        \n        # 0. CHECK EMERGENCY CONTEXT\n        if not is_emergency and trigger_type == 'stress':\n            if ctx.active_neurons.get('anxiety', 50) >= 90:\n                is_emergency = True\n                print(f\"🚨 {loc('log_emergency_context', default='Emergency context detected (Anxiety > 90)')}\")\n\n        # 1. HARD TYPE CAP & STRENGTHENING LOGIC\n        # FORCE CAP OF 5 FOR STRESS NEURONS\n        default_caps = {'stress': 5, 'novelty': 6, 'reward': 6, 'connector': 10}\n        max_for_this_type = self.config.neurogenesis.get('max_per_type', default_caps).get(trigger_type, 5)\n        \n        # Ensure stress is strictly 5\n        if trigger_type == 'stress': max_for_this_type = 5\n\n        current_type_count = len([name for name, fn in self.functional_neurons.items() if fn.neuron_type == trigger_type])\n\n        # IF CAP REACHED: STRENGTHEN EXISTING (Even in Emergency)\n        if current_type_count >= max_for_this_type:\n            msg = loc('log_type_cap_reached', default=\"Type cap reached for {type} ({count}/{max}), strengthening existing\", type=trigger_type, count=current_type_count, max=max_for_this_type)\n            print(f\"   {msg}\")\n            \n            # If emergency, we perform a strengthening action as the coping mechanism\n            if is_emergency:\n                print(f\"   💪 Emergency: Boosting stress tolerance via existing neurons.\")\n                # [FIXED] Apply SCALED relief to BOTH brain_widget AND squid\n                if 'anxiety' in self.brain_widget.state:\n                    self._apply_anxiety_relief(15.0, \"Emergency strengthen\", is_emergency=True)\n                \n            self._strengthen_existing_neuron(trigger_type, self._preview_specialization(ctx))\n            return None\n\n        # 2. SPECIALIZATION\n        spec = self._preview_specialization(ctx)\n        base_name = f\"{trigger_type}_{spec}\"\n\n        # 3. GLOBAL NEURON LIMIT\n        current_total = len(self.brain_widget.neuron_positions) - len(self.brain_widget.excluded_neurons)\n        max_neurons = self.config.neurogenesis.get('max_neurons', 32)\n        if current_total >= max_neurons and not is_emergency:\n            print(f\"   Max neurons reached ({current_total}/{max_neurons})\")\n            return None\n        elif is_emergency and current_total >= max_neurons:\n             print(f\"⚠️ {loc('log_global_cap_bypass', default='Emergency override: Bypassing global neuron limit!')}\")\n\n        neuron_name = self._get_unique_neuron_name(base_name)\n        func_neuron = FunctionalNeuron(neuron_name, trigger_type, ctx)\n        self.functional_neurons[neuron_name] = func_neuron\n        if trigger_type == 'novelty': self.novelty_neuron_count += 1\n        self.last_creation_by_type[trigger_type] = time.time()\n        self.neurons_created_this_session += 1\n        \n        position = self._calculate_functional_position(func_neuron)\n        self.brain_widget.neuron_positions[neuron_name] = position\n        self._set_neuron_appearance(neuron_name, func_neuron)\n        self.brain_widget.state[neuron_name] = 50.0\n        \n        # [FIXED] Immediate Anxiety Relief upon creation - SCALED based on existing neurons!\n        if trigger_type == 'stress' and 'anxiety' in self.brain_widget.state:\n            base_drop = 15.0  # Base drop amount (will be scaled by helper)\n            new_anxiety = self._apply_anxiety_relief(base_drop, f\"Stress Neuron '{neuron_name}' created\", is_emergency=is_emergency)\n            print(f\"   📉 Stress Neuron Created: Anxiety now at {new_anxiety:.1f} (cap: {self._get_anxiety_cap():.0f})\")\n\n        connections = func_neuron.get_functional_connections(list(self.brain_widget.neuron_positions.keys()))\n        for target, weight in connections.items():\n            if abs(weight) < 0.05: continue\n            self.brain_widget.weights[(neuron_name, target)] = weight\n        self._make_reciprocal_connections(neuron_name)\n        \n        if hasattr(self.brain_widget, 'visible_neurons'): self.brain_widget.visible_neurons.add(neuron_name)\n        self.brain_widget.neurogenesis_highlight = {'neuron': neuron_name, 'start_time': time.time(), 'duration': 8.0 if trigger_type == 'connector' else 4.0, 'pulse_phase': 0}\n        self._log_neuron_creation(neuron_name, trigger_type, spec, trigger_value_for_log)\n        self._record_neurogenesis_memory(neuron_name)\n        return neuron_name\n\n    def _record_neurogenesis_memory(self, neuron_name: str):\n        \"\"\"Permanently records a new neuron growth event in long-term memory.\"\"\"\n        try:\n            if (hasattr(self.brain_widget, 'tamagotchi_logic') and\n                    self.brain_widget.tamagotchi_logic and\n                    hasattr(self.brain_widget.tamagotchi_logic, 'squid') and\n                    self.brain_widget.tamagotchi_logic.squid and\n                    hasattr(self.brain_widget.tamagotchi_logic.squid, 'memory_manager')):\n                mm = self.brain_widget.tamagotchi_logic.squid.memory_manager\n                mm.add_long_term_memory(\n                    'neurogenesis',\n                    f'grew_neuron_{neuron_name}',\n                    f'GREW A NEW NEURON!: {neuron_name}'\n                )\n        except Exception as e:\n            print(f\"⚠️ Could not record neurogenesis memory: {e}\")\n\n    def _on_neuron_created(self, neuron_name: str, neuron_type: str):\n        self._trigger_link_toggle_effect()\n    \n    def _get_unique_neuron_name(self, base_name: str) -> str:\n        if base_name not in self.brain_widget.neuron_positions: return base_name\n        counter = 2\n        while True:\n            candidate = f\"{base_name}_{counter}\"\n            if candidate not in self.brain_widget.neuron_positions: return candidate\n            counter += 1\n\n    def _rebuild_new_neurons_details(self):\n        core = {'hunger', 'happiness', 'cleanliness', 'sleepiness', 'satisfaction', 'anxiety', 'curiosity'}\n        details = self.brain_widget.neurogenesis_data.setdefault('new_neurons_details', {})\n        for name, fn in self.functional_neurons.items():\n            if name in core or name in self.brain_widget.excluded_neurons: continue\n            if name not in details:\n                details[name] = {'created_at': fn.creation_context.timestamp, 'trigger_type': fn.neuron_type, 'trigger_value_at_creation': 0, 'specialisation': fn.specialization, 'display_name': fn.display_name}\n            details[name]['display_name'] = fn.display_name\n\n    def _rebuild_new_neurons_details_for_lab(self):\n        self._rebuild_new_neurons_details()\n    \n    def _preview_specialization(self, ctx: ExperienceContext) -> str:\n        if ctx.trigger_type == 'reward':\n            if ctx.environmental_state.get('is_eating', False): return 'feeding_satisfaction'\n            if ctx.active_neurons.get('cleanliness', 50) > 70 and ctx.outcome == 'positive': return 'cleanliness_reward'\n            if ctx.active_neurons.get('sleepiness', 50) < 30 and ctx.outcome == 'positive': return 'rest_reward'\n            return 'general_reward'\n        if ctx.trigger_type == 'stress':\n            if ctx.active_neurons.get('hunger', 50) > 70: return 'hunger_stress_response'\n            if ctx.active_neurons.get('cleanliness', 50) < 30: return 'filth_avoidance'\n            if ctx.active_neurons.get('anxiety', 50) > 70: return 'anxiety_regulation'\n            return 'general_stress_coping'\n        if ctx.trigger_type == 'novelty':\n            if ctx.environmental_state.get('has_rock', False) or 'rock' in str(ctx.environmental_state): return 'object_investigation'\n            if 'new_location' in ctx.recent_actions: return 'exploration_memory'\n            return 'general_novelty_processing'\n        return 'undefined'\n    \n    def _calculate_functional_position(self, func_neuron: FunctionalNeuron) -> Tuple[float, float]:\n        all_neurons = list(self.brain_widget.neuron_positions.keys())\n        connections = func_neuron.get_functional_connections(all_neurons)\n        if not connections: return (random.randint(100, 900), random.randint(100, 600))\n        total_weight = 0\n        center_x, center_y = 0, 0\n        for target, weight in connections.items():\n            if target in self.brain_widget.neuron_positions:\n                pos = self.brain_widget.neuron_positions[target]\n                abs_weight = abs(weight)\n                center_x += pos[0] * abs_weight\n                center_y += pos[1] * abs_weight\n                total_weight += abs_weight\n        if total_weight > 0:\n            center_x /= total_weight\n            center_y /= total_weight\n            offset_x = random.randint(-80, 80)\n            offset_y = random.randint(-80, 80)\n            x = max(50, min(974, center_x + offset_x))\n            y = max(50, min(668, center_y + offset_y))\n            return (x, y)\n        return (random.randint(100, 900), random.randint(100, 600))\n\n    def rescue_orphan(self, orphan_name: str):\n        connector_type = 'connector'\n        neuron_name = self._get_unique_neuron_name(f\"{connector_type}_rescue\")\n        ctx = ExperienceContext(trigger_type=connector_type, active_neurons=self.brain_widget.state.copy(), recent_actions=[], environmental_state={'orphan_rescue': True}, outcome='neutral', timestamp=time.time())\n        func_neuron = FunctionalNeuron(neuron_name, connector_type, ctx)\n        func_neuron.specialization = 'network_bridge'\n        self.functional_neurons[neuron_name] = func_neuron\n        orphan_pos = self.brain_widget.neuron_positions.get(orphan_name, (500, 300))\n        center_x, center_y = 512, 384\n        new_x = (orphan_pos[0] + center_x) / 2 + random.randint(-50, 50)\n        new_y = (orphan_pos[1] + center_y) / 2 + random.randint(-50, 50)\n        self.brain_widget.neuron_positions[neuron_name] = (new_x, new_y)\n        self.brain_widget.state[neuron_name] = 50.0\n        binary_neurons = {\"can_see_food\", \"is_eating\", \"is_sleeping\", \"is_sick\", \"is_fleeing\", \"pursuing_food\", \"is_startled\", \"external_stimulus\", \"plant_proximity\"}\n        candidates = [n for n in self.brain_widget.neuron_positions.keys() if n != orphan_name and n != neuron_name and n not in self.brain_widget.excluded_neurons and n not in binary_neurons]\n        targets = []\n        if candidates:\n            def get_dist_sq(n_name):\n                pos = self.brain_widget.neuron_positions[n_name]\n                return (pos[0] - orphan_pos[0])**2 + (pos[1] - orphan_pos[1])**2\n            candidates.sort(key=get_dist_sq)\n            targets.append(candidates.pop(0))\n            if candidates: targets.append(random.choice(candidates))\n        weight = random.uniform(0.5, 0.9)\n        if random.random() > 0.5: self.brain_widget.weights[(neuron_name, orphan_name)] = weight\n        else: self.brain_widget.weights[(orphan_name, neuron_name)] = weight\n        for target in targets:\n            w = random.uniform(-0.5, 0.8)\n            if abs(w) < 0.2: w = 0.3\n            if random.random() > 0.5: self.brain_widget.weights[(neuron_name, target)] = w\n            else: self.brain_widget.weights[(target, neuron_name)] = w\n        self._set_neuron_appearance(neuron_name, func_neuron)\n        if hasattr(self.brain_widget, 'visible_neurons'): self.brain_widget.visible_neurons.add(neuron_name)\n        self.brain_widget.neurogenesis_highlight = {'neuron': neuron_name, 'start_time': time.time(), 'duration': 8.0, 'pulse_phase': 0}\n        self.brain_widget.log_neurogenesis_event(neuron_name, \"created\", details={'trigger_type': 'connector', 'trigger_value': 1.0, 'specialization': 'orphan_rescue', 'display_name': func_neuron.display_name})\n        self._record_neurogenesis_memory(neuron_name)\n        print(f\"🔗 Connector neuron {neuron_name} created to rescue {orphan_name} (connected to closest: {targets[0] if targets else 'None'})\")\n    \n    def _set_neuron_appearance(self, name: str, func_neuron: FunctionalNeuron):\n        spec = func_neuron.specialization\n        neuron_type = func_neuron.neuron_type\n        shape_map = {'novelty': 'diamond', 'stress': 'square', 'reward': 'triangle', 'connector': 'hexagon'}\n        assigned_shape = shape_map.get(neuron_type, 'circle')\n        self.brain_widget.neuron_shapes[name] = assigned_shape\n        print(f\"🔧 SET NEURON APPEARANCE: {name} -> type={neuron_type}, shape={assigned_shape}\")\n        if neuron_type == 'connector': self.brain_widget.state_colors[name] = (50, 51, 100)\n        elif 'stress' in spec or 'anxiety' in spec: self.brain_widget.state_colors[name] = (255, 150, 150)\n        elif 'reward' in spec or 'satisfaction' in spec: self.brain_widget.state_colors[name] = (150, 255, 150)\n        elif 'investigation' in spec or 'exploration' in spec: self.brain_widget.state_colors[name] = (255, 215, 0)\n        else:\n            color_map = {'novelty': (255, 255, 150), 'stress': (255, 0, 0), 'reward': (173, 216, 230)}\n            self.brain_widget.state_colors[name] = color_map.get(neuron_type, (200, 200, 255))\n    \n    def _log_neuron_creation(self, name: str, trigger_type: str, spec: str, trigger_value: Optional[float]):\n        display_name = self.functional_neurons[name].display_name\n        self.brain_widget.log_neurogenesis_event(name, \"created\", details={'trigger_type': trigger_type, 'trigger_value': trigger_value or 0, 'specialization': spec, 'display_name': display_name})\n    \n    def _strengthen_existing_neuron(self, trigger_type: str, specialization: str):\n        prefix = f\"{trigger_type}_{specialization}\"\n        existing = [(name, neuron) for name, neuron in self.functional_neurons.items() if name.startswith(prefix)]\n        if not existing:\n            brain_neurons = [name for name in self.brain_widget.neuron_positions.keys() if name.startswith(prefix)]\n            if brain_neurons:\n                self._ensure_functional_neuron(brain_neurons[0], trigger_type, specialization)\n                if brain_neurons[0] in self.functional_neurons: existing = [(brain_neurons[0], self.functional_neurons[brain_neurons[0]])]\n        \n        # If no specific specialization match, fallback to any neuron of the same type\n        # This ensures coping mechanisms upgrade ANY stress neuron if the specific one is missing\n        if not existing and trigger_type == 'stress':\n             existing = [(name, neuron) for name, neuron in self.functional_neurons.items() if neuron.neuron_type == 'stress']\n\n        if not existing: return\n        \n        existing.sort(key=lambda x: x[1].utility_score, reverse=True)\n        best_name, best_neuron = existing[0]\n        \n        best_neuron.strength_multiplier += 0.5\n        best_neuron.utility_score += 0.1\n        self.brain_widget.communication_events[best_name] = time.time()\n        self.brain_widget.update()\n        \n        # We manually format the string to ensure the variables are injected\n        raw_msg = loc('log_strengthened_neuron', default=\"Strengthened: {name} (multiplier: {mult}x)\")\n        formatted_msg = raw_msg.format(name=best_neuron.display_name, mult=f\"{best_neuron.strength_multiplier:.1f}\")\n        print(f\"   💪 {formatted_msg}\")\n        \n        if self._on_neuron_leveled_callback:\n            try: self._on_neuron_leveled_callback(best_name, best_neuron.strength_multiplier)\n            except Exception as e: print(f\"Level callback error: {e}\")\n        self.brain_widget.update()\n    \n    def _ensure_functional_neuron(self, name: str, neuron_type: str = None, specialization: str = None) -> Optional[FunctionalNeuron]:\n        if name in self.functional_neurons: return self.functional_neurons[name]\n        if name not in self.brain_widget.neuron_positions: return None\n        if neuron_type is None:\n            if name.startswith('novelty'): neuron_type = 'novelty'\n            elif name.startswith('stress'): neuron_type = 'stress'\n            elif name.startswith('reward'): neuron_type = 'reward'\n            else: neuron_type = 'novelty'\n        ctx = ExperienceContext(trigger_type=neuron_type, active_neurons=dict(self.brain_widget.state), recent_actions=[], environmental_state={}, outcome='neutral', timestamp=time.time())\n        func_neuron = FunctionalNeuron(name, neuron_type, ctx)\n        if specialization: func_neuron.specialization = specialization\n        self.functional_neurons[name] = func_neuron\n        print(f\"   {loc('log_converted_neuron', default='Converted {name} to FunctionalNeuron', name=name)}\")\n        return func_neuron\n    \n    def ensure_all_neurons_functional(self, force_sync=False):\n        core_neurons = ['hunger', 'happiness', 'cleanliness', 'sleepiness', 'satisfaction', 'anxiety', 'curiosity']\n        excluded = getattr(self.brain_widget, 'excluded_neurons', [])\n        for name in list(self.brain_widget.neuron_positions.keys()):\n            if name in core_neurons or name in excluded: continue\n            if name not in self.functional_neurons: self._ensure_functional_neuron(name)\n        restored_positions = 0\n        restored_states = 0\n        restored_visible = 0\n        for name, fn in self.functional_neurons.items():\n            if name in core_neurons or name in excluded: continue\n            if name not in self.brain_widget.neuron_positions:\n                position = self._calculate_functional_position(fn)\n                self.brain_widget.neuron_positions[name] = position\n                restored_positions += 1\n            if name not in self.brain_widget.state:\n                self.brain_widget.state[name] = 50.0\n                restored_states += 1\n            if hasattr(self.brain_widget, 'visible_neurons'):\n                if name not in self.brain_widget.visible_neurons: restored_visible += 1\n                self.brain_widget.visible_neurons.add(name)\n            self._set_neuron_appearance(name, fn)\n            all_neurons = list(self.brain_widget.neuron_positions.keys())\n            connections = fn.get_functional_connections(all_neurons)\n            for target, weight in connections.items():\n                if (name, target) not in self.brain_widget.weights: self.brain_widget.weights[(name, target)] = weight\n        self._rebuild_new_neurons_details()\n        new_neurons_list = self.brain_widget.neurogenesis_data.setdefault('new_neurons', [])\n        restored_to_list = 0\n        for name, fn in self.functional_neurons.items():\n            if name in core_neurons or name in excluded: continue\n            if name not in new_neurons_list:\n                new_neurons_list.append(name)\n                restored_to_list += 1\n        restored_count = len([n for n in self.functional_neurons if n not in core_neurons and n not in excluded])\n        if restored_count > 0: print(f\"✅ {loc('log_sync_complete', default='Neurogenesis sync complete')}: {restored_count}\")\n    \n    def set_achievement_callbacks(self, on_created=None, on_leveled=None):\n        self._on_neuron_created_callback = on_created\n        self._on_neuron_leveled_callback = on_leveled\n\n    def get_global_cooldown_remaining(self) -> float:\n        if not self.functional_neurons: return 0.0\n        current_time = time.time()\n        last_creation = max(n.creation_context.timestamp for n in self.functional_neurons.values())\n        global_cooldown = self.config.neurogenesis.get('cooldown', 60)\n        time_since_last = current_time - last_creation\n        remaining = global_cooldown - time_since_last\n        return max(0.0, remaining)\n\n    def track_action(self, action: str):\n        self.recent_actions.append(action)\n\n    def track_state_change(self, state: dict):\n        self.last_states.append(state.copy())\n\n    def check_and_capture_experience(self, brain_state: dict, environment: dict):\n        trigger_type = self._detect_trigger_type(brain_state, environment)\n        if trigger_type:\n            self.capture_experience_context(trigger_type=trigger_type, brain_state=brain_state, recent_actions=list(self.recent_actions), environment=environment)\n    \n    def _detect_trigger_type(self, brain_state: dict, environment: dict) -> Optional[str]:\n        anxiety = brain_state.get('anxiety', 50)\n        satisfaction = brain_state.get('satisfaction', 50)\n        curiosity = brain_state.get('curiosity', 50)\n        happiness = brain_state.get('happiness', 50)\n        if anxiety > 75: return 'stress'\n        if environment.get('new_object_encountered', False) or curiosity > 70: return 'novelty'\n        if environment.get('recent_positive_outcome', False) or satisfaction > 70 or happiness > 70: return 'reward'\n        if len(self.last_states) > 0:\n            prev_anxiety = self.last_states[-1].get('anxiety', 50)\n            if prev_anxiety > 60 and anxiety < 40: return 'stress'\n        return None\n\n    def capture_experience_context(self, trigger_type: str, brain_state: dict, recent_actions: list, environment: dict) -> ExperienceContext:\n        if self._first_real_tick is None: self._first_real_tick = time.time()\n        if not isinstance(recent_actions, list): recent_actions = []\n        ctx = self._build_context(trigger_type, brain_state, environment)\n        ctx.recent_actions = recent_actions[-5:] if recent_actions else []\n        elapsed = time.time() - self._first_real_tick\n        if elapsed < 1.5: return ctx\n        is_sleeping = brain_state.get('is_sleeping', False)\n        anxiety = brain_state.get('anxiety', 50)\n        satisfaction = brain_state.get('satisfaction', 50)\n        if is_sleeping and anxiety < 15 and satisfaction > 85: return ctx\n        self.experience_buffer.add_experience(ctx)\n        return ctx\n    \n    def should_create_neuron(self, ctx: ExperienceContext) -> bool:\n        current_time = time.time()\n        if ctx.trigger_type == 'stress' and ctx.active_neurons.get('anxiety', 50) >= 95:\n            print(f\"🚨 {loc('log_emergency_forcing_neuron', default='EMERGENCY: Critical anxiety - forcing stress neuron!')}\")\n            return True\n        if self._first_real_tick is None: return False\n        elapsed = current_time - self._first_real_tick\n        if elapsed < 5.0: return False\n        pattern = ctx.get_pattern_signature()\n        if len(pattern.split('_')) < 4: return False\n        if self.experience_buffer.pattern_counts.get(pattern, 0) > 20: return False\n        current_count = len(self.brain_widget.neuron_positions)\n        max_neurons = self.config.neurogenesis.get('max_neurons', 32)\n        if current_count >= max_neurons: return False\n        global_cooldown = self.config.neurogenesis.get('cooldown', 60)\n        default_time = self._first_real_tick or time.time()\n        last_creation = max((n.creation_context.timestamp for n in self.functional_neurons.values()), default=default_time)\n        if current_time - last_creation < global_cooldown: return False\n        pattern_level, count, _ = self.experience_buffer.get_pattern_recurrence(ctx)\n        thresholds = {'specific': 2, 'parent': 3, 'core': 5}\n        if count < thresholds.get(pattern_level, 2): return False\n        return True\n    \n    def update_neuron_activations(self, brain_state: Dict[str, float]) -> None:\n            # 1. Normalize inputs\n            for key in list(brain_state.keys()):\n                if not isinstance(brain_state[key], (int, float)):\n                    try: brain_state[key] = float(brain_state[key])\n                    except (ValueError, TypeError): brain_state[key] = 50.0\n\n            # 2. Standard Neural Activation\n            for name, func_neuron in self.functional_neurons.items():\n                if name in self.brain_widget.state:\n                    activation = func_neuron.calculate_activation(brain_state, self.brain_widget.weights)\n                    self.brain_widget.state[name] = activation\n\n            # 3. === SCALED STRESS REDUCTION MECHANIC ===\n            # Each stress neuron contributes to anxiety suppression, with scaling based on count\n            if 'anxiety' in brain_state:\n                current_anxiety = brain_state['anxiety']\n                stress_count = self._get_stress_neuron_count()\n                anxiety_cap = self._get_anxiety_cap()\n                \n                # First, enforce the cap - anxiety should never exceed the cap\n                if current_anxiety > anxiety_cap:\n                    current_anxiety = anxiety_cap\n                    brain_state['anxiety'] = anxiety_cap\n                    self.brain_widget.state['anxiety'] = anxiety_cap\n                \n                total_reduction = 0.0\n                \n                for name, fn in self.functional_neurons.items():\n                    if getattr(fn, 'neuron_type', '') == 'stress':\n                        multiplier = getattr(fn, 'strength_multiplier', 1.0)\n                        \n                        # Force stress neuron to activate proportionally to anxiety\n                        driven = 50.0 + (current_anxiety - 50.0) * 1.2\n                        driven = max(50.0, min(100.0, driven))\n                        \n                        current_val = self.brain_widget.state.get(name, 50.0)\n                        final_val = max(current_val, driven)\n                        self.brain_widget.state[name] = final_val\n                        \n                        if final_val > 55:\n                            infl = (final_val - 50.0) / 50.0 \n                            # Base force per neuron, scaled by strength multiplier\n                            reduction_force = 25.0 * multiplier * infl\n                            total_reduction += reduction_force\n\n                if total_reduction > 0:\n                    # Scale reduction based on stress neuron count (cumulative resilience)\n                    # More neurons = faster anxiety reduction\n                    count_multiplier = 1.0 + (0.3 * stress_count)  # 1.0 to 2.5x\n                    total_reduction *= count_multiplier\n                    \n                    # Crisis response multipliers\n                    if current_anxiety > 80:\n                        total_reduction *= 3.0  # Strong crisis response\n                    elif current_anxiety > 60:\n                        total_reduction *= 1.5\n                    \n                    # Apply reduction with timestep factor\n                    reduction_amount = total_reduction * 0.3\n                    new_anxiety = max(0.0, current_anxiety - reduction_amount)\n                    \n                    # Enforce cap again after reduction\n                    new_anxiety = min(new_anxiety, anxiety_cap)\n                    \n                    # Apply to brain state dictionaries\n                    brain_state['anxiety'] = new_anxiety\n                    self.brain_widget.state['anxiety'] = new_anxiety\n                    \n                    # CRITICAL: Also apply to the actual squid!\n                    if (hasattr(self.brain_widget, 'tamagotchi_logic') and \n                        self.brain_widget.tamagotchi_logic and\n                        hasattr(self.brain_widget.tamagotchi_logic, 'squid') and\n                        self.brain_widget.tamagotchi_logic.squid):\n                        squid = self.brain_widget.tamagotchi_logic.squid\n                        \n                        # Also enforce cap on squid's current anxiety before we compare\n                        if squid.anxiety > anxiety_cap:\n                            squid.anxiety = anxiety_cap\n                        \n                        old_squid_anxiety = squid.anxiety\n                        squid.anxiety = new_anxiety\n                        \n                        if reduction_amount > 1.0:  # Only log significant reductions\n                            print(f\"   🧘 Stress suppression ({stress_count} neurons): \"\n                                  f\"{old_squid_anxiety:.1f} → {new_anxiety:.1f} \"\n                                  f\"(-{reduction_amount:.1f}, cap: {anxiety_cap:.0f})\")\n\n            # 4. Visualizations\n            if not getattr(self.brain_widget, 'animations_enabled', True): return\n            if not hasattr(self.brain_widget, 'trigger_activation_pulse'): return\n\n            now = time.time()\n            for (src, dst), weight in self.brain_widget.weights.items():\n                if abs(weight) < 0.15: continue\n                seed = hash((src, dst)) % 1_000_000 / 1_000_000.0\n                excite = seed * 0.85 + 0.15\n                if excite < 0.22: continue\n                skip = int(3 + seed * 9)\n                if int(now * 60) % skip != 0: continue\n\n                src_act = brain_state.get(src, 50.0)\n                influence = (src_act - 50.0) * weight\n                if abs(influence) < 6.0: continue\n\n                if influence > 0:\n                    base_hue = 65 + seed * 25; sat = 70 + seed * 40; val = 120 + seed * 30\n                else:\n                    base_hue = 5 + seed * 20; sat = 75 + seed * 35; val = 115 + seed * 30\n\n                rgb = self._hsv_to_rgb(base_hue, sat, val)\n                alpha = 80 + int(seed * 60)\n                colour = (*rgb, alpha)\n                duration = 1.5 + seed * 1.5\n                speed = 0.3 + seed * 0.3\n\n                self.brain_widget.weight_animations.append({\n                    'pair': (src, dst), 'start_time': now, 'duration': duration,\n                    'start_weight': weight, 'end_weight': weight,\n                    'neuron1': src, 'neuron2': dst, 'color': colour, 'pulse_speed': speed\n                })\n\n    def _hsv_to_rgb(self, h, s, v):\n        s, v = s / 255.0, v / 255.0\n        c = v * s\n        x = c * (1 - abs((h / 60.0) % 2 - 1))\n        m = v - c\n        if h < 60: r, g, b = c, x, 0\n        elif h <120: r, g, b = x, c, 0\n        elif h <180: r, g, b = 0, c, x\n        elif h <240: r, g, b = 0, x, c\n        elif h <300: r, g, b = x, 0, c\n        else: r, g, b = c, 0, x\n        return int((r + m) * 255), int((g + m) * 255), int((b + m) * 255)\n    \n    def intelligent_pruning(self) -> Optional[str]:\n        candidates = []\n        for name, func_neuron in self.functional_neurons.items():\n            if func_neuron.neuron_type == 'connector': continue\n            if time.time() - func_neuron.creation_context.timestamp < 300: continue\n            score = 0.0\n            score += func_neuron.utility_score * 0.4\n            recency = time.time() - func_neuron.last_activated\n            if recency < 300: score += 0.3\n            elif recency < 1800: score += 0.15\n            similar_count = sum(1 for n in self.functional_neurons.values() if n.specialization == func_neuron.specialization)\n            if similar_count == 1: score += 0.3\n            total_strength = sum(abs(w) for (a, b), w in self.brain_widget.weights.items() if a == name or b == name)\n            score += min(total_strength / 5.0, 0.3)\n            candidates.append((name, score))\n        if not candidates: return None\n        candidates.sort(key=lambda x: x[1])\n        neuron_to_prune = candidates[0][0]\n        if neuron_to_prune in self.brain_widget.neuron_positions: del self.brain_widget.neuron_positions[neuron_to_prune]\n        if neuron_to_prune in self.brain_widget.state: del self.brain_widget.state[neuron_to_prune]\n        for conn in list(self.brain_widget.weights.keys()):\n            if neuron_to_prune in conn: del self.brain_widget.weights[conn]\n        if neuron_to_prune in self.functional_neurons:\n            fn = self.functional_neurons[neuron_to_prune]\n            if fn.neuron_type == 'novelty': self.novelty_neuron_count -= 1\n            del self.functional_neurons[neuron_to_prune]\n        print(f\"🗑️ {loc('log_pruned', default='Pruned')}: {neuron_to_prune}\")\n        return neuron_to_prune\n    \n    def to_dict(self) -> dict:\n        return {\n            'functional_neurons': {name: neuron.to_dict() for name, neuron in self.functional_neurons.items()},\n            'experience_buffer': self.experience_buffer.to_dict(),\n            'novelty_neuron_count': self.novelty_neuron_count,\n            'neurons_created_this_session': self.neurons_created_this_session,\n            'last_creation_by_type': self.last_creation_by_type.copy(),\n            'awarded_neurons': list(self._awarded_neurons)\n        }\n    \n    def from_dict(self, data: dict):\n        self.functional_neurons = {}\n        for name, neuron_data in data.get('functional_neurons', {}).items():\n            self.functional_neurons[name] = FunctionalNeuron.from_dict(neuron_data)\n        if 'experience_buffer' in data:\n            self.experience_buffer = ExperienceBuffer.from_dict(data['experience_buffer'])\n        self.novelty_neuron_count = data.get('novelty_neuron_count', 0)\n        self.neurons_created_this_session = data.get('neurons_created_this_session', 0)\n        self.last_creation_by_type = data.get('last_creation_by_type', {'novelty': 0, 'stress': 0, 'reward': 0})\n        self._awarded_neurons = set(data.get('awarded_neurons', []))\n        self.ensure_all_neurons_functional()\n        self._rebuild_new_neurons_details_for_lab()\n    \n    def reset_state(self):\n        self.functional_neurons.clear()\n        self.experience_buffer = ExperienceBuffer()\n        self.novelty_neuron_count = 0\n        self.neurons_created_this_session = 0\n        self.last_creation_by_type = {'novelty': 0, 'stress': 0, 'reward': 0}\n        self._awarded_neurons.clear()\n        self._first_real_tick = None\n        print(\"🔄 Neurogenesis state reset\")\n\nclass NeurogenesisTriggerSystem:\n    def __init__(self, tamagotchi_logic):\n        self.logic = tamagotchi_logic\n        self.recent_actions = deque(maxlen=10)\n        self.last_states = deque(maxlen=5)\n        \n    def track_action(self, action: str):\n        self.recent_actions.append(action)\n    \n    def track_state_change(self, state: Dict[str, float]):\n        self.last_states.append(state.copy())\n    \n    def check_for_significant_experience(self) -> Optional[Tuple[str, ExperienceContext]]:\n        if len(self.last_states) < 2: return None\n        current = self.last_states[-1]\n        previous = self.last_states[-2]\n        state_change = 0\n        for key in ['hunger', 'happiness', 'satisfaction', 'anxiety', 'curiosity']:\n            if key in current and key in previous:\n                state_change += abs(current.get(key, 50) - previous.get(key, 50))\n        if state_change < 5 and not getattr(self.logic, 'new_object_encountered', False): return None\n        if self._detect_novelty_experience(current, previous):\n            context = self._build_context('novelty', current)\n            return ('novelty', context)\n        if self._detect_stress_experience(current, previous):\n            context = self._build_context('stress', current)\n            return ('stress', context)\n        if self._detect_reward_experience(current, previous):\n            context = self._build_context('reward', current)\n            return ('reward', context)\n        return None\n    \n    def _detect_novelty_experience(self, current, previous) -> bool:\n        if current.get('is_sleeping', False): return False\n        current_curiosity = current.get('curiosity', 50)\n        previous_curiosity = previous.get('curiosity', 50)\n        curiosity_delta = current_curiosity - previous_curiosity\n        if current_curiosity >= 99 or current_curiosity <= 1: return False\n        new_object = getattr(self.logic, 'new_object_encountered', False)\n        meaningful_spike = curiosity_delta > 15 and current_curiosity > 40\n        return meaningful_spike or new_object\n    \n    def _detect_stress_experience(self, current, previous) -> bool:\n        if current.get('is_sleeping', False) and current.get('anxiety', 50) < 40: return False\n        current_anxiety = current.get('anxiety', 50)\n        previous_anxiety = previous.get('anxiety', 50)\n        current_hunger = current.get('hunger', 50)\n        if current_anxiety <= 5: return False\n        anxiety_high = current_anxiety > 65 and previous_anxiety > 60\n        anxiety_spike = (current_anxiety - previous_anxiety) > 15 and current_anxiety > 50\n        hunger_crisis = current_hunger > 85 and (current_hunger - previous.get('hunger', 50)) >= 0\n        return anxiety_high or anxiety_spike or hunger_crisis\n    \n    def _detect_reward_experience(self, current, previous) -> bool:\n        if current.get('is_sleeping', False):\n            happiness = current.get('happiness', 50)\n            satisfaction = current.get('satisfaction', 50)\n            if happiness >= 90 and satisfaction >= 90: return False\n        current_happiness = current.get('happiness', 50)\n        previous_happiness = previous.get('happiness', 50)\n        current_satisfaction = current.get('satisfaction', 50)\n        previous_satisfaction = previous.get('satisfaction', 50)\n        happiness_delta = current_happiness - previous_happiness\n        satisfaction_delta = current_satisfaction - previous_satisfaction\n        if (current_happiness >= 99 and previous_happiness >= 99) or \\\n           (current_satisfaction >= 99 and previous_satisfaction >= 99): return False\n        if current_happiness >= 95 and happiness_delta < 3: return False\n        if current_satisfaction >= 95 and satisfaction_delta < 3: return False\n        positive_outcome = getattr(self.logic, 'recent_positive_outcome', False)\n        significant_happiness = happiness_delta > 15 and current_happiness > 40\n        significant_satisfaction = satisfaction_delta > 15 and current_satisfaction > 40\n        return significant_happiness or significant_satisfaction or positive_outcome\n    \n    def _build_context(self, trigger_type: str, current_state: Dict) -> ExperienceContext:\n        filtered_neurons = {k: v for k, v in current_state.items() \n                        if not k.startswith('is_') and k not in [\n                            'novelty_exposure', 'sustained_stress', 'recent_rewards', \n                            'neurogenesis_active', 'personality', 'pursuing_food', 'direction',  \n                            'is_startled', 'is_fleeing', 'is_sick']}\n        return ExperienceContext(\n            trigger_type=trigger_type,\n            active_neurons=filtered_neurons,\n            recent_actions=list(self.recent_actions),\n            environmental_state={\n                'food_count': len(self.logic.food_items),\n                'poop_count': len(self.logic.poop_items),\n                'is_sick': self.logic.squid.is_sick,\n                'is_eating': current_state.get('is_eating', False),\n                'has_rock': hasattr(self.logic, 'rock_items') and len(self.logic.rock_items) > 0\n            },\n            outcome='neutral',\n            timestamp=time.time()\n        )\n"
  },
  {
    "path": "src/neurogenesis_show.py",
    "content": "\"\"\"\r\nShowmanNeurogenesis v2.5.0\r\n\r\nWrapper around EnhancedNeurogenesis that keeps all the *real* logic\r\nbut guarantees the player *sees* a neuron when something cool happens.\r\n\r\n\"\"\"\r\n\r\nimport time\r\nimport random\r\nfrom typing import Optional, Callable, Dict, Any\r\nfrom enum import Enum\r\n\r\n\r\nclass NeurogenesisEvent(Enum):\r\n    \"\"\"Semantic events that can trigger neurogenesis for dramatic effect\"\"\"\r\n    FIRST_FEEDING = \"first_feeding\"\r\n    FIRST_ROCK = \"first_rock\"\r\n    FIRST_SLEEP = \"first_sleep\"\r\n    ANXIETY_SPIKE = \"anxiety_spike\"\r\n    ANXIETY_RELIEF = \"anxiety_relief\"\r\n    NOVEL_OBJECT = \"novel_object\"\r\n    RECOVERED_FROM_ILLNESS = \"recovered\"\r\n    MAX_HAPPINESS = \"max_happiness\"\r\n    HUNGER_CRISIS = \"hunger_crisis\"\r\n    HUNGER_SATISFIED = \"hunger_satisfied\"\r\n    SPOTLESS_TANK = \"spotless_tank\"\r\n    CURIOSITY_EXPLOSION = \"curiosity_explosion\"\r\n    FIRST_DECORATION_PUSH = \"first_decoration_push\"\r\n\r\n\r\nclass ShowmanNeurogenesis:\r\n    \"\"\"\r\n    Wrapper around EnhancedNeurogenesis that keeps all the *real* logic\r\n    but guarantees the player *sees* a neuron when something cool happens.\r\n    \r\n    The showmanship feature can be disabled via config.ini [Neurogenesis] showmanship = False\r\n    When disabled, this wrapper becomes a pure passthrough to the real engine.\r\n    \"\"\"\r\n\r\n    def __init__(self, real_enhanced_neurogenesis):\r\n        self.en = real_enhanced_neurogenesis          # the real system\r\n        self.config = self.en.config\r\n        self.last_showman_creation = 0                # our own cooldown\r\n        self.showman_cooldown = 15.0                  # seconds between \"show\" neurons\r\n        \r\n        # Check if showmanship is enabled in config\r\n        self._showmanship_enabled = self._check_showmanship_enabled()\r\n        \r\n        # Track events we've already triggered (for first-time events)\r\n        self._triggered_events: set = set()\r\n        \r\n        # Achievement integration callbacks\r\n        self._on_dramatic_neuron_callback: Optional[Callable] = None\r\n        self._on_event_triggered_callback: Optional[Callable[[NeurogenesisEvent], None]] = None\r\n        \r\n        # Name pools for dramatic neuron naming\r\n        self.name_pool = {\r\n            'novelty': [\r\n                \"encountered_novel_object\",         # saw something new\r\n                \"exploration_reward\",               # payoff for investigating\r\n                \"novel_positive_experience\",  # moves toward new thing\r\n                \"wonder_trigger\",                   # pure \"what's that?\" spark\r\n                \"discovery_satisfaction\"            # aha-feeling after exploring\r\n            ],\r\n            'stress': [\r\n                \"anxiety_reduction\",\r\n                \"trauma\",\r\n                \"extreme_stress\",\r\n                \"threat_avoidance\",\r\n                \"comfort_seeking\"\r\n            ],\r\n            'reward': [\r\n                \"food_reward\",                # payoff for eating\r\n                \"cleanliness_reward\",         # payoff for getting clean\r\n                \"rest_reward\",                # payoff for sleeping\r\n                \"play_reward\",                # payoff for fun interactions\r\n                \"achievement_reward\"          # payoff for completing any goal\r\n            ]\r\n        }\r\n        \r\n        # Extended name pools for more variety\r\n        self.extended_name_pool = {\r\n            'novelty': [\r\n                \"positive_experience\", \"investigation_drive\", \"new_experience\",\r\n                \"wonder_neuron\", \"exploration_joy\", \"discovery_rush\"\r\n            ],\r\n            'stress': [\r\n                \"calm_restore\", \"fear_dampener\", \"safety_signal\",\r\n                \"relaxation_trigger\", \"peace_seeker\", \"tension_release\"\r\n            ],\r\n            'reward': [\r\n                \"satisfaction_burst\", \"happiness_boost\", \"contentment_signal\",\r\n                \"pleasure_response\", \"joy_neuron\", \"fulfillment_marker\"\r\n            ]\r\n        }\r\n\r\n\r\n    def _check_showmanship_enabled(self) -> bool:\r\n        \"\"\"Check if showmanship is enabled in config. Config is single source of truth.\"\"\"\r\n        # Try to get from config object\r\n        if hasattr(self.config, 'neurogenesis'):\r\n            ng_config = self.config.neurogenesis\r\n            if isinstance(ng_config, dict):\r\n                return ng_config.get('showmanship', True)\r\n        \r\n        # Try config manager style access\r\n        if hasattr(self.config, 'get_showmanship_enabled'):\r\n            return self.config.get_showmanship_enabled()\r\n        \r\n        # Try direct config access\r\n        if hasattr(self.config, 'getboolean'):\r\n            try:\r\n                return self.config.getboolean('Neurogenesis', 'showmanship', fallback=True)\r\n            except:\r\n                pass\r\n        \r\n        # Default to enabled\r\n        return True\r\n\r\n    def is_showmanship_enabled(self) -> bool:\r\n        \"\"\"Public getter for showmanship state. Re-checks config each time.\"\"\"\r\n        return self._check_showmanship_enabled()\r\n\r\n    def set_callbacks(self, on_dramatic_neuron=None, on_event_triggered=None):\r\n        \"\"\"Set callbacks for external integration (achievements, etc.)\"\"\"\r\n        self._on_dramatic_neuron_callback = on_dramatic_neuron\r\n        self._on_event_triggered_callback = on_event_triggered\r\n\r\n    # -------------- public API – same as EnhancedNeurogenesis --------------\r\n\r\n    def get_global_cooldown_remaining(self) -> float:\r\n        \"\"\"\r\n        Return the REAL cooldown from the underlying EnhancedNeurogenesis system.\r\n        This matches the cooldown shown in console and actually blocks creation.\r\n        \"\"\"\r\n        return self.en.get_global_cooldown_remaining()\r\n\r\n    def capture_experience_context(self, trigger, state, actions, env):\r\n        \"\"\"Delegate to real system\"\"\"\r\n        return self.en.capture_experience_context(trigger, state, actions, env)\r\n\r\n    def should_create_neuron(self, ctx) -> bool:\r\n        \"\"\"\r\n        Showman wrapper: decide whether a neuron should be created.\r\n        Emergency anxiety ≥ 100 bypasses EVERYTHING and returns True instantly.\r\n        Otherwise delegates to the real EnhancedNeurogenesis logic.\r\n        \"\"\"\r\n        # =====================================================================\r\n        # EMERGENCY BYPASS: anxiety == 100  →  create immediately\r\n        if ctx.trigger_type == 'stress' and ctx.active_neurons.get('anxiety', 50) >= 95:\r\n            print(f\"🚨 EMERGENCY OVERRIDE: anxiety={ctx.active_neurons.get('anxiety')} - creating stress neuron NOW!\")\r\n            # (Optionally lift specialization cap for this single creation)\r\n            original_max = self.en.config.neurogenesis.get('max_per_specialization', 3)\r\n            self.en.config.neurogenesis['max_per_specialization'] = original_max + 1\r\n            # Restore cap after creation (done in create_functional_neuron)\r\n            return True\r\n        # =====================================================================\r\n\r\n        # Normal path – use real system (respects cooldowns, caps, patterns)\r\n        return self.en.should_create_neuron(ctx)\r\n\r\n    def create_functional_neuron(self, ctx, is_emergency: bool = False) -> Optional[str]:\r\n        \"\"\"Create the neuron through the real system\"\"\"\r\n        \r\n        # Ask real system to do the heavy lifting\r\n        real_name = self.en.create_functional_neuron(ctx, is_emergency=is_emergency)\r\n        \r\n        if real_name is None:\r\n            return None\r\n\r\n        # If showmanship disabled, return the real name without cosmetic changes\r\n        if not self.is_showmanship_enabled():\r\n            return real_name\r\n\r\n        # ---- cosmetic upgrade (showmanship enabled) ----\r\n        pretty_name = self._rename_for_drama(ctx.trigger_type, real_name)\r\n        if pretty_name != real_name:\r\n            # Migrate everything to the new name\r\n            self._migrate_neuron(real_name, pretty_name)\r\n\r\n        # Bigger, longer highlight for the player\r\n        burst_color = self._burst_color(ctx.trigger_type)\r\n        self.en.brain_widget.neurogenesis_highlight = {\r\n            'neuron': pretty_name,\r\n            'start_time': time.time(),\r\n            'duration': 10.0,              # longer pulse\r\n            'pulse_phase': 0,\r\n            'is_emergency': False,\r\n            'is_showman': True,            # flag for renderer (optional)\r\n            'color_burst': burst_color\r\n        }\r\n        self.last_showman_creation = time.time()\r\n        \r\n        # Fire callback for achievement integration\r\n        if self._on_dramatic_neuron_callback:\r\n            try:\r\n                self._on_dramatic_neuron_callback(pretty_name, ctx.trigger_type)\r\n            except Exception as e:\r\n                print(f\"Showman callback error: {e}\")\r\n        \r\n        return pretty_name\r\n\r\n    # ------------------ Event Detection ------------------\r\n    \r\n    def _detect_dramatic_moment(self, ctx) -> Optional[NeurogenesisEvent]:\r\n        \"\"\"Detect visually significant moments - VERY PERMISSIVE\"\"\"\r\n        recent = ' '.join(ctx.recent_actions[-3:]).lower() if ctx.recent_actions else ''\r\n        \r\n        # ANY anxiety spike over 70 (not 90)\r\n        if ctx.active_neurons.get('anxiety', 50) >= 70:\r\n            return NeurogenesisEvent.ANXIETY_SPIKE\r\n        \r\n        # ANY feeding moment (not just after crisis)\r\n        if ctx.trigger_type == 'reward' and ('eat' in recent or 'food' in recent):\r\n            return NeurogenesisEvent.HUNGER_SATISFIED\r\n        \r\n        # ANY curiosity over 75 (not 95)\r\n        if ctx.active_neurons.get('curiosity', 50) >= 75:\r\n            return NeurogenesisEvent.CURIOSITY_EXPLOSION\r\n        \r\n        # Clean tank (allow multiple)\r\n        if ctx.active_neurons.get('cleanliness', 50) >= 90:\r\n            return NeurogenesisEvent.SPOTLESS_TANK\r\n        \r\n        # High happiness (allow multiple)\r\n        if ctx.active_neurons.get('happiness', 50) >= 85:\r\n            return NeurogenesisEvent.MAX_HAPPINESS\r\n        \r\n        # ANY decoration interaction\r\n        if 'decoration' in recent or 'push' in recent:\r\n            return NeurogenesisEvent.FIRST_DECORATION_PUSH\r\n        \r\n        return None\r\n    \r\n    def _fire_event(self, event: NeurogenesisEvent):\r\n        \"\"\"Record event and fire callback\"\"\"\r\n        self._triggered_events.add(event)\r\n        \r\n        if self._on_event_triggered_callback:\r\n            try:\r\n                self._on_event_triggered_callback(event)\r\n            except Exception as e:\r\n                print(f\"Event callback error: {e}\")\r\n\r\n    def trigger_event(self, event: NeurogenesisEvent) -> bool:\r\n        \"\"\"\r\n        Manually trigger a dramatic event from external code.\r\n        Useful for achievement system integration.\r\n        Returns True if a neuron was created.\r\n        \r\n        Note: Respects showmanship config flag - returns False if disabled.\r\n        \"\"\"\r\n        # Check if showmanship is enabled\r\n        if not self.is_showmanship_enabled():\r\n            return False\r\n        \r\n        if event in self._triggered_events:\r\n            return False  # Already triggered\r\n        \r\n        now = time.time()\r\n        if now - self.last_showman_creation < self.showman_cooldown:\r\n            return False\r\n        \r\n        # Map events to trigger types\r\n        event_to_trigger = {\r\n            NeurogenesisEvent.FIRST_FEEDING: 'reward',\r\n            NeurogenesisEvent.FIRST_ROCK: 'novelty',\r\n            NeurogenesisEvent.FIRST_SLEEP: 'reward',\r\n            NeurogenesisEvent.ANXIETY_SPIKE: 'stress',\r\n            NeurogenesisEvent.ANXIETY_RELIEF: 'stress',\r\n            NeurogenesisEvent.NOVEL_OBJECT: 'novelty',\r\n            NeurogenesisEvent.RECOVERED_FROM_ILLNESS: 'reward',\r\n            NeurogenesisEvent.MAX_HAPPINESS: 'reward',\r\n            NeurogenesisEvent.HUNGER_CRISIS: 'stress',\r\n            NeurogenesisEvent.HUNGER_SATISFIED: 'reward',\r\n            NeurogenesisEvent.SPOTLESS_TANK: 'reward',\r\n            NeurogenesisEvent.CURIOSITY_EXPLOSION: 'novelty',\r\n            NeurogenesisEvent.FIRST_DECORATION_PUSH: 'novelty',\r\n        }\r\n        \r\n        trigger_type = event_to_trigger.get(event, 'novelty')\r\n        \r\n        # Create a minimal context\r\n        from .neurogenesis import ExperienceContext\r\n        ctx = ExperienceContext(\r\n            trigger_type=trigger_type,\r\n            active_neurons=dict(self.en.brain_widget.state),\r\n            recent_actions=[event.value],\r\n            environmental_state={},\r\n            outcome='positive',\r\n            timestamp=now\r\n        )\r\n        \r\n        print(f\"🎭 Manual event trigger: {event.value}\")\r\n        self._fire_event(event)\r\n        \r\n        # Create the neuron\r\n        result = self.create_functional_neuron(ctx)\r\n        return result is not None\r\n\r\n    # ------------------ internal helpers ------------------\r\n\r\n    def _moment_deserves_neuron(self, ctx) -> bool:\r\n        \"\"\"\r\n        Return True if this is a *visually* cool moment for the player.\r\n        Keep the rules simple and transparent.\r\n        DEPRECATED: Use _detect_dramatic_moment instead.\r\n        \"\"\"\r\n        return self._detect_dramatic_moment(ctx) is not None\r\n\r\n    def _rename_for_drama(self, trigger: str, old: str) -> str:\r\n        \"\"\"Pick a cinematic name that still encodes the specialization.\"\"\"\r\n        pool = self.name_pool.get(trigger, [])\r\n        extended = self.extended_name_pool.get(trigger, [])\r\n        all_names = pool + extended\r\n        \r\n        if not all_names:\r\n            return old\r\n        \r\n        # Keep pool unique per session\r\n        used = {n for n in self.en.functional_neurons.keys() if n in all_names}\r\n        available = [n for n in all_names if n not in used]\r\n        \r\n        if available:\r\n            return available[0]\r\n        \r\n        # Fallback – append digit so we never duplicate\r\n        root = pool[0] if pool else old.split('_')[0]\r\n        counter = 2\r\n        while f\"{root}_{counter}\" in self.en.functional_neurons:\r\n            counter += 1\r\n        return f\"{root}_{counter}\"\r\n\r\n    def _migrate_neuron(self, old_name: str, new_name: str):\r\n        bw = self.en.brain_widget\r\n\r\n        # Positions\r\n        if old_name in bw.neuron_positions:\r\n            bw.neuron_positions[new_name] = bw.neuron_positions.pop(old_name)\r\n        \r\n        # State\r\n        if old_name in bw.state:\r\n            bw.state[new_name] = bw.state.pop(old_name)\r\n        \r\n        # Colours\r\n        if hasattr(bw, 'state_colors') and old_name in bw.state_colors:\r\n            bw.state_colors[new_name] = bw.state_colors.pop(old_name)\r\n        \r\n        # Shapes\r\n        if hasattr(bw, 'neuron_shapes') and old_name in bw.neuron_shapes:\r\n            bw.neuron_shapes[new_name] = bw.neuron_shapes.pop(old_name)\r\n\r\n        # Weights – rebuild keys\r\n        new_weights = {}\r\n        for (src, dst), w in bw.weights.items():\r\n            src = new_name if src == old_name else src\r\n            dst = new_name if dst == old_name else dst\r\n            new_weights[(src, dst)] = w\r\n        bw.weights = new_weights\r\n\r\n        # Functional neurons dict\r\n        if old_name in self.en.functional_neurons:\r\n            self.en.functional_neurons[new_name] = self.en.functional_neurons.pop(old_name)\r\n            self.en.functional_neurons[new_name].name = new_name\r\n        \r\n        # FIX: Update visible_neurons set\r\n        if hasattr(bw, 'visible_neurons'):\r\n            if old_name in bw.visible_neurons:\r\n                bw.visible_neurons.discard(old_name)\r\n                bw.visible_neurons.add(new_name)\r\n        \r\n        # FIX: Update neuron_reveal_animations\r\n        if hasattr(bw, 'neuron_reveal_animations'):\r\n            if old_name in bw.neuron_reveal_animations:\r\n                bw.neuron_reveal_animations[new_name] = bw.neuron_reveal_animations.pop(old_name)\r\n        \r\n        # FIX: Update communication_events\r\n        if hasattr(bw, 'communication_events'):\r\n            if old_name in bw.communication_events:\r\n                bw.communication_events[new_name] = bw.communication_events.pop(old_name)\r\n        \r\n        # FIX: Update weight_change_events\r\n        if hasattr(bw, 'weight_change_events'):\r\n            new_weight_events = {}\r\n            for key, event_time in bw.weight_change_events.items():\r\n                if \"->\" in key:\r\n                    src, dst = key.split(\"->\", 1)\r\n                else:\r\n                    # Fallback in case of malformed key\r\n                    continue\r\n                src = new_name if src == old_name else src\r\n                dst = new_name if dst == old_name else dst\r\n                new_key = f\"{src}->{dst}\"\r\n                new_weight_events[new_key] = event_time\r\n            bw.weight_change_events = new_weight_events\r\n        \r\n        # FIX: Update link animation dicts\r\n        for attr in ['_link_opacities', '_link_targets', '_link_start_times', '_link_fade_speeds']:\r\n            if hasattr(bw, attr):\r\n                old_dict = getattr(bw, attr)\r\n                new_dict = {}\r\n                for key, val in old_dict.items():\r\n                    if isinstance(key, tuple) and len(key) == 2:\r\n                        src, dst = key\r\n                        src = new_name if src == old_name else src\r\n                        dst = new_name if dst == old_name else dst\r\n                        new_dict[(src, dst)] = val\r\n                    else:\r\n                        # Skip malformed keys\r\n                        continue\r\n                setattr(bw, attr, new_dict)\r\n\r\n    def _burst_color(self, trigger: str) -> tuple:\r\n        \"\"\"Bright colour flash for the neuron birth.\"\"\"\r\n        base_colors = {\r\n            'novelty': (255, 215, 0),   # gold\r\n            'stress':  (255, 100, 100), # crimson\r\n            'reward':  (100, 255, 100)  # mint\r\n        }\r\n        \r\n        # Add some variation\r\n        base = base_colors.get(trigger, (200, 200, 255))\r\n        \r\n        # Slight random variation for visual interest\r\n        r = max(0, min(255, base[0] + random.randint(-20, 20)))\r\n        g = max(0, min(255, base[1] + random.randint(-20, 20)))\r\n        b = max(0, min(255, base[2] + random.randint(-20, 20)))\r\n        \r\n        return (r, g, b)\r\n\r\n    def get_triggered_events(self) -> set:\r\n        \"\"\"Return set of events that have been triggered\"\"\"\r\n        return self._triggered_events.copy()\r\n\r\n    def reset_events(self):\r\n        \"\"\"Reset triggered events for new game\"\"\"\r\n        self._triggered_events.clear()\r\n        self.last_showman_creation = 0\r\n\r\n    # -------------- passthrough anything we don't override --------------\r\n    def __getattr__(self, item):\r\n        return getattr(self.en, item)\r\n"
  },
  {
    "path": "src/personality.py",
    "content": "from enum import Enum \r\n\r\nclass Personality(Enum):\r\n    TIMID = \"timid\"\r\n    ADVENTUROUS = \"adventurous\"\r\n    LAZY = \"lazy\"\r\n    ENERGETIC = \"energetic\"\r\n    INTROVERT = \"introvert\"\r\n    GREEDY = \"greedy\"\r\n    STUBBORN = \"stubborn\""
  },
  {
    "path": "src/personality_traits.py",
    "content": "from enum import Enum\r\nfrom .personality import Personality\r\n\r\npersonality_traits = {}\r\n\r\ndef register_personality(name, decision_function, attribute_modifiers):\r\n    # Base goal-oriented modifiers\r\n    base_modifiers = {\r\n        \"organization_urgency\": 1.0,\r\n        \"rock_interaction_chance\": 1.0,\r\n        \"plant_seeking_urgency\": 1.0,\r\n        \"goal_persistence\": 1.0\r\n    }\r\n    \r\n    # Personality-specific overrides\r\n\r\n    \r\n    if name == Personality.ADVENTUROUS:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 1.8,\r\n            \"rock_interaction_chance\": 2.2,\r\n            \"goal_persistence\": 1.5,\r\n            \"exploration_bonus\": 1.3\r\n        })\r\n    elif name == Personality.TIMID:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 0.6,\r\n            \"rock_interaction_chance\": 0.4,\r\n            \"plant_seeking_urgency\": 1.2,  # Timid squids prefer plants\r\n            \"goal_persistence\": 0.7\r\n        })\r\n    elif name == Personality.LAZY:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 0.3,\r\n            \"rock_interaction_chance\": 0.8,\r\n            \"goal_persistence\": 0.5\r\n        })\r\n    elif name == Personality.ENERGETIC:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 1.5,\r\n            \"rock_interaction_chance\": 1.7,\r\n            \"goal_persistence\": 1.8\r\n        })\r\n    elif name == Personality.GREEDY:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 0.9,\r\n            \"rock_interaction_chance\": 1.1,\r\n            \"food_seeking_priority\": 2.0  # Overrides other goals when hungry\r\n        })\r\n    elif name == Personality.STUBBORN:\r\n        base_modifiers.update({\r\n            \"organization_urgency\": 1.2,\r\n            \"rock_interaction_chance\": 0.3,\r\n            \"goal_persistence\": 2.0  # Very persistent once committed\r\n        })\r\n    \r\n    # Combine with passed modifiers\r\n    attribute_modifiers.update(base_modifiers)\r\n    \r\n    personality_traits[name] = {\r\n        \"decision_function\": decision_function,\r\n        \"attribute_modifiers\": attribute_modifiers,\r\n        \"goal_weights\": {\r\n            \"organize\": base_modifiers[\"organization_urgency\"],\r\n            \"interact\": base_modifiers[\"rock_interaction_chance\"],\r\n            \"clean\": base_modifiers[\"plant_seeking_urgency\"]\r\n        }\r\n    }\r\n    return name\r\n\r\n# Register all personality types\r\ndef register_all_personalities():\r\n    register_personality(\r\n        Personality.TIMID,\r\n        lambda squid: squid.anxiety * 1.5,  # Decision function\r\n        {\"anxiety_growth\": 1.3, \"curiosity_growth\": 0.7}\r\n    )\r\n    \r\n    register_personality(\r\n        Personality.ADVENTUROUS,\r\n        lambda squid: squid.curiosity * 1.8,\r\n        {\"curiosity_growth\": 1.5, \"exploration_boost\": 1.4}\r\n    )\r\n    \r\n    register_personality(\r\n        Personality.LAZY,\r\n        lambda squid: squid.sleepiness * 1.2,\r\n        {\"energy_drain\": 0.6, \"movement_speed\": 0.8}\r\n    )\r\n    \r\n    register_personality(\r\n        Personality.ENERGETIC,\r\n        lambda squid: 100 - squid.sleepiness,\r\n        {\"energy_drain\": 1.4, \"movement_speed\": 1.3}\r\n    )\r\n    \r\n    register_personality(\r\n        Personality.GREEDY,\r\n        lambda squid: squid.hunger * 2.0,\r\n        {\"hunger_growth\": 1.3, \"satisfaction_decay\": 1.2}\r\n    )\r\n    \r\n    register_personality(\r\n        Personality.STUBBORN,\r\n        lambda squid: 100 if squid.hunger > 70 else 30,\r\n        {\"food_preference\": \"sushi\", \"adaptability\": 0.3}\r\n    )\r\n\r\nregister_all_personalities()"
  },
  {
    "path": "src/plugin_manager.py",
    "content": "import os\nimport importlib.util\nimport inspect\nimport logging\nimport sys\nfrom typing import Dict, List, Callable, Any\n\n# ANSI escape codes for console colours\nclass ANSI:\n    BLUE = \"\\x1b[34m\"\n    RED = \"\\x1b[31m\"\n    YELLOW = \"\\x1b[33m\"\n    CYAN = \"\\x1b[36m\"\n    RESET = \"\\x1b[0m\"\n\nclass ColoredFormatter(logging.Formatter):\n    \"\"\"\n    A custom logging formatter that colors only the 'LEVEL:NAME:' prefix\n    for messages from the 'PluginManager' logger.\n    \"\"\"\n    \n    COLORS = {\n        logging.DEBUG: ANSI.CYAN,\n        logging.INFO: ANSI.CYAN,\n        logging.WARNING: ANSI.YELLOW,\n        logging.ERROR: ANSI.RED,\n        logging.CRITICAL: ANSI.RED,\n    }\n\n    def __init__(self, fmt=\"%(levelname)s:%(name)s:%(message)s\", datefmt=None, style='%'):\n        # We call super().__init__ but will override format completely\n        super().__init__(fmt, datefmt, style)\n\n    def format(self, record):\n        # Check if the log is from our target logger\n        if record.name == \"PluginManager\":\n            # Get the appropriate color for the log level\n            color = self.COLORS.get(record.levelno, ANSI.RESET) # Default to RESET if no colour found\n            \n            # Create the prefix string (e.g., \"INFO:PluginManager:\")\n            prefix = f\"{record.levelname}:{record.name}:\"\n            \n            # Get the actual log message\n            message = record.getMessage()\n            \n            # Append exception information if present\n            if record.exc_info:\n                if not record.exc_text:\n                    record.exc_text = self.formatException(record.exc_info)\n                if record.exc_text:\n                    message = message + \"\\n\" + record.exc_text\n            \n            # Construct the final coloured log string\n            # Only the prefix is coloured; the message remains default (white/black) until RESET\n            return f\"{color}{prefix}{ANSI.RESET} {message}\"\n        else:\n            # For any other logger, use the default formatter behavior (uncoloured)\n            return super().format(record)\n\nclass PluginManager:\n    _instance = None  # Singleton instance reference\n    \n    def __new__(cls, *args, **kwargs):\n        \"\"\"Singleton pattern implementation\"\"\"\n        if cls._instance is None:\n            cls._instance = super().__new__(cls)\n            cls._instance._initialized = False  # Mark as uninitialized\n        return cls._instance\n    \n    def __init__(self, plugin_directory=\"plugins\"):\n        \"\"\"Initialize the plugin manager (only once due to singleton)\"\"\"\n        if self._initialized:\n            return\n            \n        self.plugin_directory = plugin_directory\n        self.plugins: Dict[str, Dict] = {}        # Stores loaded plugins' metadata and instances\n        self.hooks: Dict[str, List[Dict]] = {}    # Registered hooks and their subscribers\n        self.enabled_plugins: set[str] = set()    # Names of enabled plugins (use lowercase)\n        self.auto_load_blacklist: set[str] = {\"multiplayer\", \"stdp\"}  ### FIX: Stop Multiplayer plugin freaking out at startup  ** ESSENTIAL **\n                                                                ## This is super important. All plugins start austomatically unless\n                                                                ## specifically blacklisted here... DO NOT LET MULTIPLAYER AUTO START.\n\n        self.auto_load_allowlist: set[str] = set()  # If non-empty, ONLY these plugins load\n        self._whitelist: set[str] = set()  # Populated from whitelist.txt; controls auto-enable\n                                                                 \n        # Custom neuron handlers registered by plugins\n        # Maps neuron_name -> {'handler': callable, 'plugin': plugin_name, 'metadata': dict}\n        self._neuron_handlers: Dict[str, Dict] = {}\n        \n        # Configure the logger for PluginManager\n        self.logger = logging.getLogger(\"PluginManager\")\n        \n        if not self.logger.handlers: # Avoid adding multiple handlers\n            self.logger.setLevel(logging.INFO) # Set the minimum level\n\n            ch = logging.StreamHandler(sys.stdout) # Log to standard output\n            \n            # Use the NEW ColoredFormatter. \n            # The 'fmt' here is less critical since we override format(), \n            # but it acts as a fallback or for other loggers.\n            formatter = ColoredFormatter() \n            ch.setFormatter(formatter)\n            \n            self.logger.addHandler(ch)\n            self.logger.propagate = False # Prevent logs from going to root logger\n\n        self._discovered_plugins: Dict[str, Dict] | None = None\n        \n        os.makedirs(plugin_directory, exist_ok=True)\n        \n        self._initialize_hooks()\n        self._initialized = True\n\n    # --- Start of Original PluginManager Methods ---\n    # (These methods remain largely the same, only the logger setup in __init__ \n    # and the Formatter class definition are the core changes for coloring)\n\n    def _initialize_hooks(self):\n        \"\"\"Initialize standard hooks that plugins can register for\"\"\"\n        # Lifecycle hooks\n        self.register_hook(\"on_startup\")\n        self.register_hook(\"on_shutdown\")\n        self.register_hook(\"on_new_game\")\n        self.register_hook(\"on_save_game\")\n        self.register_hook(\"on_load_game\")\n        \n        # Simulation hooks\n        self.register_hook(\"pre_update\")\n        self.register_hook(\"post_update\")\n        self.register_hook(\"on_speed_change\")\n        \n        # Squid state hooks\n        self.register_hook(\"on_squid_state_change\")\n        self.register_hook(\"on_hunger_change\")\n        self.register_hook(\"on_happiness_change\")\n        self.register_hook(\"on_cleanliness_change\")\n        self.register_hook(\"on_sleepiness_change\")\n        self.register_hook(\"on_satisfaction_change\")\n        self.register_hook(\"on_anxiety_change\")\n        self.register_hook(\"on_curiosity_change\")\n        \n        # Action hooks\n        self.register_hook(\"on_feed\")\n        self.register_hook(\"on_clean\")\n        self.register_hook(\"on_medicine\")\n        self.register_hook(\"on_sleep\")\n        self.register_hook(\"on_wake\")\n        self.register_hook(\"on_startle\")\n        \n        # Interaction hooks\n        self.register_hook(\"on_rock_pickup\")\n        self.register_hook(\"on_rock_throw\")\n        self.register_hook(\"on_decoration_interaction\")\n        self.register_hook(\"on_ink_cloud\")\n        \n        # Neural/memory hooks\n        self.register_hook(\"on_brain_state_update\")\n        self.register_hook(\"on_neurogenesis\")\n        self.register_hook(\"on_memory_created\")\n        self.register_hook(\"on_memory_to_long_term\")\n        \n        # UI hooks\n        self.register_hook(\"on_menu_creation\")\n        self.register_hook(\"on_message_display\")\n        \n        # Custom menu action hooks\n        self.register_hook(\"register_menu_actions\")\n\n        # Plugin lifecycle hooks\n        self.register_hook(\"on_plugin_enabled\")\n        self.register_hook(\"on_plugin_disabled\")\n        \n        # Custom neuron hooks - allows plugins to register input neuron handlers\n        self.register_hook(\"register_neuron_handlers\")\n        \n        # Neuron output hooks - triggered when neurons fire above threshold\n        # Movement behaviors\n        self.register_hook(\"neuron_output_flee\")\n        self.register_hook(\"neuron_output_seek_food\")\n        self.register_hook(\"neuron_output_seek_plant\")\n        self.register_hook(\"neuron_output_approach_rock\")\n        self.register_hook(\"neuron_output_wander\")\n        \n        # Action behaviors\n        self.register_hook(\"neuron_output_throw_rock\")\n        self.register_hook(\"neuron_output_pick_up_rock\")\n        self.register_hook(\"neuron_output_ink_cloud\")\n        self.register_hook(\"neuron_output_eat\")\n        self.register_hook(\"neuron_output_change_color\")\n        \n        # State changes\n        self.register_hook(\"neuron_output_sleep\")\n        self.register_hook(\"neuron_output_wake\")\n        self.register_hook(\"neuron_output_startle\")\n        self.register_hook(\"neuron_output_calm\")\n        \n        # Stat modifications\n        self.register_hook(\"neuron_output_boost_happiness\")\n        self.register_hook(\"neuron_output_boost_curiosity\")\n        self.register_hook(\"neuron_output_reduce_anxiety\")\n        \n        # Custom/plugin-defined outputs\n        self.register_hook(\"neuron_output_custom\")\n\n\n\n    def register_all_sensors(self, tamagotchi_logic):\n        \"\"\"\n        Register all available sensors (built-in and plugin) with the plugin manager.\n        \n        This creates a unified registry by registering built-in sensors that normally \n        live in BrainNeuronHooks, making them discoverable through the plugin manager's API.\n        \n        Args:\n            tamagotchi_logic: TamagotchiLogic instance needed for sensor handlers\n            \n        Returns:\n            int: Number of built-in sensors registered\n        \"\"\"\n        # Lazy imports to avoid circular dependencies\n        from .designer_sensor_discovery import get_builtin_sensors\n        from .brain_neuron_hooks import BrainNeuronHooks\n        \n        brain_hooks = BrainNeuronHooks(tamagotchi_logic)\n        builtin_sensors = get_builtin_sensors()\n        \n        count = 0\n        for name, info in builtin_sensors.items():\n            # Skip if already registered by a plugin\n            if name in self._neuron_handlers:\n                existing = self._neuron_handlers[name]\n                if existing.get('plugin') != 'system':\n                    self.logger.debug(\n                        f\"Skipping built-in sensor '{name}' - \"\n                        f\"overridden by plugin '{existing.get('plugin')}'\"\n                    )\n                continue\n            \n            # Register if handler exists\n            if name in brain_hooks.handlers:\n                handler = brain_hooks.handlers[name]\n                \n                metadata = {\n                    'description': info.get('description', ''),\n                    'is_binary': info.get('is_binary', False),\n                    'category': info.get('category', 'built-in'),\n                    'default_connections': info.get('default_connections', []),\n                    'source': 'built-in'\n                }\n                \n                self.register_neuron_handler(\n                    neuron_name=name,\n                    handler=handler,\n                    plugin_name='system',\n                    metadata=metadata\n                )\n                count += 1\n        \n        if count > 0:\n            self.logger.info(f\"Registered {count} built-in sensors with PluginManager\")\n        return count\n    \n    def register_hook(self, hook_name: str) -> None:\n        \"\"\"\n        Register a new hook that plugins can subscribe to.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.hooks[hook_name] = []\n            self.logger.debug(f\"Registered hook: {hook_name}\")\n    \n    def subscribe_to_hook(self, hook_name: str, plugin_name: str, callback: Callable) -> bool:\n        \"\"\"\n        Subscribe a plugin's callback to a specific hook.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.logger.warning(f\"Plugin {plugin_name} tried to subscribe to non-existent hook: {hook_name}\")\n            return False\n        \n        self.hooks[hook_name].append({\n            \"plugin\": plugin_name,\n            \"callback\": callback\n        })\n        self.logger.debug(f\"Plugin {plugin_name} subscribed to hook: {hook_name}\")\n        return True\n    \n    def unsubscribe_from_hook(self, hook_name: str, plugin_name: str) -> bool:\n        \"\"\"\n        Unsubscribe a plugin from a specific hook.\n        \"\"\"\n        if hook_name not in self.hooks:\n            return False\n        \n        self.hooks[hook_name] = [\n            h for h in self.hooks[hook_name] \n            if h[\"plugin\"] != plugin_name\n        ]\n        return True\n    \n    def trigger_hook(self, hook_name, **kwargs):\n        \"\"\"\n        Trigger a hook, calling all subscribed plugin callbacks.\n        \"\"\"\n        if hook_name not in self.hooks:\n            self.logger.warning(f\"Attempted to trigger non-existent hook: {hook_name}\")\n            return []\n        \n        results = []\n        for subscriber in self.hooks[hook_name]:\n            plugin_name = subscriber[\"plugin\"]\n            # Only trigger hooks for enabled plugins\n            if plugin_name.lower() not in self.enabled_plugins:\n                continue\n                \n            try:\n                callback = subscriber[\"callback\"]\n                result = callback(**kwargs)\n                results.append(result)\n            except Exception as e:\n                self.logger.error(f\"Error in plugin {plugin_name} for hook {hook_name}: {str(e)}\", exc_info=True)\n        \n        return results\n    \n    def discover_plugins(self) -> Dict[str, Dict]:\n        \"\"\"\n        Discover available plugins from the plugin directory.\n        Ensures plugin names (keys in the returned dict) are lowercase.\n        \"\"\"\n        plugin_info: Dict[str, Dict] = {}\n        \n        if not os.path.exists(self.plugin_directory):\n            self.logger.warning(f\"Plugin directory does not exist: {self.plugin_directory}\")\n            return plugin_info\n        \n\n        for plugin_dir in os.listdir(self.plugin_directory):\n            plugin_path = os.path.join(self.plugin_directory, plugin_dir)\n            \n            if not os.path.isdir(plugin_path):\n                continue\n                \n            main_py = os.path.join(plugin_path, \"main.py\")\n            \n            if not os.path.exists(main_py):\n                self.logger.debug(f\"No main.py found in {plugin_path}\")\n                continue\n                \n            try:\n                module_name = f\"plugins.{plugin_dir}.main\"\n                spec = importlib.util.spec_from_file_location(module_name, main_py)\n                if spec is None or spec.loader is None:\n                    self.logger.error(f\"Could not create spec for plugin {plugin_dir} at {main_py}\")\n                    continue\n                module = importlib.util.module_from_spec(spec)\n                \n                sys.modules[module_name] = module\n\n                spec.loader.exec_module(module)\n                \n                plugin_name_attr = getattr(module, \"PLUGIN_NAME\", plugin_dir)\n                plugin_name = plugin_name_attr.lower()\n                \n                metadata = {\n                    \"name\": plugin_name,\n                    \"original_name\": plugin_name_attr,\n                    \"version\": getattr(module, \"PLUGIN_VERSION\", \"1.0.0\"),\n                    \"author\": getattr(module, \"PLUGIN_AUTHOR\", \"Unknown\"),\n                    \"description\": getattr(module, \"PLUGIN_DESCRIPTION\", \"\"),\n                    \"requires\": [req.lower() for req in getattr(module, \"PLUGIN_REQUIRES\", [])],\n                    \"path\": main_py,\n                    \"directory\": plugin_path,\n                    \"module\": module,\n                    \"main_class_name\": getattr(module, \"PLUGIN_MAIN_CLASS\", None) \n                }\n                \n                plugin_info[plugin_name] = metadata\n                #self.logger.info(f\"Discovered plugin:   {metadata['original_name']} v{metadata['version']} (key: {plugin_name})\")\n                \n            except Exception as e:\n                self.logger.error(f\"Error discovering plugin in '{plugin_dir}': {str(e)}\", exc_info=True)\n        \n        self._discovered_plugins = plugin_info\n        if not plugin_info:\n            self.logger.info(\"No plugins discovered to load.\")\n        return plugin_info\n\n    def load_plugin(self, plugin_name: str) -> bool:\n        \"\"\"\n        Load and initialize a plugin by name. Assumes plugin_name is already lowercase.\n        (Using the version from previous turns, without the 'instance' check that was causing issues)\n        \"\"\"\n        plugin_name = plugin_name.lower()\n\n        if plugin_name in self.plugins:\n            self.logger.info(f\"Plugin '{plugin_name}' already loaded.\")\n            return True\n\n        if self._discovered_plugins is None:\n            self.logger.error(\"Plugin discovery must be run before loading.\")\n            self._discovered_plugins = self.discover_plugins()\n\n        if plugin_name not in self._discovered_plugins:\n            self.logger.error(f\"Plugin '{plugin_name}' not found.\")\n            return False\n\n        plugin_data = self._discovered_plugins[plugin_name]\n        module = plugin_data[\"module\"]\n        original_plugin_name_display = plugin_data.get(\"original_name\", plugin_name)\n\n        #self.logger.info(f\"Attempting to load plugin '{original_plugin_name_display}' (key: '{plugin_name}')\")\n        #self.logger.info(f\"Plugin '{plugin_name}': Module '{module.__name__}' found.\")\n\n        required_plugins = plugin_data.get(\"requires\", [])\n        if required_plugins:\n            missing_plugins = []\n            for required_name_lower in required_plugins:\n                if required_name_lower not in self.plugins:\n                    missing_plugins.append(required_name_lower)\n            if missing_plugins:\n                #self.logger.error(f\"Plugin '{plugin_name}' requires missing plugin(s): {', '.join(missing_plugins)}.\")\n                #self.logger.error(f\"Plugin '{plugin_name}': Dependency check failed.\")\n                return False\n        #self.logger.info(f\"Plugin '{plugin_name}': Dependencies satisfied.\")\n\n        if not hasattr(module, \"initialize\"):\n            self.logger.error(f\"Plugin '{plugin_name}' has no 'initialize' function.\")\n            return False\n        #self.logger.info(f\"Plugin '{plugin_name}': Found 'initialize' function. Attempting to call.\")\n\n        try:\n            initialize_func = getattr(module, \"initialize\")\n            success = initialize_func(self)  # Call initialize\n\n            if success:\n                #self.logger.info(f\"Plugin '{plugin_name}': 'initialize' function executed successfully.\")\n                \n                # Check if plugin registered itself (especially important for multiplayer's pattern)\n                if plugin_name not in self.plugins:\n                     # If it didn't register itself, add the discovered data now.\n                     # This might happen for simpler plugins. If it was *supposed* to register and didn't,\n                     # it might cause issues later if an instance is expected.\n                     self.logger.info(f\"Plugin '{plugin_name}' did not self-register; adding discovered data.\")\n                     self.plugins[plugin_name] = plugin_data\n\n                # Check if an instance *is* now present in the (potentially updated) record\n                if plugin_name in self.plugins and ('instance' not in self.plugins[plugin_name] or self.plugins[plugin_name].get('instance') is None):\n                     # This is the warning that replaces the previous hard error\n                     self.logger.warning(f\"Plugin '{plugin_name}': Instance was not explicitly set in manager's records by 'initialize'.\")\n                elif plugin_name in self.plugins:\n                     self.logger.info(f\"Success\")\n\n                if plugin_name not in self.auto_load_blacklist \\\n                        and (not self._whitelist or plugin_name in self._whitelist):\n                    self.enabled_plugins.add(plugin_name)\n                \n                return True\n            else:\n                self.logger.error(f\"Plugin '{plugin_name}' 'initialize' function returned False or failed.\")\n                return False\n\n        except Exception as e:\n            self.logger.error(f\"Error during initialization of plugin '{plugin_name}': {str(e)}\", exc_info=True)\n            return False\n\n    def load_all_plugins(self) -> Dict[str, bool]:\n        \"\"\"\n        Load all discovered plugins except those in auto_load_blacklist.\n        If a whitelist.txt exists in the plugin directory, only those plugins load.\n        \"\"\"\n        self.logger.info(\"  Discovering plugins...\")\n        self.plugins.clear()\n        self.enabled_plugins.clear()\n\n        # --- Whitelist ---\n        self._whitelist = set()\n        whitelist_path = os.path.join(self.plugin_directory, 'whitelist.txt')\n        if os.path.exists(whitelist_path):\n            with open(whitelist_path, 'r') as f:\n                for line in f:\n                    name = line.strip().lower()\n                    if name and not name.startswith('#'):\n                        self._whitelist.add(name)\n            self.logger.info(f\"             Whitelist active: {sorted(self._whitelist)}\")\n\n        self._discovered_plugins = self.discover_plugins()\n        if not self._discovered_plugins:\n            return {}\n\n        results = {}\n        plugins_to_load_ordered = [\n            name for name in self._discovered_plugins.keys()\n            if name not in self.auto_load_blacklist\n            and (not self._whitelist or name in self._whitelist)\n        ]\n\n        for plugin_name_key in plugins_to_load_ordered:\n            result = self.load_plugin(plugin_name_key)\n            results[plugin_name_key] = result\n\n        blacklisted_found = [name for name in self._discovered_plugins.keys() if name in self.auto_load_blacklist]\n        if blacklisted_found:\n            self.logger.info(f\"Skipped auto-loading of {blacklisted_found}\")\n\n        return results\n    \n    def unload_plugin(self, plugin_name: str) -> bool:\n        \"\"\"Unload a plugin by name.\"\"\"\n        plugin_name_lower = plugin_name.lower()\n        if plugin_name_lower not in self.plugins:\n            self.logger.warning(f\"Plugin '{plugin_name_lower}' not found for unloading.\")\n            return False\n\n        plugin_data = self.plugins.get(plugin_name_lower)\n        if plugin_data:\n            instance = plugin_data.get('instance')\n            if instance and hasattr(instance, 'shutdown'):\n                try:\n                    instance.shutdown()\n                    self.logger.info(f\"Plugin '{plugin_name_lower}' shutdown method called.\")\n                except Exception as e:\n                    self.logger.error(f\"Error during plugin '{plugin_name_lower}' shutdown: {e}\", exc_info=True)\n        \n        if plugin_name_lower in self.enabled_plugins:\n            self.enabled_plugins.remove(plugin_name_lower)\n            self.logger.info(f\"Plugin '{plugin_name_lower}' disabled.\")\n        \n        for hook_name in list(self.hooks.keys()):\n            self.hooks[hook_name] = [\n                sub for sub in self.hooks[hook_name] if sub['plugin'].lower() != plugin_name_lower\n            ]\n\n        del self.plugins[plugin_name_lower]\n        self.logger.info(f\"Plugin '{plugin_name_lower}' unloaded successfully.\")\n        return True\n\n    def unload_all_plugins(self) -> None:\n        \"\"\"Unload all active plugins.\"\"\"\n        self.logger.info(\"Unloading all plugins...\")\n        for plugin_name_key in list(self.plugins.keys()):\n            self.unload_plugin(plugin_name_key)\n        self.logger.info(\"All plugins have been unloaded.\")\n\n    def reload_all_plugins(self) -> Dict[str, bool]:\n        \"\"\"\n        Reload all plugins by unloading and then loading them again.\n        This is useful when starting a new game to ensure plugins start fresh.\n        \n        Returns:\n            Dict[str, bool]: Dictionary mapping plugin names to their load success status\n        \"\"\"\n        self.logger.info(\"Reloading all plugins...\")\n        \n        # First unload all plugins\n        self.unload_all_plugins()\n        \n        # Then load them all again\n        results = self.load_all_plugins()\n        \n        self.logger.info(\"All plugins have been reloaded.\")\n        return results\n\n    def enable_plugin(self, plugin_key: str) -> bool:\n        plugin_key_lower = plugin_key.lower()  # Normalize to lowercase\n\n        if plugin_key_lower in self.enabled_plugins:\n            self.logger.info(f\"Plugin '{plugin_key_lower}' is already enabled.\")\n            return True\n\n        # NEW: If plugin is not loaded but is discovered, load it first\n        if plugin_key_lower not in self.plugins:\n            if self._discovered_plugins is None:\n                self.logger.info(\"Plugin discovery not yet run. Discovering plugins...\")\n                self.discover_plugins()\n            \n            if plugin_key_lower in self._discovered_plugins:\n                self.logger.info(f\"Plugin '{plugin_key_lower}' is discovered but not loaded. Loading it first...\")\n                if not self.load_plugin(plugin_key_lower):\n                    self.logger.error(f\"Failed to load plugin '{plugin_key_lower}' before enabling.\")\n                    return False\n            else:\n                self.logger.error(f\"Plugin '{plugin_key_lower}' not found in discovered plugins.\")\n                return False\n\n        # Now proceed with the original enabling logic...\n        plugin_data = self.plugins.get(plugin_key_lower)\n        if not plugin_data or 'instance' not in plugin_data:\n            self.logger.error(f\"ERROR:PluginManager: Plugin '{plugin_key_lower}' not found or has no instance for enabling.\")\n            return False\n\n        instance = plugin_data['instance']\n        if not instance:\n            self.logger.error(f\"ERROR:PluginManager: Instance for plugin '{plugin_key_lower}' is None.\")\n            return False\n\n        # --- Call setup() if it hasn't been run ---\n        if hasattr(instance, 'setup') and callable(instance.setup):\n            if not plugin_data.get('is_setup', False): \n                try:\n                    self.logger.info(f\"INFO:PluginManager: Calling setup() for plugin '{plugin_key_lower}'.\")\n                    tamagotchi_logic_ref = getattr(self, 'tamagotchi_logic', None)\n                    if tamagotchi_logic_ref:\n                        instance.setup(self, tamagotchi_logic_ref)\n                    else:\n                        self.logger.warning(f\"WARNING:PluginManager: tamagotchi_logic not available in PluginManager when setting up '{plugin_key_lower}'. Passing None.\")\n                        instance.setup(self, None)\n\n                    plugin_data['is_setup'] = True\n                    self.logger.info(f\"INFO:PluginManager: setup() for plugin '{plugin_key_lower}' completed.\")\n                except Exception as e:\n                    self.logger.error(f\"ERROR:PluginManager: Exception during setup of plugin '{plugin_key_lower}': {e}\", exc_info=True)\n                    return False\n            else:\n                self.logger.info(f\"INFO:PluginManager: Plugin '{plugin_key_lower}' already marked as setup by PluginManager. Skipping setup() call.\")\n\n        # --- Now, call the plugin's own enable method ---\n        if hasattr(instance, 'enable') and callable(instance.enable):\n            try:\n                self.logger.info(f\"INFO:PluginManager: Calling enable() method on plugin instance '{plugin_key_lower}'.\")\n                if instance.enable():\n                    self.enabled_plugins.add(plugin_key_lower)\n                    self.logger.info(f\"INFO:PluginManager: Plugin '{plugin_key_lower}' successfully enabled and added to enabled set.\")\n                    self.trigger_hook(\"on_plugin_enabled\", plugin_key=plugin_key_lower)\n                    return True\n                else:\n                    self.logger.error(f\"ERROR:PluginManager: Plugin '{plugin_key_lower}' enable() method returned False.\")\n                    return False\n            except Exception as e:\n                self.logger.error(f\"ERROR:PluginManager: Exception during enable() of plugin '{plugin_key_lower}': {e}\", exc_info=True)\n                return False\n        else:\n            # If the plugin has no specific enable method, just mark it as enabled in the manager\n            self.enabled_plugins.add(plugin_key_lower)\n            self.logger.info(f\"INFO:PluginManager: Plugin '{plugin_key_lower}' has no custom enable() method, marked as enabled in manager.\")\n            self.trigger_hook(\"on_plugin_enabled\", plugin_key=plugin_key_lower)\n            return True\n\n    def disable_plugin(self, plugin_name: str) -> bool:\n        \"\"\"Disable an enabled plugin.\"\"\"\n        plugin_name_lower = plugin_name.lower()\n        \n        if plugin_name_lower not in self.enabled_plugins:\n            self.logger.warning(f\"Plugin '{plugin_name_lower}' is not currently enabled.\")\n            return False\n            \n        plugin_data = self.plugins.get(plugin_name_lower)\n        if plugin_data:\n            plugin_instance = plugin_data.get('instance')\n            if plugin_instance and hasattr(plugin_instance, 'disable'):\n                try:\n                    plugin_instance.disable()\n                    self.logger.info(f\"Plugin '{plugin_name_lower}'.disable() method called.\")\n                except Exception as e:\n                    self.logger.error(f\"Error calling .disable() on plugin '{plugin_name_lower}': {e}\", exc_info=True)\n        \n        self.enabled_plugins.remove(plugin_name_lower)\n        self.logger.info(f\"Plugin '{plugin_name_lower}' disabled.\")\n        self.trigger_hook(\"on_plugin_disabled\", plugin_key=plugin_name_lower)\n        return True\n    \n    def get_plugin_info(self, plugin_name: str) -> Dict | None:\n        \"\"\"Get information about a loaded plugin.\"\"\"\n        plugin_name_lower = plugin_name.lower()\n        return self.plugins.get(plugin_name_lower)\n    \n    def get_loaded_plugins(self) -> List[str]:\n        \"\"\"Get original names of all loaded plugins.\"\"\"\n        return [data.get('original_name', key) for key, data in self.plugins.items()]\n    \n    def get_enabled_plugins(self) -> List[str]:\n        \"\"\"Get original names of all enabled plugins.\"\"\"\n        enabled_original_names = []\n        for name_lower in self.enabled_plugins:\n            if name_lower in self.plugins:\n                enabled_original_names.append(self.plugins[name_lower].get('original_name', name_lower))\n            else:\n                enabled_original_names.append(name_lower) \n        return enabled_original_names\n\n    def check_dependencies(self, plugin_name_to_check: str) -> bool:\n        \"\"\"Check if dependencies for a plugin are met.\"\"\"\n        plugin_name_to_check = plugin_name_to_check.lower()\n        if self._discovered_plugins is None or plugin_name_to_check not in self._discovered_plugins:\n            self.logger.error(f\"Plugin '{plugin_name_to_check}' not found for dependency check.\")\n            return False\n            \n        plugin_data = self._discovered_plugins[plugin_name_to_check]\n        required_plugin_keys = plugin_data.get(\"requires\", []) \n        \n        if not required_plugin_keys:\n            return True\n            \n        for required_key in required_plugin_keys:\n            if required_key not in self.plugins:\n                self.logger.error(f\"Plugin '{plugin_name_to_check}' requires '{required_key}' which is not loaded.\")\n                return False\n        return True\n\n    def set_tamagotchi_logic(self, tamagotchi_logic_instance):\n        \"\"\"Allows setting a reference to the main TamagotchiLogic instance.\"\"\"\n        setattr(self, 'tamagotchi_logic', tamagotchi_logic_instance)\n        self.logger.info(\"TamagotchiLogic instance has been linked to PluginManager.\")\n\n    # =========================================================================\n    # CUSTOM NEURON HANDLER REGISTRATION\n    # =========================================================================\n    \n    def register_neuron_handler(\n        self, \n        neuron_name: str, \n        handler: Callable, \n        plugin_name: str,\n        metadata: Dict = None\n    ) -> bool:\n        \"\"\"\n        Register a custom handler for a brain input neuron.\n        \n        This allows plugins to add new sensor neurons that can be wired into\n        the squid's neural network via the brain designer.\n        \n        Args:\n            neuron_name: Unique name for the neuron (e.g., 'music_beat_detector')\n            handler: A callable that returns a float (0-100) activation value.\n                     Should take no arguments and return the current activation.\n            plugin_name: Name of the plugin registering this handler\n            metadata: Optional dict with additional info:\n                - 'description': Human-readable description\n                - 'is_binary': True if neuron only outputs 0 or 100\n                - 'category': Category for grouping (e.g., 'environmental', 'social')\n                - 'default_connections': List of neurons to auto-connect to\n                \n        Returns:\n            True if registered successfully, False if neuron name already exists\n            \n        Example:\n            def my_beat_handler():\n                # Return 100 when beat detected, 0 otherwise\n                return 100.0 if detect_beat() else 0.0\n            \n            plugin_manager.register_neuron_handler(\n                'music_beat', \n                my_beat_handler, \n                'MusicPlugin',\n                metadata={\n                    'description': 'Detects music beats',\n                    'is_binary': True,\n                    'category': 'audio'\n                }\n            )\n        \"\"\"\n        plugin_name_lower = plugin_name.lower()\n        \n        if neuron_name in self._neuron_handlers:\n            existing = self._neuron_handlers[neuron_name]\n            self.logger.warning(\n                f\"Neuron handler '{neuron_name}' already registered by \"\n                f\"'{existing.get('plugin', 'unknown')}'. Overwriting with '{plugin_name}'.\"\n            )\n        \n        self._neuron_handlers[neuron_name] = {\n            'handler': handler,\n            'plugin': plugin_name_lower,\n            'metadata': metadata or {}\n        }\n        \n        self.logger.info(f\"Registered neuron handler: '{neuron_name}' from plugin '{plugin_name}'\")\n        return True\n    \n    def unregister_neuron_handler(self, neuron_name: str, plugin_name: str) -> bool:\n        \"\"\"\n        Unregister a neuron handler.\n        \n        Args:\n            neuron_name: Name of the neuron to unregister\n            plugin_name: Name of the plugin that registered it (for verification)\n            \n        Returns:\n            True if unregistered successfully, False otherwise\n        \"\"\"\n        plugin_name_lower = plugin_name.lower()\n        \n        if neuron_name not in self._neuron_handlers:\n            self.logger.warning(f\"Cannot unregister '{neuron_name}': not found\")\n            return False\n        \n        existing = self._neuron_handlers[neuron_name]\n        if existing.get('plugin') != plugin_name_lower:\n            self.logger.warning(\n                f\"Cannot unregister '{neuron_name}': registered by \"\n                f\"'{existing.get('plugin')}', not '{plugin_name}'\"\n            )\n            return False\n        \n        del self._neuron_handlers[neuron_name]\n        self.logger.info(f\"Unregistered neuron handler: '{neuron_name}'\")\n        return True\n    \n    def get_neuron_handlers(self) -> Dict[str, Callable]:\n        \"\"\"\n        Get all registered neuron handlers as a dict of name -> callable.\n        \n        This is called by BrainNeuronHooks to merge plugin handlers with\n        built-in handlers.\n        \n        Returns:\n            Dict mapping neuron names to their handler callables\n        \"\"\"\n        return {\n            name: data['handler'] \n            for name, data in self._neuron_handlers.items()\n        }\n    \n    def get_neuron_handler_info(self, neuron_name: str) -> Dict | None:\n        \"\"\"\n        Get full info about a registered neuron handler.\n        \n        Returns:\n            Dict with 'handler', 'plugin', and 'metadata' keys, or None if not found\n        \"\"\"\n        return self._neuron_handlers.get(neuron_name)\n    \n    def get_all_neuron_handler_info(self) -> Dict[str, Dict]:\n        \"\"\"\n        Get info about all registered neuron handlers.\n        \n        Returns:\n            Dict mapping neuron names to their full registration info\n        \"\"\"\n        return dict(self._neuron_handlers)\n    \n    def get_plugin_neuron_handlers(self, plugin_name: str) -> List[str]:\n        \"\"\"\n        Get list of neuron handlers registered by a specific plugin.\n        \n        Args:\n            plugin_name: Name of the plugin\n            \n        Returns:\n            List of neuron names registered by that plugin\n        \"\"\"\n        plugin_name_lower = plugin_name.lower()\n        return [\n            name for name, data in self._neuron_handlers.items()\n            if data.get('plugin') == plugin_name_lower\n        ]\n"
  },
  {
    "path": "src/plugin_manager_dialog.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nimport os\r\n\r\nclass PluginManagerDialog(QtWidgets.QDialog):\r\n    def __init__(self, plugin_manager, parent=None):\r\n        super().__init__(parent)\r\n        self.plugin_manager = plugin_manager\r\n        self.setWindowTitle(\"Plugin Manager\")\r\n        self.resize(800, 500)\r\n        \r\n        self.setup_ui()\r\n        self.load_plugin_data()\r\n        \r\n    def setup_ui(self):\r\n        # Main layout\r\n        layout = QtWidgets.QVBoxLayout(self)\r\n        layout.setSpacing(15)\r\n        layout.setContentsMargins(20, 20, 20, 20)\r\n        \r\n        # Header label\r\n        header = QtWidgets.QLabel(\"🧩 Plugin Manager\")\r\n        header.setAlignment(QtCore.Qt.AlignCenter)\r\n        layout.addWidget(header)\r\n        \r\n        # Splitter for resizable sections\r\n        splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)\r\n        \r\n        # Plugin list container\r\n        list_container = QtWidgets.QWidget()\r\n        list_layout = QtWidgets.QVBoxLayout(list_container)\r\n        list_layout.setContentsMargins(0, 0, 0, 0)\r\n        \r\n        list_label = QtWidgets.QLabel(\"Available Plugins\")\r\n        list_layout.addWidget(list_label)\r\n        \r\n        self.plugin_list = QtWidgets.QListWidget()\r\n        self.plugin_list.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)\r\n        self.plugin_list.currentItemChanged.connect(self.on_plugin_selected)\r\n        self.plugin_list.setIconSize(QtCore.QSize(16, 16))\r\n        list_layout.addWidget(self.plugin_list)\r\n        \r\n        splitter.addWidget(list_container)\r\n        \r\n        # Right panel\r\n        right_panel = QtWidgets.QWidget()\r\n        right_layout = QtWidgets.QVBoxLayout(right_panel)\r\n        right_layout.setContentsMargins(0, 0, 0, 0)\r\n        \r\n        # Plugin details group\r\n        details_group = QtWidgets.QGroupBox(\"Plugin Details\")\r\n        details_layout = QtWidgets.QFormLayout(details_group)\r\n        details_layout.setSpacing(10)\r\n        details_layout.setLabelAlignment(QtCore.Qt.AlignRight)\r\n        details_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)\r\n        \r\n        self.plugin_name = QtWidgets.QLabel()\r\n        details_layout.addRow(\"Name:\", self.plugin_name)\r\n        \r\n        self.plugin_version = QtWidgets.QLabel()\r\n        details_layout.addRow(\"Version:\", self.plugin_version)\r\n        \r\n        self.plugin_author = QtWidgets.QLabel()\r\n        details_layout.addRow(\"Author:\", self.plugin_author)\r\n        \r\n        self.plugin_description = QtWidgets.QLabel()\r\n        self.plugin_description.setWordWrap(True)\r\n        self.plugin_description.setMinimumHeight(60)\r\n        details_layout.addRow(\"Description:\", self.plugin_description)\r\n        \r\n        self.plugin_requires = QtWidgets.QLabel()\r\n        details_layout.addRow(\"Dependencies:\", self.plugin_requires)\r\n        \r\n        self.plugin_status = QtWidgets.QLabel()\r\n        details_layout.addRow(\"Status:\", self.plugin_status)\r\n        \r\n        right_layout.addWidget(details_group)\r\n        \r\n        # Actions group\r\n        actions_group = QtWidgets.QGroupBox(\"Actions\")\r\n        actions_layout = QtWidgets.QHBoxLayout(actions_group)\r\n        actions_layout.setSpacing(10)\r\n        \r\n        self.enable_button = QtWidgets.QPushButton(\"Enable\")\r\n        self.enable_button.clicked.connect(self.enable_selected_plugin)\r\n        actions_layout.addWidget(self.enable_button)\r\n        \r\n        self.disable_button = QtWidgets.QPushButton(\"Disable\")\r\n        self.disable_button.clicked.connect(self.disable_selected_plugin)\r\n        actions_layout.addWidget(self.disable_button)\r\n        \r\n        self.refresh_button = QtWidgets.QPushButton(\"Refresh List\")\r\n        self.refresh_button.clicked.connect(self.load_plugin_data)\r\n        actions_layout.addWidget(self.refresh_button)\r\n        \r\n        right_layout.addWidget(actions_group)\r\n        right_layout.addStretch()\r\n        \r\n        splitter.addWidget(right_panel)\r\n        splitter.setSizes([300, 400])\r\n        layout.addWidget(splitter)\r\n        \r\n        # Close button\r\n        button_container = QtWidgets.QWidget()\r\n        button_layout = QtWidgets.QHBoxLayout(button_container)\r\n        button_layout.setContentsMargins(0, 0, 0, 0)\r\n        \r\n        button_layout.addStretch()\r\n        self.close_button = QtWidgets.QPushButton(\"Close\")\r\n        self.close_button.clicked.connect(self.accept)\r\n        layout.addWidget(button_container)\r\n        \r\n    def load_plugin_data(self):\r\n        \"\"\"Load plugin data into the list\"\"\"\r\n        self.plugin_list.clear()\r\n        \r\n        # Get all plugin states\r\n        loaded_plugins = {name.lower(): True for name in self.plugin_manager.get_loaded_plugins()}\r\n        enabled_plugins = {name.lower(): True for name in self.plugin_manager.get_enabled_plugins()}\r\n        \r\n        # First, add loaded plugins\r\n        for plugin_name in self.plugin_manager.get_loaded_plugins():\r\n            plugin_data = self.plugin_manager.plugins.get(plugin_name.lower(), {})\r\n            original_name = plugin_data.get('original_name', plugin_name)\r\n            \r\n            item = QtWidgets.QListWidgetItem(original_name)\r\n            item.setData(QtCore.Qt.UserRole, plugin_data)\r\n            \r\n            if plugin_name.lower() in enabled_plugins:\r\n                item.setIcon(self.get_status_icon(\"enabled\"))\r\n            else:\r\n                item.setIcon(self.get_status_icon(\"loaded\"))\r\n                \r\n            self.plugin_list.addItem(item)\r\n        \r\n        # Then add discovered plugins that aren't loaded\r\n        if hasattr(self.plugin_manager, '_discovered_plugins'):\r\n            for plugin_key, plugin_data in self.plugin_manager._discovered_plugins.items():\r\n                if plugin_key not in loaded_plugins:\r\n                    original_name = plugin_data.get('original_name', plugin_key)\r\n                    item = QtWidgets.QListWidgetItem(original_name)\r\n                    item.setData(QtCore.Qt.UserRole, plugin_data)\r\n                    item.setIcon(self.get_status_icon(\"discovered\"))\r\n                    self.plugin_list.addItem(item)\r\n        \r\n        # Select the first plugin if available\r\n        if self.plugin_list.count() > 0:\r\n            self.plugin_list.setCurrentRow(0)\r\n        else:\r\n            self.clear_plugin_details()\r\n            \r\n    def get_status_icon(self, status):\r\n        \"\"\"Create a colored dot icon for the plugin status\"\"\"\r\n        pixmap = QtGui.QPixmap(16, 16)\r\n        pixmap.fill(QtCore.Qt.transparent)\r\n        \r\n        painter = QtGui.QPainter(pixmap)\r\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\r\n        \r\n        if status == \"enabled\":\r\n            color = QtGui.QColor(0, 200, 0)  # Green\r\n        elif status == \"loaded\":\r\n            color = QtGui.QColor(200, 200, 0)  # Yellow\r\n        else:\r\n            color = QtGui.QColor(150, 150, 150)  # Gray\r\n            \r\n        painter.setBrush(QtGui.QBrush(color))\r\n        painter.setPen(QtGui.QPen(QtCore.Qt.black, 1))\r\n        painter.drawEllipse(2, 2, 12, 12)\r\n        painter.end()\r\n        \r\n        return QtGui.QIcon(pixmap)\r\n        \r\n    def on_plugin_selected(self, current, previous):\r\n        \"\"\"Handle plugin selection change with fixed enable logic\"\"\"\r\n        if not current:\r\n            self.clear_plugin_details()\r\n            return\r\n            \r\n        plugin_data = current.data(QtCore.Qt.UserRole)\r\n        plugin_key = plugin_data.get('name', current.text()).lower()\r\n        original_name = plugin_data.get('original_name', current.text())\r\n        \r\n        # Update details\r\n        self.plugin_name.setText(original_name)\r\n        self.plugin_version.setText(plugin_data.get('version', 'Unknown'))\r\n        self.plugin_author.setText(plugin_data.get('author', 'Unknown'))\r\n        self.plugin_description.setText(plugin_data.get('description', 'No description available'))\r\n        \r\n        # Dependencies\r\n        requires = plugin_data.get('requires', [])\r\n        self.plugin_requires.setText(\", \".join(requires) if requires else \"None\")\r\n        \r\n        # Status and button logic\r\n        is_loaded = plugin_key in {name.lower() for name in self.plugin_manager.get_loaded_plugins()}\r\n        is_enabled = plugin_key in {name.lower() for name in self.plugin_manager.get_enabled_plugins()}\r\n        \r\n        # NEW: Check if plugin can be enabled (loaded OR discovered)\r\n        can_enable = not is_enabled and (is_loaded or plugin_key in self.plugin_manager._discovered_plugins)\r\n        \r\n        if is_enabled:\r\n            self.plugin_status.setText(\"✓ ENABLED\")\r\n        elif is_loaded:\r\n            self.plugin_status.setText(\"Loaded (Not Enabled)\")\r\n        else:\r\n            self.plugin_status.setText(\"Discovered (Not Loaded)\")\r\n        \r\n        # FIX: Enable button for discovered-but-not-loaded plugins\r\n        self.enable_button.setEnabled(bool(can_enable))\r\n        self.disable_button.setEnabled(bool(is_enabled))\r\n        \r\n    def clear_plugin_details(self):\r\n        \"\"\"Clear all plugin details\"\"\"\r\n        self.plugin_name.clear()\r\n        self.plugin_version.clear()\r\n        self.plugin_author.clear()\r\n        self.plugin_description.clear()\r\n        self.plugin_requires.clear()\r\n        self.plugin_status.clear()\r\n        \r\n        self.enable_button.setEnabled(False)\r\n        self.disable_button.setEnabled(False)\r\n        \r\n    def enable_selected_plugin(self):\r\n        \"\"\"Enable the selected plugin with automatic loading\"\"\"\r\n        current_item = self.plugin_list.currentItem()\r\n        if not current_item:\r\n            return\r\n            \r\n        plugin_data = current_item.data(QtCore.Qt.UserRole)\r\n        plugin_key = plugin_data.get('name', current_item.text()).lower()\r\n        \r\n        try:\r\n            # If plugin isn't loaded yet, load it first\r\n            if plugin_key not in self.plugin_manager.plugins:\r\n                self.plugin_manager.logger.info(f\"Plugin '{plugin_key}' not loaded. Loading now...\")\r\n                if not self.plugin_manager.load_plugin(plugin_key):\r\n                    QtWidgets.QMessageBox.warning(\r\n                        self, \r\n                        \"Error\", \r\n                        f\"Failed to load plugin '{plugin_key}'. Check logs for details.\"\r\n                    )\r\n                    return\r\n            \r\n            # Now enable it\r\n            success = self.plugin_manager.enable_plugin(plugin_key)\r\n            \r\n            if success:\r\n                QtWidgets.QMessageBox.information(\r\n                    self,\r\n                    \"Success\",\r\n                    f\"Plugin '{plugin_data.get('original_name', plugin_key)}' enabled successfully\"\r\n                )\r\n                self.load_plugin_data()\r\n            else:\r\n                QtWidgets.QMessageBox.warning(\r\n                    self,\r\n                    \"Error\",\r\n                    f\"Failed to enable plugin '{plugin_key}'\"\r\n                )\r\n        except Exception as e:\r\n            QtWidgets.QMessageBox.warning(\r\n                self, \r\n                \"Error\", \r\n                f\"Error enabling plugin: {str(e)}\"\r\n            )\r\n    \r\n    def disable_selected_plugin(self):\r\n        \"\"\"Disable the selected plugin\"\"\"\r\n        current_item = self.plugin_list.currentItem()\r\n        if not current_item:\r\n            return\r\n            \r\n        plugin_name = current_item.text()\r\n        \r\n        try:\r\n            success = self.plugin_manager.disable_plugin(plugin_name)\r\n            \r\n            if success:\r\n                # Call custom disable method if available\r\n                if plugin_name.lower() in self.plugin_manager.plugins:\r\n                    plugin_instance = self.plugin_manager.plugins[plugin_name.lower()].get('instance')\r\n                    if plugin_instance and hasattr(plugin_instance, 'disable'):\r\n                        try:\r\n                            plugin_instance.disable()\r\n                        except Exception as e:\r\n                            print(f\"Error in plugin disable method: {e}\")\r\n                \r\n                QtWidgets.QMessageBox.information(\r\n                    self,\r\n                    \"Success\",\r\n                    f\"Plugin '{plugin_name}' disabled successfully\"\r\n                )\r\n                self.load_plugin_data()\r\n            else:\r\n                QtWidgets.QMessageBox.warning(\r\n                    self,\r\n                    \"Error\",\r\n                    f\"Failed to disable plugin '{plugin_name}'\"\r\n                )\r\n        except Exception as e:\r\n            QtWidgets.QMessageBox.warning(\r\n                self, \r\n                \"Error\", \r\n                f\"Error disabling plugin: {str(e)}\"\r\n            )"
  },
  {
    "path": "src/preferences.py",
    "content": "\r\nimport os\r\nimport sys\r\nimport glob\r\nimport re\r\nfrom PyQt5 import QtWidgets, QtCore, QtGui\r\nfrom .config_manager import ConfigManager\r\nfrom .localisation import Localization\r\nfrom .display_scaling import DisplayScaling\r\nimport zipfile\r\n\r\n# Define the path to the ZIP file - must match localisation.py - NEEDED FOR BACKWARDS COMPAT\r\nZIP_FILENAME = \"languages.zip\"\r\n\r\nclass PreferencesWindow(QtWidgets.QDialog):\r\n    \"\"\"Floating preferences window for Dosidicus configuration\"\"\"\r\n    \r\n    # NOTE: LANGUAGE_MAP is only used for displaying the name in the preferences window\r\n    # if the localisation system failed to provide a name.\r\n    LANGUAGE_MAP = {\r\n        'en': 'English',\r\n        'es': 'Spanish (Español)',\r\n        'fr': 'French (Français)',\r\n        'de': 'German (Deutsch)',\r\n        'pl': 'Polish (Polski)',\r\n        'uk': 'Ukrainian (Українська)',\r\n        'zh': 'Chinese (中文)',\r\n        'ja': 'Japanese (日本語)',\r\n        'pt': 'Portuguese (Português)',\r\n        'cy': 'Welsh (Cymraeg)',\r\n        'ga': 'Irish (Gaeilge)',\r\n        'ml': 'Millennial',\r\n    }\r\n\r\n    def __init__(self, parent=None):\r\n        super().__init__(parent, QtCore.Qt.Window)\r\n        \r\n        # Ensure scaling is initialized based on the screen this window is on\r\n        screen = QtWidgets.QApplication.primaryScreen()\r\n        screen_geometry = screen.availableGeometry()\r\n        self.config = ConfigManager()\r\n        \r\n        # Re-initialize scaling to ensure context is correct for this window creation\r\n        DisplayScaling.initialize(screen_geometry.width(), screen_geometry.height())\r\n        \r\n        self.setWindowTitle(\"Preferences\")\r\n        self.setModal(False)\r\n        \r\n        # Scale the initial window size\r\n        w = DisplayScaling.scale(1024)\r\n        h = DisplayScaling.scale(800)\r\n        self.resize(w, h)\r\n        \r\n        # Center on screen\r\n        x = (screen_geometry.width() - self.width()) // 2\r\n        y = (screen_geometry.height() - self.height()) // 2\r\n        self.move(x, y)\r\n        \r\n        # Load config manager (use same instance if available)\r\n        self.config_manager = ConfigManager()\r\n        \r\n        # Track if changes were made\r\n        self.changes_made = False\r\n        self.original_values = {}\r\n        \r\n        # Apply styles first (no dependencies)\r\n        self._apply_styles()\r\n        \r\n        # Initialize UI components\r\n        self.init_ui()\r\n        \r\n        # Load config values into UI\r\n        self.load_current_config()\r\n        \r\n    def _apply_styles(self):\r\n        \"\"\"Apply global stylesheet with scaling logic\"\"\"\r\n        \r\n        # BASE SIZES\r\n        base_font = DisplayScaling.font_size(18)    \r\n        header_font = DisplayScaling.font_size(22)  \r\n        small_font = DisplayScaling.font_size(14)   \r\n        \r\n        large_font = DisplayScaling.font_size(32)   \r\n        \r\n        # SUBTITLE SIZE\r\n        subtitle_font = DisplayScaling.font_size(24) \r\n        \r\n        padding_std = DisplayScaling.scale(12)      \r\n        padding_lrg = DisplayScaling.scale(20)\r\n        radius = DisplayScaling.scale(8)\r\n        border_width = max(1, DisplayScaling.scale(2))\r\n        \r\n        # DEVELOPER: ADJUST THE VALUE BELOW TO CHANGE TOP TAB WIDTH\r\n        tab_min_width = DisplayScaling.scale(150)\r\n        \r\n        css = f\"\"\"\r\n            QWidget {{\r\n                font-size: {base_font}px;\r\n                color: #2c3e50;\r\n            }}\r\n            \r\n            /* Headers and Labels */\r\n            QLabel {{\r\n                font-size: {base_font}px;\r\n            }}\r\n            \r\n            /* Group Boxes */\r\n            QGroupBox {{\r\n                font-weight: bold;\r\n                font-size: {header_font}px;\r\n                border: {border_width}px solid #bdc3c7;\r\n                border-radius: {radius}px;\r\n                margin-top: {DisplayScaling.scale(30)}px;\r\n                padding-top: {DisplayScaling.scale(15)}px;\r\n            }}\r\n            QGroupBox::title {{\r\n                subcontrol-origin: margin;\r\n                left: {DisplayScaling.scale(15)}px;\r\n                padding: 0 {DisplayScaling.scale(8)}px;\r\n                color: #34495e;\r\n            }}\r\n            \r\n            /* Tabs */\r\n            QTabWidget::pane {{\r\n                border: {border_width}px solid #bdc3c7;\r\n                background: #fdfdfd;\r\n                border-radius: {radius}px;\r\n            }}\r\n            QTabBar::tab {{\r\n                background: #ecf0f1;\r\n                color: #2c3e50;\r\n                font-size: {base_font}px;\r\n                padding: {padding_std}px {padding_lrg}px;\r\n                border-top-left-radius: {radius}px;\r\n                border-top-right-radius: {radius}px;\r\n                margin-right: {DisplayScaling.scale(4)}px;\r\n                font-weight: bold;\r\n                min-width: {tab_min_width}px;\r\n                alignment: center;\r\n            }}\r\n            QTabBar::tab:selected {{\r\n                background: #003366; /* Dark Blue */\r\n                color: white;        /* White Text */\r\n            }}\r\n            QTabBar::tab:hover:!selected {{\r\n                background: #bdc3c7;\r\n            }}\r\n\r\n            /* Inputs and Buttons */\r\n            QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {{\r\n                padding: {DisplayScaling.scale(8)}px;\r\n                border: {border_width}px solid #bdc3c7;\r\n                border-radius: {DisplayScaling.scale(6)}px;\r\n                background: white;\r\n                min-height: {DisplayScaling.scale(40)}px;\r\n            }}\r\n            \r\n            /* Buttons */\r\n            QPushButton {{\r\n                background-color: #ecf0f1;\r\n                border: {border_width}px solid #bdc3c7;\r\n                border-radius: {radius}px;\r\n                padding: {padding_std}px {padding_lrg}px;\r\n                min-height: {DisplayScaling.scale(45)}px;\r\n                font-weight: bold;\r\n            }}\r\n            QPushButton:hover {{\r\n                background-color: #dfe6e9;\r\n            }}\r\n            QPushButton:pressed {{\r\n                background-color: #b2bec3;\r\n            }}\r\n            \r\n            /* The Save Button */\r\n            QPushButton#SaveButton {{\r\n                background-color: #27ae60;\r\n                color: white;\r\n                font-size: {header_font}px;\r\n            }}\r\n            QPushButton#SaveButton:disabled {{\r\n                background-color: #95a5a6;\r\n            }}\r\n            \r\n            /* LARGE LANGUAGE SELECTION */\r\n            QComboBox#LanguageCombo {{\r\n                font-size: {large_font}px;\r\n                height: {DisplayScaling.scale(80)}px;\r\n                padding: {DisplayScaling.scale(15)}px;\r\n                font-weight: bold;\r\n                background-color: #e8f6f3;\r\n                border: {DisplayScaling.scale(3)}px solid #1abc9c;\r\n                border-radius: {radius}px;\r\n            }}\r\n            QComboBox#LanguageCombo QAbstractItemView {{\r\n                font-size: {large_font}px;\r\n                selection-background-color: #1abc9c;\r\n            }}\r\n            \r\n            /* The Label showing 'en - English' */\r\n            QLabel#LanguageNameLabel {{\r\n                font-size: {subtitle_font}px; /* LARGER FONT */\r\n                color: #34495e;\r\n                font-weight: bold;\r\n                padding-left: {DisplayScaling.scale(5)}px;\r\n                margin-top: {DisplayScaling.scale(10)}px;\r\n            }}\r\n            \r\n            /* Specific helper for description text */\r\n            QLabel#DescriptionLabel {{\r\n                font-size: {small_font}px;\r\n                color: #7f8c8d;\r\n                font-style: italic;\r\n            }}\r\n            \r\n            /* Two-column layout for interactions */\r\n            QFrame#ColumnFrame {{\r\n                border: {border_width}px solid #bdc3c7;\r\n                border-radius: {radius}px;\r\n                padding: {padding_std}px;\r\n            }}\r\n        \"\"\"\r\n        self.setStyleSheet(css)\r\n\r\n    def _get_available_languages(self):\r\n        \"\"\"\r\n        Returns a sorted list of language DISPLAY STRINGS (code - name format)\r\n        by asking the localisation system for available languages.\r\n        \"\"\"\r\n        languages = []\r\n        \r\n        # Get available language codes from localisation\r\n        try:\r\n            lang_codes = Localization.instance().get_available_languages()\r\n            \r\n            # Build display strings with native names\r\n            for code in lang_codes:\r\n                name = Localization.instance().get_language_name(code)\r\n                languages.append(f\"{code} - {name}\")\r\n                \r\n        except Exception as e:\r\n            print(f\"[Preferences] Error getting languages from localisation: {e}\")\r\n            \r\n            # Emergency fallback to hardcoded map\r\n            for code, name in self.LANGUAGE_MAP.items():\r\n                languages.append(f\"{code} - {name}\")\r\n        \r\n        # Sort alphabetically by code, but put 'gen_z' last\r\n        if any(item.startswith('gen_z -') for item in languages):\r\n            languages = [item for item in languages if not item.startswith('gen_z -')]\r\n            sorted_items = sorted(languages)\r\n            gen_z_name = Localization.instance().get_language_name('gen_z', fallback=True)\r\n            sorted_items.append(f\"gen_z - {gen_z_name}\")\r\n            return sorted_items\r\n        \r\n        return sorted(languages)\r\n\r\n    def init_ui(self):\r\n        \"\"\"Initialize the UI components\"\"\"\r\n        layout = QtWidgets.QVBoxLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(\r\n            DisplayScaling.scale(25), \r\n            DisplayScaling.scale(25), \r\n            DisplayScaling.scale(25), \r\n            DisplayScaling.scale(25)\r\n        )\r\n        \r\n        # Create tab widget\r\n        self.tab_widget = QtWidgets.QTabWidget()\r\n        \r\n        # General tab\r\n        self.general_tab = self._create_general_tab()\r\n        self.tab_widget.addTab(self.general_tab, \"General\")\r\n        \r\n        # Interactions tab - NEW COMBINED TAB\r\n        self.interactions_tab = self._create_interactions_tab()\r\n        self.tab_widget.addTab(self.interactions_tab, \"Interactions\")\r\n        \r\n        # Neurogenesis tab\r\n        self.neurogenesis_tab = self._create_neurogenesis_tab()\r\n        self.tab_widget.addTab(self.neurogenesis_tab, \"Neurogenesis\")\r\n        \r\n        # Hebbian tab - NEW TAB\r\n        self.hebbian_tab = self._create_hebbian_tab()\r\n        self.tab_widget.addTab(self.hebbian_tab, \"Hebbian Learning\")\r\n        \r\n        # Display tab\r\n        self.display_tab = self._create_display_tab()\r\n        self.tab_widget.addTab(self.display_tab, \"Display\")\r\n        \r\n        # Designer tab\r\n        self.designer_tab = self._create_designer_tab()\r\n        self.tab_widget.addTab(self.designer_tab, \"Designer\")\r\n        \r\n        layout.addWidget(self.tab_widget)\r\n        \r\n        # Buttons\r\n        button_layout = QtWidgets.QHBoxLayout()\r\n        button_layout.setSpacing(DisplayScaling.scale(15))\r\n        button_layout.addStretch()\r\n        \r\n        self.save_button = QtWidgets.QPushButton(\"Save && Restart\")\r\n        self.save_button.setObjectName(\"SaveButton\") # ID for styling\r\n        self.save_button.clicked.connect(self.save_and_restart)\r\n        self.save_button.setEnabled(False)\r\n        \r\n        self.cancel_button = QtWidgets.QPushButton(\"Cancel\")\r\n        self.cancel_button.clicked.connect(self.close)\r\n        \r\n        button_layout.addWidget(self.cancel_button)\r\n        button_layout.addWidget(self.save_button)\r\n        \r\n        layout.addLayout(button_layout)\r\n        self.setLayout(layout)\r\n        \r\n    def _create_general_tab(self):\r\n        \"\"\"Create General settings tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QVBoxLayout() \r\n        layout.setSpacing(DisplayScaling.scale(25))\r\n        layout.setContentsMargins(DisplayScaling.scale(40), DisplayScaling.scale(40), DisplayScaling.scale(40), DisplayScaling.scale(40))\r\n        \r\n        # Language Label\r\n        lang_label = QtWidgets.QLabel(\"Interface Language:\")\r\n        layout.addWidget(lang_label)\r\n        \r\n        # Language dropdown - Styled via CSS #LanguageCombo\r\n        self.language_combo = QtWidgets.QComboBox()\r\n        self.language_combo.setObjectName(\"LanguageCombo\") \r\n        \r\n        # POPULATE USING NEW SCANNING METHOD (.py files)\r\n        available_langs = self._get_available_languages()\r\n        self.language_combo.addItems(available_langs)\r\n        \r\n        # Connect change signal\r\n        self.language_combo.currentTextChanged.connect(self._on_change)\r\n        \r\n        layout.addWidget(self.language_combo)\r\n        \r\n        # Add description\r\n        desc_label = QtWidgets.QLabel(\"Requires restart to take effect\")\r\n        desc_label.setObjectName(\"DescriptionLabel\")\r\n        layout.addWidget(desc_label)\r\n\r\n        facts_group = QtWidgets.QGroupBox(\"🦑 Random Squid Facts\")\r\n        facts_layout = QtWidgets.QFormLayout()\r\n        facts_layout.setSpacing(DisplayScaling.scale(15))\r\n\r\n        self.facts_enabled = QtWidgets.QCheckBox(\"Enable Humboldt squid facts\")\r\n        self.facts_enabled.stateChanged.connect(self._on_change)\r\n        facts_layout.addRow(self.facts_enabled)\r\n\r\n        self.facts_interval = QtWidgets.QSpinBox()\r\n        self.facts_interval.setRange(1, 60)\r\n        self.facts_interval.setSuffix(\" minutes\")\r\n        self.facts_interval.valueChanged.connect(self._on_change)\r\n        facts_layout.addRow(\"Show every:\", self.facts_interval)\r\n\r\n        self.facts_display = QtWidgets.QSpinBox()\r\n        self.facts_display.setRange(5, 60)\r\n        self.facts_display.setSuffix(\" seconds\")\r\n        self.facts_display.valueChanged.connect(self._on_change)\r\n        facts_layout.addRow(\"Display for:\", self.facts_display)\r\n\r\n        facts_group.setLayout(facts_layout)\r\n        layout.addWidget(facts_group)\r\n\r\n        # Link Blink group\r\n        linkblink_group = QtWidgets.QGroupBox(\"Connection Blink Effects\")\r\n        linkblink_layout = QtWidgets.QFormLayout()\r\n        linkblink_layout.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.linkblink_interval_min = QtWidgets.QDoubleSpinBox()\r\n        self.linkblink_interval_min.setRange(1.0, 60.0)\r\n        self.linkblink_interval_min.setSingleStep(1.0)\r\n        self.linkblink_interval_min.setSuffix(\" sec\")\r\n        self.linkblink_interval_min.valueChanged.connect(self._on_change)\r\n        linkblink_layout.addRow(\"Min Blink Interval:\", self.linkblink_interval_min)\r\n        \r\n        self.linkblink_interval_max = QtWidgets.QDoubleSpinBox()\r\n        self.linkblink_interval_max.setRange(5.0, 120.0)\r\n        self.linkblink_interval_max.setSingleStep(1.0)\r\n        self.linkblink_interval_max.setSuffix(\" sec\")\r\n        self.linkblink_interval_max.valueChanged.connect(self._on_change)\r\n        linkblink_layout.addRow(\"Max Blink Interval:\", self.linkblink_interval_max)\r\n        \r\n        self.linkblink_duration = QtWidgets.QDoubleSpinBox()\r\n        self.linkblink_duration.setRange(0.5, 10.0)\r\n        self.linkblink_duration.setSingleStep(0.5)\r\n        self.linkblink_duration.setSuffix(\" sec\")\r\n        self.linkblink_duration.valueChanged.connect(self._on_change)\r\n        linkblink_layout.addRow(\"Blink Duration:\", self.linkblink_duration)\r\n        \r\n        linkblink_group.setLayout(linkblink_layout)\r\n        layout.addWidget(linkblink_group)\r\n        \r\n        layout.addStretch() \r\n        widget.setLayout(layout)\r\n        return widget\r\n\r\n    def _create_interactions_tab(self):\r\n        \"\"\"Combined Rock & Poop Interactions tab with two columns\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        main_layout = QtWidgets.QVBoxLayout()\r\n        main_layout.setSpacing(DisplayScaling.scale(25))\r\n        main_layout.setContentsMargins(DisplayScaling.scale(20), DisplayScaling.scale(20), DisplayScaling.scale(20), DisplayScaling.scale(20))\r\n        \r\n        # Create horizontal layout for two columns\r\n        columns_layout = QtWidgets.QHBoxLayout()\r\n        columns_layout.setSpacing(DisplayScaling.scale(30))\r\n        \r\n        # Left column - Rock Interactions\r\n        rock_frame = QtWidgets.QFrame()\r\n        rock_frame.setObjectName(\"ColumnFrame\")\r\n        rock_layout = QtWidgets.QVBoxLayout(rock_frame)\r\n        \r\n        rock_header = QtWidgets.QLabel(\"🪨 Rock Interactions\")\r\n        rock_header.setStyleSheet(\"\"\"\r\n            font-size: 20px; \r\n            font-weight: bold; \r\n            color: #34495e;\r\n            padding-bottom: 5px;\r\n            border-bottom: 2px solid #3498db;\r\n        \"\"\")\r\n        rock_layout.addWidget(rock_header)\r\n        \r\n        # Shared field titles - Rock column\r\n        rock_form = QtWidgets.QFormLayout()\r\n        rock_form.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        # Probabilities\r\n        self.rock_pickup_prob = QtWidgets.QDoubleSpinBox()\r\n        self.rock_pickup_prob.setRange(0.0, 1.0)\r\n        self.rock_pickup_prob.setSingleStep(0.1)\r\n        self.rock_pickup_prob.setDecimals(2)\r\n        self.rock_pickup_prob.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Pickup Probability:\", self.rock_pickup_prob)\r\n        \r\n        self.rock_throw_prob = QtWidgets.QDoubleSpinBox()\r\n        self.rock_throw_prob.setRange(0.0, 1.0)\r\n        self.rock_throw_prob.setSingleStep(0.1)\r\n        self.rock_throw_prob.setDecimals(2)\r\n        self.rock_throw_prob.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Throw Probability:\", self.rock_throw_prob)\r\n        \r\n        # Durations with suffix\r\n        self.rock_min_carry = QtWidgets.QDoubleSpinBox()\r\n        self.rock_min_carry.setRange(0.5, 30.0)\r\n        self.rock_min_carry.setSingleStep(0.5)\r\n        self.rock_min_carry.setSuffix(\" sec\")\r\n        self.rock_min_carry.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Min Carry Duration:\", self.rock_min_carry)\r\n        \r\n        self.rock_max_carry = QtWidgets.QDoubleSpinBox()\r\n        self.rock_max_carry.setRange(1.0, 60.0)\r\n        self.rock_max_carry.setSingleStep(0.5)\r\n        self.rock_max_carry.setSuffix(\" sec\")\r\n        self.rock_max_carry.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Max Carry Duration:\", self.rock_max_carry)\r\n        \r\n        self.rock_cooldown = QtWidgets.QDoubleSpinBox()\r\n        self.rock_cooldown.setRange(0.0, 60.0)\r\n        self.rock_cooldown.setSingleStep(1.0)\r\n        self.rock_cooldown.setSuffix(\" sec\")\r\n        self.rock_cooldown.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Cooldown After Throw:\", self.rock_cooldown)\r\n        \r\n        # Stat effects\r\n        self.rock_happiness_boost = QtWidgets.QSpinBox()\r\n        self.rock_happiness_boost.setRange(0, 100)\r\n        self.rock_happiness_boost.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Happiness Boost:\", self.rock_happiness_boost)\r\n        \r\n        self.rock_satisfaction_boost = QtWidgets.QSpinBox()\r\n        self.rock_satisfaction_boost.setRange(0, 100)\r\n        self.rock_satisfaction_boost.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Satisfaction Boost:\", self.rock_satisfaction_boost)\r\n        \r\n        self.rock_anxiety_reduction = QtWidgets.QSpinBox()\r\n        self.rock_anxiety_reduction.setRange(0, 100)\r\n        self.rock_anxiety_reduction.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Anxiety Reduction:\", self.rock_anxiety_reduction)\r\n        \r\n        # Memory\r\n        self.rock_memory_decay = QtWidgets.QDoubleSpinBox()\r\n        self.rock_memory_decay.setRange(0.0, 1.0)\r\n        self.rock_memory_decay.setSingleStep(0.05)\r\n        self.rock_memory_decay.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Memory Decay Rate:\", self.rock_memory_decay)\r\n        \r\n        self.rock_max_memories = QtWidgets.QSpinBox()\r\n        self.rock_max_memories.setRange(1, 20)\r\n        self.rock_max_memories.valueChanged.connect(self._on_change)\r\n        rock_form.addRow(\"Max Rock Memories:\", self.rock_max_memories)\r\n        \r\n        rock_layout.addLayout(rock_form)\r\n        rock_layout.addStretch()\r\n        \r\n        # Right column - Poop Interactions\r\n        poop_frame = QtWidgets.QFrame()\r\n        poop_frame.setObjectName(\"ColumnFrame\")\r\n        poop_layout = QtWidgets.QVBoxLayout(poop_frame)\r\n        \r\n        poop_header = QtWidgets.QLabel(\"💩 Poop Interactions\")\r\n        poop_header.setStyleSheet(\"\"\"\r\n            font-size: 20px; \r\n            font-weight: bold; \r\n            color: #34495e;\r\n            padding-bottom: 5px;\r\n            border-bottom: 2px solid #e67e22;\r\n        \"\"\")\r\n        poop_layout.addWidget(poop_header)\r\n        \r\n        # Shared field titles - Poop column (same labels)\r\n        poop_form = QtWidgets.QFormLayout()\r\n        poop_form.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.poop_pickup_prob = QtWidgets.QDoubleSpinBox()\r\n        self.poop_pickup_prob.setRange(0.0, 1.0)\r\n        self.poop_pickup_prob.setSingleStep(0.1)\r\n        self.poop_pickup_prob.setDecimals(2)\r\n        self.poop_pickup_prob.valueChanged.connect(self._on_change)\r\n        poop_form.addRow(\"Pickup Probability:\", self.poop_pickup_prob)\r\n        \r\n        self.poop_throw_prob = QtWidgets.QDoubleSpinBox()\r\n        self.poop_throw_prob.setRange(0.0, 1.0)\r\n        self.poop_throw_prob.setSingleStep(0.1)\r\n        self.poop_throw_prob.setDecimals(2)\r\n        self.poop_throw_prob.valueChanged.connect(self._on_change)\r\n        poop_form.addRow(\"Throw Probability:\", self.poop_throw_prob)\r\n        \r\n        self.poop_min_carry = QtWidgets.QDoubleSpinBox()\r\n        self.poop_min_carry.setRange(0.5, 30.0)\r\n        self.poop_min_carry.setSingleStep(0.5)\r\n        self.poop_min_carry.setSuffix(\" sec\")\r\n        self.poop_min_carry.valueChanged.connect(self._on_change)\r\n        poop_form.addRow(\"Min Carry Duration:\", self.poop_min_carry)\r\n        \r\n        self.poop_max_carry = QtWidgets.QDoubleSpinBox()\r\n        self.poop_max_carry.setRange(1.0, 60.0)\r\n        self.poop_max_carry.setSingleStep(0.5)\r\n        self.poop_max_carry.setSuffix(\" sec\")\r\n        self.poop_max_carry.valueChanged.connect(self._on_change)\r\n        poop_form.addRow(\"Max Carry Duration:\", self.poop_max_carry)\r\n        \r\n        self.poop_cooldown = QtWidgets.QDoubleSpinBox()\r\n        self.poop_cooldown.setRange(0.0, 60.0)\r\n        self.poop_cooldown.setSingleStep(1.0)\r\n        self.poop_cooldown.setSuffix(\" sec\")\r\n        self.poop_cooldown.valueChanged.connect(self._on_change)\r\n        poop_form.addRow(\"Cooldown After Throw:\", self.poop_cooldown)\r\n        \r\n        # Spacer for alignment (poop doesn't have these stats)\r\n        poop_form.addRow(QtWidgets.QLabel(\"\"))\r\n        poop_form.addRow(QtWidgets.QLabel(\"\"))\r\n        poop_form.addRow(QtWidgets.QLabel(\"\"))\r\n        poop_form.addRow(QtWidgets.QLabel(\"\"))\r\n        poop_form.addRow(QtWidgets.QLabel(\"\"))\r\n        \r\n        poop_layout.addLayout(poop_form)\r\n        poop_layout.addStretch()\r\n        \r\n        # Add columns to main layout\r\n        columns_layout.addWidget(rock_frame)\r\n        columns_layout.addWidget(poop_frame)\r\n        \r\n        main_layout.addLayout(columns_layout)\r\n        widget.setLayout(main_layout)\r\n        return widget\r\n    \r\n    def _create_neurogenesis_tab(self):\r\n        \"\"\"Create Neurogenesis settings tab with sub-tabs\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QVBoxLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(20), DisplayScaling.scale(20), DisplayScaling.scale(20), DisplayScaling.scale(20))\r\n        \r\n        # Enable checkbox\r\n        self.neurogenesis_enabled = QtWidgets.QCheckBox(\"Enable Neurogenesis\")\r\n        self.neurogenesis_enabled.stateChanged.connect(self._on_change)\r\n        layout.addWidget(self.neurogenesis_enabled)\r\n        \r\n        # Showmanship checkbox\r\n        self.showmanship_enabled = QtWidgets.QCheckBox(\"Enable Showmanship (Dramatic neuron creation)\")\r\n        self.showmanship_enabled.stateChanged.connect(self._on_change)\r\n        layout.addWidget(self.showmanship_enabled)\r\n        \r\n        # Pruning checkbox\r\n        self.pruning_enabled = QtWidgets.QCheckBox(\"Enable Pruning (Remove weak neurons)\")\r\n        self.pruning_enabled.stateChanged.connect(self._on_change)\r\n        layout.addWidget(self.pruning_enabled)\r\n        \r\n        # Sub-tabs\r\n        sub_tabs = QtWidgets.QTabWidget()\r\n        \r\n        # General sub-tab\r\n        general_subtab = self._create_neurogenesis_general()\r\n        sub_tabs.addTab(general_subtab, \"General\")\r\n        \r\n        # Triggers sub-tab\r\n        triggers_subtab = self._create_neurogenesis_triggers()\r\n        sub_tabs.addTab(triggers_subtab, \"Triggers\")\r\n        \r\n        # Appearance sub-tab\r\n        appearance_subtab = self._create_neurogenesis_appearance()\r\n        sub_tabs.addTab(appearance_subtab, \"Appearance\")\r\n        \r\n        # Advanced sub-tab\r\n        advanced_subtab = self._create_neurogenesis_advanced()\r\n        sub_tabs.addTab(advanced_subtab, \"Advanced\")\r\n        \r\n        layout.addWidget(sub_tabs)\r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_neurogenesis_general(self):\r\n        \"\"\"Create Neurogenesis General sub-tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        # Cooldowns\r\n        self.neuro_cooldown = QtWidgets.QDoubleSpinBox()\r\n        self.neuro_cooldown.setRange(30.0, 600.0)\r\n        self.neuro_cooldown.setSingleStep(30.0)\r\n        self.neuro_cooldown.setSuffix(\" sec\")\r\n        self.neuro_cooldown.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Global Cooldown:\", self.neuro_cooldown)\r\n        \r\n        self.neuro_per_type_cooldown = QtWidgets.QDoubleSpinBox()\r\n        self.neuro_per_type_cooldown.setRange(10.0, 120.0)\r\n        self.neuro_per_type_cooldown.setSingleStep(10.0)\r\n        self.neuro_per_type_cooldown.setSuffix(\" sec\")\r\n        self.neuro_per_type_cooldown.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Per-Type Cooldown:\", self.neuro_per_type_cooldown)\r\n        \r\n        # Max neurons\r\n        self.neuro_max_neurons = QtWidgets.QSpinBox()\r\n        self.neuro_max_neurons.setRange(10, 100)\r\n        self.neuro_max_neurons.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Max Neurons:\", self.neuro_max_neurons)\r\n        \r\n        # Initial neuron count\r\n        self.neuro_initial_count = QtWidgets.QSpinBox()\r\n        self.neuro_initial_count.setRange(5, 20)\r\n        self.neuro_initial_count.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Initial Neuron Count:\", self.neuro_initial_count)\r\n\r\n        #Positioning & Physics\r\n        pos_group = QtWidgets.QGroupBox(\"Positioning & Physics\")\r\n        pos_layout = QtWidgets.QFormLayout()\r\n        pos_layout.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.random_start_pos = QtWidgets.QCheckBox(\"Randomize Start Positions\")\r\n        self.random_start_pos.stateChanged.connect(self._on_change)\r\n        pos_layout.addRow(self.random_start_pos)\r\n        \r\n        self.force_bounds = QtWidgets.QCheckBox(\"Enforce Canvas Bounds\")\r\n        self.force_bounds.stateChanged.connect(self._on_change)\r\n        pos_layout.addRow(self.force_bounds)\r\n        \r\n        self.canvas_padding = QtWidgets.QSpinBox()\r\n        self.canvas_padding.setRange(0, 300)\r\n        self.canvas_padding.setSingleStep(10)\r\n        self.canvas_padding.setSuffix(\" px\")\r\n        self.canvas_padding.valueChanged.connect(self._on_change)\r\n        pos_layout.addRow(\"Canvas Padding:\", self.canvas_padding)\r\n        \r\n        self.centering_force = QtWidgets.QDoubleSpinBox()\r\n        self.centering_force.setRange(0.0, 0.5)\r\n        self.centering_force.setSingleStep(0.01)\r\n        self.centering_force.setDecimals(3)\r\n        self.centering_force.valueChanged.connect(self._on_change)\r\n        pos_layout.addRow(\"Centering Force:\", self.centering_force)\r\n        \r\n        pos_group.setLayout(pos_layout)\r\n        layout.addRow(pos_group)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_neurogenesis_triggers(self):\r\n        \"\"\"Create Neurogenesis Triggers sub-tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        main_layout = QtWidgets.QVBoxLayout()\r\n        main_layout.setSpacing(DisplayScaling.scale(20))\r\n        \r\n        # Scroll area for smaller screens if needed\r\n        scroll = QtWidgets.QScrollArea()\r\n        scroll.setWidgetResizable(True)\r\n        content_widget = QtWidgets.QWidget()\r\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\r\n        content_layout.setSpacing(DisplayScaling.scale(25))\r\n        \r\n        # Novelty trigger\r\n        novelty_group = QtWidgets.QGroupBox(\"Novelty Trigger\")\r\n        novelty_layout = QtWidgets.QFormLayout()\r\n        novelty_layout.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.novelty_enabled = QtWidgets.QCheckBox(\"Enabled\")\r\n        self.novelty_enabled.stateChanged.connect(self._on_change)\r\n        novelty_layout.addRow(self.novelty_enabled)\r\n        \r\n        self.novelty_threshold = QtWidgets.QDoubleSpinBox()\r\n        self.novelty_threshold.setRange(0.5, 10.0)\r\n        self.novelty_threshold.setSingleStep(0.5)\r\n        self.novelty_threshold.valueChanged.connect(self._on_change)\r\n        novelty_layout.addRow(\"Threshold:\", self.novelty_threshold)\r\n        \r\n        self.novelty_decay = QtWidgets.QDoubleSpinBox()\r\n        self.novelty_decay.setRange(0.0, 1.0)\r\n        self.novelty_decay.setSingleStep(0.05)\r\n        self.novelty_decay.valueChanged.connect(self._on_change)\r\n        novelty_layout.addRow(\"Decay Rate:\", self.novelty_decay)\r\n        \r\n        novelty_group.setLayout(novelty_layout)\r\n        content_layout.addWidget(novelty_group)\r\n        \r\n        # Stress trigger\r\n        stress_group = QtWidgets.QGroupBox(\"Stress Trigger\")\r\n        stress_layout = QtWidgets.QFormLayout()\r\n        stress_layout.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.stress_enabled = QtWidgets.QCheckBox(\"Enabled\")\r\n        self.stress_enabled.stateChanged.connect(self._on_change)\r\n        stress_layout.addRow(self.stress_enabled)\r\n        \r\n        self.stress_threshold = QtWidgets.QDoubleSpinBox()\r\n        self.stress_threshold.setRange(0.5, 10.0)\r\n        self.stress_threshold.setSingleStep(0.5)\r\n        self.stress_threshold.valueChanged.connect(self._on_change)\r\n        stress_layout.addRow(\"Threshold:\", self.stress_threshold)\r\n        \r\n        self.stress_decay = QtWidgets.QDoubleSpinBox()\r\n        self.stress_decay.setRange(0.0, 1.0)\r\n        self.stress_decay.setSingleStep(0.05)\r\n        self.stress_decay.valueChanged.connect(self._on_change)\r\n        stress_layout.addRow(\"Decay Rate:\", self.stress_decay)\r\n        \r\n        stress_group.setLayout(stress_layout)\r\n        content_layout.addWidget(stress_group)\r\n        \r\n        # Reward trigger\r\n        reward_group = QtWidgets.QGroupBox(\"Reward Trigger\")\r\n        reward_layout = QtWidgets.QFormLayout()\r\n        reward_layout.setSpacing(DisplayScaling.scale(15))\r\n        \r\n        self.reward_enabled = QtWidgets.QCheckBox(\"Enabled\")\r\n        self.reward_enabled.stateChanged.connect(self._on_change)\r\n        reward_layout.addRow(self.reward_enabled)\r\n        \r\n        self.reward_threshold = QtWidgets.QDoubleSpinBox()\r\n        self.reward_threshold.setRange(0.5, 10.0)\r\n        self.reward_threshold.setSingleStep(0.5)\r\n        self.reward_threshold.valueChanged.connect(self._on_change)\r\n        reward_layout.addRow(\"Threshold:\", self.reward_threshold)\r\n        \r\n        self.reward_decay = QtWidgets.QDoubleSpinBox()\r\n        self.reward_decay.setRange(0.0, 1.0)\r\n        self.reward_decay.setSingleStep(0.05)\r\n        self.reward_decay.valueChanged.connect(self._on_change)\r\n        reward_layout.addRow(\"Decay Rate:\", self.reward_decay)\r\n        \r\n        reward_group.setLayout(reward_layout)\r\n        content_layout.addWidget(reward_group)\r\n        \r\n        scroll.setWidget(content_widget)\r\n        main_layout.addWidget(scroll)\r\n        widget.setLayout(main_layout)\r\n        return widget\r\n    \r\n    def _create_neurogenesis_appearance(self):\r\n        \"\"\"Create Neurogenesis Appearance sub-tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        # Colours for each type\r\n        self.novelty_color = self._create_color_button()\r\n        self.novelty_color.clicked.connect(self._on_change)\r\n        layout.addRow(\"Novelty Color:\", self.novelty_color)\r\n        \r\n        self.stress_color = self._create_color_button()\r\n        self.stress_color.clicked.connect(self._on_change)\r\n        layout.addRow(\"Stress Color:\", self.stress_color)\r\n        \r\n        self.reward_color = self._create_color_button()\r\n        self.reward_color.clicked.connect(self._on_change)\r\n        layout.addRow(\"Reward Color:\", self.reward_color)\r\n        \r\n        # Visual effects\r\n        self.highlight_duration = QtWidgets.QDoubleSpinBox()\r\n        self.highlight_duration.setRange(1.0, 20.0)\r\n        self.highlight_duration.setSingleStep(0.5)\r\n        self.highlight_duration.setSuffix(\" sec\")\r\n        self.highlight_duration.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Highlight Duration:\", self.highlight_duration)\r\n        \r\n        self.pulse_effect = QtWidgets.QCheckBox(\"Enable Pulse Effect\")\r\n        self.pulse_effect.stateChanged.connect(self._on_change)\r\n        layout.addRow(self.pulse_effect)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_neurogenesis_advanced(self):\r\n        \"\"\"NEW: Create Neurogenesis Advanced sub-tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        # Advanced neurogenesis parameters\r\n        self.max_novelty_neurons = QtWidgets.QSpinBox()\r\n        self.max_novelty_neurons.setRange(1, 20)\r\n        self.max_novelty_neurons.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Max Novelty Neurons:\", self.max_novelty_neurons)\r\n        \r\n        self.pattern_threshold = QtWidgets.QSpinBox()\r\n        self.pattern_threshold.setRange(1, 10)\r\n        self.pattern_threshold.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Pattern Threshold:\", self.pattern_threshold)\r\n        \r\n        self.experience_buffer_size = QtWidgets.QSpinBox()\r\n        self.experience_buffer_size.setRange(10, 100)\r\n        self.experience_buffer_size.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Experience Buffer Size:\", self.experience_buffer_size)\r\n        \r\n        self.min_utility_for_keep = QtWidgets.QDoubleSpinBox()\r\n        self.min_utility_for_keep.setRange(0.0, 1.0)\r\n        self.min_utility_for_keep.setSingleStep(0.05)\r\n        self.min_utility_for_keep.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Min Utility for Keep:\", self.min_utility_for_keep)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_hebbian_tab(self):\r\n        \"\"\"NEW: Create Hebbian Learning tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        hebbian_desc = QtWidgets.QLabel(\r\n            \"Hebbian learning strengthens connections between neurons that fire together. \"\r\n            \"This simulates the 'cells that fire together, wire together' principle.\"\r\n        )\r\n        hebbian_desc.setWordWrap(True)\r\n        hebbian_desc.setStyleSheet(\"color: #7f8c8d; font-style: italic;\")\r\n        layout.addRow(hebbian_desc)\r\n        \r\n        layout.addRow(QtWidgets.QLabel(\"\"))  # Spacer\r\n        \r\n        self.hebbian_learning_interval = QtWidgets.QSpinBox()\r\n        self.hebbian_learning_interval.setRange(1, 300)\r\n        self.hebbian_learning_interval.setSuffix(\" sec\")\r\n        self.hebbian_learning_interval.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Learning Interval:\", self.hebbian_learning_interval)\r\n        \r\n        self.hebbian_base_rate = QtWidgets.QDoubleSpinBox()\r\n        self.hebbian_base_rate.setRange(0.0, 1.0)\r\n        self.hebbian_base_rate.setSingleStep(0.01)\r\n        self.hebbian_base_rate.setDecimals(3)\r\n        self.hebbian_base_rate.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Base Learning Rate:\", self.hebbian_base_rate)\r\n        \r\n        self.hebbian_weight_decay = QtWidgets.QDoubleSpinBox()\r\n        self.hebbian_weight_decay.setRange(0.0, 0.5)\r\n        self.hebbian_weight_decay.setSingleStep(0.01)\r\n        self.hebbian_weight_decay.setDecimals(3)\r\n        self.hebbian_weight_decay.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Weight Decay:\", self.hebbian_weight_decay)\r\n        \r\n        self.hebbian_min_weight = QtWidgets.QDoubleSpinBox()\r\n        self.hebbian_min_weight.setRange(-2.0, 0.0)\r\n        self.hebbian_min_weight.setSingleStep(0.1)\r\n        self.hebbian_min_weight.setDecimals(1)\r\n        self.hebbian_min_weight.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Min Weight:\", self.hebbian_min_weight)\r\n        \r\n        self.hebbian_max_weight = QtWidgets.QDoubleSpinBox()\r\n        self.hebbian_max_weight.setRange(0.0, 2.0)\r\n        self.hebbian_max_weight.setSingleStep(0.1)\r\n        self.hebbian_max_weight.setDecimals(1)\r\n        self.hebbian_max_weight.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Max Weight:\", self.hebbian_max_weight)\r\n        \r\n        self.hebbian_max_pairs = QtWidgets.QSpinBox()\r\n        self.hebbian_max_pairs.setRange(1, 10)\r\n        self.hebbian_max_pairs.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Max Hebbian Pairs:\", self.hebbian_max_pairs)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_color_button(self):\r\n        \"\"\"Create a colour picker button\"\"\"\r\n        button = QtWidgets.QPushButton()\r\n        # Scale the colour button size\r\n        size = DisplayScaling.scale(40)\r\n        button.setFixedSize(size * 2, size)\r\n        button.setStyleSheet(\"background-color: white; border: 1px solid gray;\")\r\n        button.clicked.connect(self._pick_color)\r\n        return button\r\n    \r\n    def _pick_color(self):\r\n        \"\"\"Open colour picker dialog\"\"\"\r\n        button = self.sender()\r\n        color = QtWidgets.QColorDialog.getColor()\r\n        if color.isValid():\r\n            button.setStyleSheet(f\"background-color: {color.name()}; border: 1px solid gray;\")\r\n            self._on_change()\r\n    \r\n    def _create_display_tab(self):\r\n\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        # Neuron settings\r\n        self.neuron_radius = QtWidgets.QSpinBox()\r\n        self.neuron_radius.setRange(10, 50)\r\n        self.neuron_radius.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Neuron Radius:\", self.neuron_radius)\r\n        \r\n        self.neuron_font_size = QtWidgets.QSpinBox()\r\n        self.neuron_font_size.setRange(6, 20)\r\n        self.neuron_font_size.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Neuron Label Size:\", self.neuron_font_size)\r\n        \r\n        # Connection settings\r\n        self.connection_width = QtWidgets.QDoubleSpinBox()\r\n        self.connection_width.setRange(0.5, 5.0)\r\n        self.connection_width.setSingleStep(0.1)\r\n        self.connection_width.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Line Width:\", self.connection_width)\r\n        \r\n        # Button settings\r\n        self.button_font_size = QtWidgets.QSpinBox()\r\n        self.button_font_size.setRange(10, 24)\r\n        self.button_font_size.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Button Font Size:\", self.button_font_size)\r\n        \r\n        self.button_width = QtWidgets.QSpinBox()\r\n        self.button_width.setRange(80, 200)\r\n        self.button_width.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Button Width:\", self.button_width)\r\n        \r\n        self.button_height = QtWidgets.QSpinBox()\r\n        self.button_height.setRange(30, 80)\r\n        self.button_height.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Button Height:\", self.button_height)\r\n        \r\n        self.button_spacing = QtWidgets.QSpinBox()\r\n        self.button_spacing.setRange(10, 50)\r\n        self.button_spacing.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Button Spacing:\", self.button_spacing)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _create_designer_tab(self):\r\n        \"\"\"Create Designer settings tab\"\"\"\r\n        widget = QtWidgets.QWidget()\r\n        layout = QtWidgets.QFormLayout()\r\n        layout.setSpacing(DisplayScaling.scale(20))\r\n        layout.setContentsMargins(DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25), DisplayScaling.scale(25))\r\n        \r\n        designer_desc = QtWidgets.QLabel(\r\n            \"Designer mode settings for creating and editing neural networks. \"\r\n            \"These constraints ensure neurons are positioned optimally.\"\r\n        )\r\n        designer_desc.setWordWrap(True)\r\n        designer_desc.setStyleSheet(\"color: #7f8c8d; font-style: italic;\")\r\n        layout.addRow(designer_desc)\r\n        \r\n        layout.addRow(QtWidgets.QLabel(\"\"))  # Spacer\r\n        \r\n        self.designer_min_distance = QtWidgets.QSpinBox()\r\n        self.designer_min_distance.setRange(50, 300)\r\n        self.designer_min_distance.setSuffix(\" px\")\r\n        self.designer_min_distance.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Min Neuron Distance:\", self.designer_min_distance)\r\n        \r\n        self.designer_max_distance = QtWidgets.QSpinBox()\r\n        self.designer_max_distance.setRange(300, 1000)\r\n        self.designer_max_distance.setSuffix(\" px\")\r\n        self.designer_max_distance.valueChanged.connect(self._on_change)\r\n        layout.addRow(\"Max Neuron Distance:\", self.designer_max_distance)\r\n        \r\n        widget.setLayout(layout)\r\n        return widget\r\n    \r\n    def _on_change(self):\r\n        \"\"\"Mark that changes have been made\"\"\"\r\n        self.changes_made = True\r\n        self.save_button.setEnabled(True)\r\n    \r\n    def load_current_config(self):\r\n        \"\"\"Load current config values into UI\"\"\"\r\n        # General\r\n        current_lang = self.config_manager.get_language()\r\n        \r\n        # Find and set the matching language display string\r\n        for i in range(self.language_combo.count()):\r\n            item_text = self.language_combo.itemText(i)\r\n            if item_text.startswith(f\"{current_lang} -\"):\r\n                self.language_combo.setCurrentIndex(i)\r\n                break\r\n\r\n        self.facts_enabled.setChecked(\r\n            self.config_manager.config.getboolean('Facts', 'enabled', fallback=True)\r\n        )\r\n        self.facts_interval.setValue(\r\n            self.config_manager.config.getint('Facts', 'interval_minutes', fallback=5)\r\n        )\r\n        self.facts_display.setValue(\r\n            self.config_manager.config.getint('Facts', 'display_seconds', fallback=18)\r\n        )\r\n\r\n        # LinkBlink config\r\n        linkblink_config = self.config_manager.get_linkblink_config()\r\n        self.linkblink_interval_min.setValue(linkblink_config['interval_min'])\r\n        self.linkblink_interval_max.setValue(linkblink_config['interval_max'])\r\n        self.linkblink_duration.setValue(linkblink_config['blink_duration'])\r\n        \r\n        # Rock config\r\n        rock_config = self.config_manager.get_rock_config()\r\n        self.rock_pickup_prob.setValue(rock_config['pickup_prob'])\r\n        self.rock_throw_prob.setValue(rock_config['throw_prob'])\r\n        self.rock_min_carry.setValue(rock_config['min_carry_duration'])\r\n        self.rock_max_carry.setValue(rock_config['max_carry_duration'])\r\n        self.rock_cooldown.setValue(rock_config['cooldown_after_throw'])\r\n        self.rock_happiness_boost.setValue(rock_config['happiness_boost'])\r\n        self.rock_satisfaction_boost.setValue(rock_config['satisfaction_boost'])\r\n        self.rock_anxiety_reduction.setValue(rock_config['anxiety_reduction'])\r\n        self.rock_memory_decay.setValue(rock_config['memory_decay_rate'])\r\n        self.rock_max_memories.setValue(rock_config['max_rock_memories'])\r\n        \r\n        # Poop config\r\n        poop_config = self.config_manager.get_poop_config()\r\n        self.poop_pickup_prob.setValue(poop_config['pickup_prob'])\r\n        self.poop_throw_prob.setValue(poop_config['throw_prob'])\r\n        self.poop_min_carry.setValue(poop_config['min_carry_duration'])\r\n        self.poop_max_carry.setValue(poop_config['max_carry_duration'])\r\n        self.poop_cooldown.setValue(poop_config['cooldown_after_throw'])\r\n        \r\n        # Neurogenesis config\r\n        neuro_config = self.config_manager.get_neurogenesis_config()\r\n        self.neurogenesis_enabled.setChecked(neuro_config['general']['enabled'])\r\n        self.showmanship_enabled.setChecked(neuro_config['general']['showmanship'])\r\n        self.pruning_enabled.setChecked(neuro_config['general']['pruning_enabled'])\r\n        self.neuro_cooldown.setValue(neuro_config['general']['cooldown'])\r\n        self.neuro_per_type_cooldown.setValue(neuro_config['general']['per_type_cooldown'])\r\n        self.neuro_max_neurons.setValue(neuro_config['general']['max_neurons'])\r\n        self.neuro_initial_count.setValue(neuro_config['general']['initial_neuron_count'])\r\n        \r\n        # Advanced neurogenesis\r\n        self.max_novelty_neurons.setValue(neuro_config['general']['max_novelty_neurons'])\r\n        self.pattern_threshold.setValue(neuro_config['general']['pattern_threshold'])\r\n        self.experience_buffer_size.setValue(neuro_config['general']['experience_buffer_size'])\r\n        self.min_utility_for_keep.setValue(neuro_config['general']['min_utility_for_keep'])\r\n        \r\n        # Load Positioning & Physics\r\n        raw_config = self.config_manager.config\r\n        sec_props = 'Neurogenesis.NeuronProperties'\r\n        \r\n        self.random_start_pos.setChecked(raw_config.getboolean(sec_props, 'randomize_start_positions', fallback=True))\r\n        self.force_bounds.setChecked(raw_config.getboolean(sec_props, 'force_bounds', fallback=True))\r\n        self.canvas_padding.setValue(raw_config.getint(sec_props, 'canvas_padding', fallback=60))\r\n        self.centering_force.setValue(raw_config.getfloat(sec_props, 'centering_force', fallback=0.02))\r\n\r\n        # Triggers\r\n        self.novelty_enabled.setChecked(neuro_config['triggers']['novelty']['enabled'])\r\n        self.novelty_threshold.setValue(neuro_config['triggers']['novelty']['threshold'])\r\n        self.novelty_decay.setValue(neuro_config['triggers']['novelty']['decay_rate'])\r\n        \r\n        self.stress_enabled.setChecked(neuro_config['triggers']['stress']['enabled'])\r\n        self.stress_threshold.setValue(neuro_config['triggers']['stress']['threshold'])\r\n        self.stress_decay.setValue(neuro_config['triggers']['stress']['decay_rate'])\r\n        \r\n        self.reward_enabled.setChecked(neuro_config['triggers']['reward']['enabled'])\r\n        self.reward_threshold.setValue(neuro_config['triggers']['reward']['threshold'])\r\n        self.reward_decay.setValue(neuro_config['triggers']['reward']['decay_rate'])\r\n        \r\n        # Appearance\r\n        self._set_color_button(self.novelty_color, neuro_config['appearance']['colors']['novelty'])\r\n        self._set_color_button(self.stress_color, neuro_config['appearance']['colors']['stress'])\r\n        self._set_color_button(self.reward_color, neuro_config['appearance']['colors']['reward'])\r\n        self.highlight_duration.setValue(neuro_config['visual_effects']['highlight_duration'])\r\n        self.pulse_effect.setChecked(neuro_config['visual_effects']['pulse_effect'])\r\n        \r\n        # Hebbian config\r\n        hebbian_config = self.config_manager.get_hebbian_config()\r\n        self.hebbian_learning_interval.setValue(hebbian_config['learning_interval'])\r\n        self.hebbian_base_rate.setValue(hebbian_config['base_learning_rate'])\r\n        self.hebbian_weight_decay.setValue(hebbian_config['weight_decay'])\r\n        self.hebbian_min_weight.setValue(hebbian_config['min_weight'])\r\n        self.hebbian_max_weight.setValue(hebbian_config['max_weight'])\r\n        self.hebbian_max_pairs.setValue(hebbian_config['max_hebbian_pairs'])\r\n        \r\n        # Display\r\n        display_config = self.config_manager.get_display_config()\r\n        self.neuron_radius.setValue(display_config['neuron_radius'])\r\n        self.neuron_font_size.setValue(display_config['neuron_label_font_size'])\r\n        self.connection_width.setValue(display_config['connection_line_width'])\r\n        self.button_font_size.setValue(display_config['button_font_size'])\r\n        self.button_width.setValue(display_config['button_width'])\r\n        self.button_height.setValue(display_config['button_height'])\r\n        self.button_spacing.setValue(display_config['button_spacing'])\r\n        \r\n        # Designer\r\n        self.designer_min_distance.setValue(self.config_manager.config.getint('Designer', 'designer_min_neuron_distance', fallback=100))\r\n        self.designer_max_distance.setValue(self.config_manager.config.getint('Designer', 'designer_max_neuron_distance', fallback=600))\r\n        \r\n        # Reset changes flag\r\n        self.changes_made = False\r\n    \r\n    def _set_color_button(self, button, rgb_list):\r\n        \"\"\"Set color button background from RGB list\"\"\"\r\n        color = QtGui.QColor(*rgb_list)\r\n        button.setStyleSheet(f\"background-color: {color.name()}; border: 1px solid gray;\")\r\n    \r\n    def save_and_restart(self):\r\n        \"\"\"Save configuration and prompt for restart\"\"\"\r\n        # Save all settings\r\n        self._save_all_settings()\r\n        \r\n        # Show restart prompt\r\n        reply = QtWidgets.QMessageBox.question(\r\n            self,\r\n            \"Restart Required\",\r\n            \"Configuration saved successfully!\\n\\nA restart is required for changes to take effect.\\n\\nWould you like to restart the application now?\",\r\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,\r\n            QtWidgets.QMessageBox.Yes\r\n        )\r\n        \r\n        if reply == QtWidgets.QMessageBox.Yes:\r\n            self._restart_application()\r\n        else:\r\n            self.close()\r\n    \r\n    def _save_all_settings(self):\r\n        \"\"\"Save all settings to config.ini\"\"\"\r\n        # General\r\n        if not self.config_manager.config.has_section('General'):\r\n            self.config_manager.config.add_section('General')\r\n        \r\n        # EXTRACT language code from display string\r\n        selected_lang_display = self.language_combo.currentText()\r\n        language_code = selected_lang_display.split(' - ')[0]\r\n        self.config_manager.config.set('General', 'language', language_code)\r\n\r\n        if not self.config_manager.config.has_section('Facts'):\r\n            self.config_manager.config.add_section('Facts')\r\n        self.config_manager.config.set('Facts', 'enabled', str(self.facts_enabled.isChecked()))\r\n        self.config_manager.config.set('Facts', 'interval_minutes', str(self.facts_interval.value()))\r\n        self.config_manager.config.set('Facts', 'display_seconds', str(self.facts_display.value()))\r\n\r\n        # LinkBlink\r\n        if not self.config_manager.config.has_section('LinkBlink'):\r\n            self.config_manager.config.add_section('LinkBlink')\r\n        self.config_manager.config.set('LinkBlink', 'interval_min', str(self.linkblink_interval_min.value()))\r\n        self.config_manager.config.set('LinkBlink', 'interval_max', str(self.linkblink_interval_max.value()))\r\n        self.config_manager.config.set('LinkBlink', 'blink_duration', str(self.linkblink_duration.value()))\r\n        \r\n        # Rock Interactions\r\n        if not self.config_manager.config.has_section('RockInteractions'):\r\n            self.config_manager.config.add_section('RockInteractions')\r\n        self.config_manager.config.set('RockInteractions', 'pickup_probability', str(self.rock_pickup_prob.value()))\r\n        self.config_manager.config.set('RockInteractions', 'throw_probability', str(self.rock_throw_prob.value()))\r\n        self.config_manager.config.set('RockInteractions', 'min_carry_duration', str(self.rock_min_carry.value()))\r\n        self.config_manager.config.set('RockInteractions', 'max_carry_duration', str(self.rock_max_carry.value()))\r\n        self.config_manager.config.set('RockInteractions', 'cooldown_after_throw', str(self.rock_cooldown.value()))\r\n        self.config_manager.config.set('RockInteractions', 'happiness_boost', str(self.rock_happiness_boost.value()))\r\n        self.config_manager.config.set('RockInteractions', 'satisfaction_boost', str(self.rock_satisfaction_boost.value()))\r\n        self.config_manager.config.set('RockInteractions', 'anxiety_reduction', str(self.rock_anxiety_reduction.value()))\r\n        self.config_manager.config.set('RockInteractions', 'memory_decay_rate', str(self.rock_memory_decay.value()))\r\n        self.config_manager.config.set('RockInteractions', 'max_rock_memories', str(self.rock_max_memories.value()))\r\n        \r\n        # Poop Interactions\r\n        if not self.config_manager.config.has_section('PoopInteractions'):\r\n            self.config_manager.config.add_section('PoopInteractions')\r\n        self.config_manager.config.set('PoopInteractions', 'pickup_probability', str(self.poop_pickup_prob.value()))\r\n        self.config_manager.config.set('PoopInteractions', 'throw_probability', str(self.poop_throw_prob.value()))\r\n        self.config_manager.config.set('PoopInteractions', 'min_carry_duration', str(self.poop_min_carry.value()))\r\n        self.config_manager.config.set('PoopInteractions', 'max_carry_duration', str(self.poop_max_carry.value()))\r\n        self.config_manager.config.set('PoopInteractions', 'cooldown_after_throw', str(self.poop_cooldown.value()))\r\n        \r\n        # Neurogenesis\r\n        if not self.config_manager.config.has_section('Neurogenesis'):\r\n            self.config_manager.config.add_section('Neurogenesis')\r\n        self.config_manager.config.set('Neurogenesis', 'enabled', str(self.neurogenesis_enabled.isChecked()))\r\n        self.config_manager.config.set('Neurogenesis', 'showmanship', str(self.showmanship_enabled.isChecked()))\r\n        self.config_manager.config.set('Neurogenesis', 'pruning_enabled', str(self.pruning_enabled.isChecked()))\r\n        self.config_manager.config.set('Neurogenesis', 'cooldown', str(self.neuro_cooldown.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'per_type_cooldown', str(self.neuro_per_type_cooldown.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'max_neurons', str(self.neuro_max_neurons.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'initial_neuron_count', str(self.neuro_initial_count.value()))\r\n        \r\n        # Advanced neurogenesis\r\n        self.config_manager.config.set('Neurogenesis', 'max_novelty_neurons', str(self.max_novelty_neurons.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'pattern_threshold', str(self.pattern_threshold.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'experience_buffer_size', str(self.experience_buffer_size.value()))\r\n        self.config_manager.config.set('Neurogenesis', 'min_utility_for_keep', str(self.min_utility_for_keep.value()))\r\n        \r\n        # Neurogenesis.NeuronProperties (Positioning)\r\n        if not self.config_manager.config.has_section('Neurogenesis.NeuronProperties'):\r\n            self.config_manager.config.add_section('Neurogenesis.NeuronProperties')\r\n            \r\n        self.config_manager.config.set('Neurogenesis.NeuronProperties', 'randomize_start_positions', str(self.random_start_pos.isChecked()))\r\n        self.config_manager.config.set('Neurogenesis.NeuronProperties', 'force_bounds', str(self.force_bounds.isChecked()))\r\n        self.config_manager.config.set('Neurogenesis.NeuronProperties', 'canvas_padding', str(self.canvas_padding.value()))\r\n        self.config_manager.config.set('Neurogenesis.NeuronProperties', 'centering_force', str(self.centering_force.value()))\r\n\r\n        # Save to file via ConfigManager helper\r\n        self.config_manager._save_config()\r\n\r\n    def _restart_application(self):\r\n        \"\"\"Restarts the current python process\"\"\"\r\n        python = sys.executable\r\n        os.execl(python, python, *sys.argv)"
  },
  {
    "path": "src/save_manager.py",
    "content": "import json\r\nimport os\r\nimport zipfile\r\nimport shutil\r\nfrom datetime import datetime\r\nfrom uuid import UUID\r\n\r\nclass DateTimeEncoder(json.JSONEncoder):\r\n    def default(self, obj):\r\n        if isinstance(obj, datetime):\r\n            return obj.isoformat()\r\n        if isinstance(obj, UUID):\r\n            return str(obj)\r\n        return super().default(obj)\r\n\r\nclass SaveManager:\r\n    def __init__(self, save_directory=\"saves\"):\r\n        self.save_directory = save_directory\r\n        os.makedirs(save_directory, exist_ok=True)\r\n\r\n        self.autosave_path = os.path.join(save_directory, \"autosave.zip\")\r\n        self.manual_path = os.path.join(save_directory, \"save_data.zip\")  # This will be updated dynamically\r\n        self.backup_path = os.path.join(save_directory, \"autosave_backup.zip\")\r\n\r\n    # --------------------------------------------------\r\n    # Public helpers\r\n    # --------------------------------------------------\r\n    def save_exists(self, autosave=False):\r\n        if autosave:\r\n            return os.path.exists(self.autosave_path)\r\n        else:\r\n            # For manual saves, we need to check if any UUID-based save exists\r\n            return self._get_manual_save_path() is not None\r\n\r\n    def get_latest_save(self):\r\n        \"\"\"Return the most recent save: permanent manual > autosave > nothing\"\"\"\r\n        # 1. Look for permanent manual save: {uuid}.zip\r\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic and self.tamagotchi_logic.squid:\r\n            uuid_str = str(self.tamagotchi_logic.squid.uuid)\r\n            manual_path = self._get_save_path_for_uuid(uuid_str, is_autosave=False)\r\n            if os.path.exists(manual_path):\r\n                return manual_path\r\n\r\n        # 2. Fallback: autosave for this UUID\r\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic and self.tamagotchi_logic.squid:\r\n            uuid_str = str(self.tamagotchi_logic.squid.uuid)\r\n            autosave_path = self._get_save_path_for_uuid(uuid_str, is_autosave=True)\r\n            if os.path.exists(autosave_path):\r\n                return autosave_path\r\n\r\n        # Optional: fallback to any old-style timestamped save (for backward compatibility)\r\n        old_manual = self._get_manual_save_path()\r\n        if old_manual:\r\n            return old_manual\r\n\r\n        return None\r\n\r\n    def _get_manual_save_path(self):\r\n        \"\"\"Find the most recent manual save file (UUID-based)\"\"\"\r\n        try:\r\n            # Look for any .zip files in save directory that aren't autosave or backup\r\n            save_files = [f for f in os.listdir(self.save_directory) \r\n                         if f.endswith('.zip') and f not in ['autosave.zip', 'autosave_backup.zip']]\r\n            \r\n            if save_files:\r\n                # Return the most recently modified one\r\n                save_files = [os.path.join(self.save_directory, f) for f in save_files]\r\n                return max(save_files, key=os.path.getmtime)\r\n            return None\r\n        except (FileNotFoundError, ValueError):\r\n            return None\r\n\r\n    def _get_save_path_for_uuid(self, uuid_str, is_autosave=False):\r\n        \"\"\"Generate save path for a specific UUID.\r\n        \r\n        - Autosaves → {uuid}_autosave.zip\r\n        - Manual saves → {uuid}.zip   ← PERMANENT, single file (no timestamp)\r\n        \"\"\"\r\n        safe_uuid = str(uuid_str).replace('-', '_')\r\n        \r\n        if is_autosave:\r\n            return os.path.join(self.save_directory, f\"{safe_uuid}_autosave.zip\")\r\n        else:\r\n            # Single permanent manual save file — no timestamp!\r\n            return os.path.join(self.save_directory, f\"{safe_uuid}.zip\")\r\n\r\n    # --------------------------------------------------\r\n    # Save API\r\n    # --------------------------------------------------\r\n\r\n    def save_game(self, save_data: dict, is_autosave: bool = False) -> str | None:\r\n        \"\"\"Save game with proper UUID management.\"\"\"\r\n        try:\r\n            from PyQt5.QtWidgets import QMessageBox\r\n            import uuid\r\n\r\n            # === PRIORITY 1: Use the live squid's UUID if available (most reliable) ===\r\n            live_squid = None\r\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\r\n                live_squid = getattr(self.tamagotchi_logic, 'squid', None)\r\n\r\n            if live_squid and hasattr(live_squid, 'uuid') and live_squid.uuid:\r\n                squid_uuid = str(live_squid.uuid)\r\n                print(f\"[SaveManager] Using live squid UUID: {squid_uuid}\")\r\n            else:\r\n                # Fallback: extract from save_data (old behavior)\r\n                squid_uuid = save_data.get('game_state', {}).get('squid', {}).get('uuid')\r\n                if not squid_uuid:\r\n                    squid_uuid = str(uuid.uuid4())\r\n                    save_data.setdefault('game_state', {}).setdefault('squid', {})['uuid'] = squid_uuid\r\n                    print(f\"[SaveManager] Generated new UUID (no live squid): {squid_uuid}\")\r\n\r\n            # Ensure UUID is in save_data for consistency\r\n            save_data.setdefault('game_state', {}).setdefault('squid', {})['uuid'] = squid_uuid\r\n\r\n            # Determine save path\r\n            target_path = self._get_save_path_for_uuid(squid_uuid, is_autosave=is_autosave)\r\n\r\n            # Skip if identical\r\n            if os.path.exists(target_path):\r\n                existing_data = self._load_single_save(target_path)\r\n                if existing_data and self._are_saves_identical(existing_data, save_data):\r\n                    print(f\"[SaveManager] Identical save already exists → skipping: {target_path}\")\r\n                    return target_path\r\n\r\n            # Backup old save\r\n            backup_path = None\r\n            if os.path.exists(target_path):\r\n                backup_path = target_path + \".backup\"\r\n                shutil.copy2(target_path, backup_path)\r\n\r\n            # Write to temp file\r\n            temp_path = target_path + \".tmp\"\r\n            with zipfile.ZipFile(temp_path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:\r\n                for key, data in save_data.items():\r\n                    zf.writestr(f\"{key}.json\", json.dumps(data, indent=4, cls=DateTimeEncoder))\r\n                zf.writestr(\"uuid.txt\", f\"SquidSignature    {squid_uuid}\")\r\n\r\n            # Atomic replace\r\n            if os.path.exists(target_path):\r\n                os.replace(target_path, target_path + \".old\")\r\n            os.replace(temp_path, target_path)\r\n\r\n            # Cleanup\r\n            for old in [f for f in os.listdir(self.save_directory) if f.endswith(('.old', '.backup'))]:\r\n                try:\r\n                    os.remove(os.path.join(self.save_directory, old))\r\n                except:\r\n                    pass\r\n\r\n            print(f\"[SaveManager] Save successful → {target_path}\")\r\n            return target_path\r\n\r\n        except Exception as e:\r\n            print(f\"[SaveManager] Save failed: {e}\")\r\n            import traceback\r\n            traceback.print_exc()\r\n            return None\r\n\r\n    def _are_saves_identical(self, save1, save2):\r\n        \"\"\"Compare two save dictionaries for equality.\"\"\"\r\n        try:\r\n            # Remove any volatile fields that might differ between saves\r\n            ignore_fields = ['_save_timestamp', 'autosave_count']\r\n            \r\n            def clean_save(save):\r\n                if isinstance(save, dict):\r\n                    return {k: clean_save(v) for k, v in save.items() \r\n                        if k not in ignore_fields}\r\n                elif isinstance(save, list):\r\n                    return [clean_save(item) for item in save]\r\n                else:\r\n                    return save\r\n            \r\n            cleaned1 = clean_save(save1)\r\n            cleaned2 = clean_save(save2)\r\n            \r\n            return cleaned1 == cleaned2\r\n        except Exception as e:\r\n            print(f\"[SaveManager] Error comparing saves: {e}\")\r\n            return False\r\n\r\n    def _load_single_save(self, path):\r\n        \"\"\"Load a single save file.\"\"\"\r\n        try:\r\n            data = {}\r\n            with zipfile.ZipFile(path, 'r') as zf:\r\n                for fname in zf.namelist():\r\n                    if fname.endswith('.json'):\r\n                        with zf.open(fname) as f:\r\n                            key = os.path.splitext(fname)[0]\r\n                            data[key] = json.loads(f.read().decode('utf-8'))\r\n            return data\r\n        except Exception as e:\r\n            print(f\"[SaveManager] Error loading save {path}: {e}\")\r\n            return None\r\n        \r\n\r\n    def cleanup_duplicate_saves(self):\r\n        \"\"\"Remove duplicate save files with identical content.\"\"\"\r\n        try:\r\n            # Get all save files\r\n            save_files = [f for f in os.listdir(self.save_directory) \r\n                        if f.endswith('.zip')]\r\n            \r\n            # Group by UUID\r\n            saves_by_uuid = {}\r\n            for save_file in save_files:\r\n                if '_autosave.zip' in save_file:\r\n                    uuid = save_file.replace('_autosave.zip', '')\r\n                else:\r\n                    # Extract UUID from timestamped files\r\n                    parts = save_file.split('_')\r\n                    if len(parts) >= 6:  # UUID has 5 parts when split by '_'\r\n                        uuid = '_'.join(parts[:5])\r\n                    else:\r\n                        continue\r\n                \r\n                if uuid not in saves_by_uuid:\r\n                    saves_by_uuid[uuid] = []\r\n                saves_by_uuid[uuid].append(save_file)\r\n            \r\n            # Check each group for duplicates\r\n            for uuid, files in saves_by_uuid.items():\r\n                if len(files) > 1:\r\n                    print(f\"[SaveManager] Found {len(files)} saves for UUID {uuid}\")\r\n                    \r\n                    # Load and compare saves\r\n                    save_contents = {}\r\n                    for file in files:\r\n                        path = os.path.join(self.save_directory, file)\r\n                        content = self._load_single_save(path)\r\n                        if content:\r\n                            save_contents[file] = content\r\n                    \r\n                    # Find unique saves\r\n                    unique_saves = {}\r\n                    for file1, content1 in save_contents.items():\r\n                        is_duplicate = False\r\n                        for file2, content2 in unique_saves.items():\r\n                            if self._are_saves_identical(content1, content2):\r\n                                is_duplicate = True\r\n                                print(f\"[SaveManager] {file1} is duplicate of {file2}\")\r\n                                # Keep the newer file\r\n                                time1 = os.path.getmtime(os.path.join(self.save_directory, file1))\r\n                                time2 = os.path.getmtime(os.path.join(self.save_directory, file2))\r\n                                if time1 > time2:\r\n                                    # Delete the older one\r\n                                    os.remove(os.path.join(self.save_directory, file2))\r\n                                    unique_saves[file2] = content1\r\n                                else:\r\n                                    # Delete the newer one\r\n                                    os.remove(os.path.join(self.save_directory, file1))\r\n                                break\r\n                        \r\n                        if not is_duplicate:\r\n                            unique_saves[file1] = content1\r\n                            \r\n        except Exception as e:\r\n            print(f\"[SaveManager] Error cleaning duplicate saves: {e}\")\r\n\r\n    # --------------------------------------------------\r\n    # Load\r\n    # --------------------------------------------------\r\n    def load_game(self) -> dict | None:\r\n        latest = self.get_latest_save()\r\n        if not latest:\r\n            return None\r\n        data = {}\r\n        squid_uuid = None\r\n        with zipfile.ZipFile(latest, 'r') as zf:\r\n            for fname in zf.namelist():\r\n                if fname == \"uuid.txt\":\r\n                    squid_uuid = zf.read(fname).decode().strip()\r\n                    continue\r\n                with zf.open(fname) as f:\r\n                    raw = f.read()\r\n                    if not raw:\r\n                        continue\r\n                    key = os.path.splitext(fname)[0]\r\n                    data[key] = json.loads(raw.decode('utf-8'))\r\n        # inject UUID into returned dict\r\n        data[\"_uuid\"] = squid_uuid\r\n        return data\r\n\r\n    # --------------------------------------------------\r\n    # House-keeping\r\n    # --------------------------------------------------\r\n    def delete_save(self, is_autosave: bool = False) -> bool:\r\n        if is_autosave:\r\n            path = self.autosave_path\r\n        else:\r\n            path = self._get_manual_save_path()\r\n        \r\n        if path and os.path.exists(path):\r\n            os.remove(path)\r\n            return True\r\n        return False\r\n\r\n    def get_save_timestamp(self, is_autosave: bool = False) -> float | None:\r\n        if is_autosave:\r\n            path = self.autosave_path\r\n        else:\r\n            path = self._get_manual_save_path()\r\n        \r\n        return os.path.getmtime(path) if path and os.path.exists(path) else None\r\n\r\n    def get_save_size(self, is_autosave: bool = False) -> int | None:\r\n        if is_autosave:\r\n            path = self.autosave_path\r\n        else:\r\n            path = self._get_manual_save_path()\r\n        \r\n        return os.path.getsize(path) if path and os.path.exists(path) else None\r\n"
  },
  {
    "path": "src/splash_screen.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nimport os\r\nfrom .display_scaling import DisplayScaling\r\nfrom .localisation import Localisation\r\n\r\nclass SplashScreen(QtWidgets.QWidget):\r\n    finished = QtCore.pyqtSignal()\r\n    second_frame = QtCore.pyqtSignal()\r\n    frame_changed = QtCore.pyqtSignal(int)\r\n\r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        self.loc = Localisation.instance()\r\n        \r\n        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint)\r\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\r\n        \r\n        self.frame_index = 0\r\n        self.frames = []\r\n        \r\n        # Load frames\r\n        for i in range(1, 7):\r\n            image_path = os.path.join(\"images\", \"egg\", f\"anim0{i}.jpg\")\r\n            if os.path.exists(image_path):\r\n                original_pixmap = QtGui.QPixmap(image_path)\r\n                if not original_pixmap.isNull():\r\n                    scaled_size = original_pixmap.size() * DisplayScaling.get_scale_factor()\r\n                    scaled_pixmap = original_pixmap.scaled(\r\n                        scaled_size,\r\n                        QtCore.Qt.KeepAspectRatio,\r\n                        QtCore.Qt.SmoothTransformation\r\n                    )\r\n                    self.frames.append(scaled_pixmap)\r\n                else:\r\n                    print(f\"Failed to load image: {image_path}\")\r\n            else:\r\n                print(f\"Image file not found: {image_path}\")\r\n            \r\n        if not self.frames:\r\n            print(\"No frames were loaded successfully.\")\r\n            self.label = QtWidgets.QLabel(\"No images loaded\", self)\r\n            self.setFixedSize(256, 256)\r\n        else:\r\n            self.label = QtWidgets.QLabel(self)\r\n            self.label.setPixmap(self.frames[0])\r\n            self.setFixedSize(self.frames[0].size())\r\n        \r\n        # Create timer but don't start it yet\r\n        self.timer = QtCore.QTimer(self)\r\n        self.timer.timeout.connect(self.next_frame)\r\n\r\n    def start_animation(self):\r\n        \"\"\"Start the animation sequence after window is ready\"\"\"\r\n        self.frame_changed.emit(self.frame_index)\r\n        self.timer.start(1000)\r\n\r\n    def next_frame(self):\r\n        self.frame_index += 1\r\n        if self.frame_index < len(self.frames):\r\n            self.label.setPixmap(self.frames[self.frame_index])\r\n            self.frame_changed.emit(self.frame_index)\r\n            if self.frame_index == 1:\r\n                self.second_frame.emit()\r\n        elif self.frame_index == len(self.frames):\r\n            QtCore.QTimer.singleShot(1500, self.end_animation)\r\n        else:\r\n            self.timer.stop()\r\n\r\n    def end_animation(self):\r\n        # Use localised messages\r\n        hatched_msg = self.loc.get(\"squid_hatched\")\r\n        look_after_msg = self.loc.get(\"look_after\")\r\n        \r\n        print(\"\")\r\n        print(\"                     ******************************\")\r\n        print(f\"                     ***  {hatched_msg}  ***\")\r\n        print(f\"                      {look_after_msg}\")\r\n        print(\"                     ******************************\")\r\n        self.hide()\r\n        self.finished.emit()\r\n\r\n    def showEvent(self, event):\r\n        self.move(self.parent().rect().center() - self.rect().center())\r\n        super().showEvent(event)\r\n"
  },
  {
    "path": "src/squid.py",
    "content": "import os\nimport random\nimport uuid\nimport time\nfrom datetime import datetime\nfrom enum import Enum\nimport math\nfrom PyQt5 import QtCore, QtGui, QtWidgets\nfrom PyQt5.QtCore import QTimer\nfrom .brain_about_tab import SQUID_NAMES\nfrom .mental_states import MentalStateManager\nfrom .memory_manager import MemoryManager\nfrom .personality import Personality\nfrom .decision_engine import DecisionEngine\nfrom .image_cache import ImageCache\nfrom .statistics_window import StatisticsWindow\nfrom .squid_statistics import SquidStatistics\nfrom .vision_worker import (\n    VisionWorker, \n    VisionResult, \n    SquidVisionState,\n    SceneObject,\n    extract_scene_objects,\n    create_squid_vision_state\n)\n\n\nclass Squid:\n\n    def __init__(self, user_interface, tamagotchi_logic=None, personality=None, neuro_cooldown=None):\n        self.ui = user_interface\n        self.tamagotchi_logic = tamagotchi_logic\n        self.memory_manager = MemoryManager()\n        self.push_animation = None\n        self.startled_icon = None\n        self.startled_icon_offset = QtCore.QPointF(0, -100)\n        self.ng_icon = None\n        self.ng_icon_offset = QtCore.QPointF(0, -100)\n        self.tint_color = None\n        self.name = random.choice(SQUID_NAMES)\n        self.statistics = SquidStatistics(self)\n\n        # Set neurogenesis cooldown (default to 180 seconds if not specified)\n        self.neuro_cooldown = neuro_cooldown if neuro_cooldown is not None else 180\n\n        # Rock interaction system\n        self.carrying_rock = False\n        self.current_rock = None  # Currently held rock\n        self.rock_being_thrown = None  # Rock in mid-flight\n\n        # Hoarding preferences\n        self.hoard_corner = {\n            Personality.GREEDY: (50, 50),          # Top-left\n            Personality.STUBBORN: (self.ui.window_width-100, 50)  # Top-right\n        }\n\n        # Rock physics properties\n        self.rock_velocity_x = 0\n        self.rock_velocity_y = 0\n        self.rock_throw_power = 10  # Base throw strength\n        self.rock_throw_cooldown = 0\n\n        # Startle transition tracking\n        self.startled_transition = False\n        self.startled_transition_frames = 0\n\n        # Multiplayer-specific\n        self.can_move = True           # Whether the squid can move (disable when away)\n        self.is_transitioning = False  # Whether the squid is currently in transit\n\n        # Rock Interactions\n        self.rock_interaction_timer = QtCore.QTimer()\n        self.rock_interaction_timer.timeout.connect(self.check_rock_interaction)\n        self.rock_interaction_timer.start(1000)  # Check every second\n        self.rock_hold_start_time = 0\n        self.rock_hold_duration = 0\n        self.rock_decision_made = False\n        self.rock_animation_timer = QtCore.QTimer()\n        self.rock_animation_timer.timeout.connect(self.update_rock_throw)\n\n        self.load_images()\n        self.load_poop_images()\n        self.initialize_attributes()\n\n        self.mental_state_manager = MentalStateManager(self, self.ui.scene)\n\n        self.squid_item = QtWidgets.QGraphicsPixmapItem(self.current_image())\n        self.squid_item.setPos(self.squid_x, self.squid_y)\n        self.squid_item.setAcceptHoverEvents(True)  # Enable hover events\n        self.squid_item.mousePressEvent = self.handle_squid_click  # Add click handler\n        self.ui.scene.addItem(self.squid_item)\n        self.anxiety_cooldown_timer = None\n        self.ui.window.resizeEvent = self.handle_window_resize\n\n        self.view_cone_item = None\n        self.base_speed = 90  # Normal movement speed\n        self.current_speed = self.base_speed\n        self.is_fleeing = False\n        self.view_cone_visible = False\n        self.poop_timer = None\n        self.animation_speed = 1\n        self.base_move_interval = 1000  # 1 second\n        self.health = 100\n        self.is_sick = False\n        self.sick_icon_item = None\n        self.sick_icon_offset = QtCore.QPointF(0, -100)  # Offset the sick icon above the squid\n\n        self.status = \"roaming\"  # Initialize status\n\n        self.view_cone_angle = math.pi / 2.5  # Squid has a view cone of 80 degrees\n        self.current_view_angle = random.uniform(0, 2 * math.pi)\n        self.view_cone_change_interval = 2000  # milliseconds\n        self.last_view_cone_change = 0\n        self.pursuing_food = False\n        self.target_food = None\n\n        # View cone periodic scanning\n        self.view_cone_timer = QtCore.QTimer()\n        self.view_cone_timer.timeout.connect(self.update_view_direction)\n        self.view_cone_timer.start(1000)  # Every 1 second\n\n        # Goal neurons\n        self.satisfaction = 50\n        self.anxiety = 0\n        self.curiosity = 50\n\n         # ===== VISION WORKER SETUP =====\n        # Background thread for vision calculations\n        self._vision_worker = VisionWorker()\n        self._vision_worker.food_visibility_changed.connect(self._on_food_visibility_changed)\n        self._vision_worker.plant_proximity_changed.connect(self._on_plant_proximity_changed)\n        self._vision_worker.visibility_update.connect(self._on_visibility_update)\n        self._vision_worker.start()\n        \n        # Cached vision results\n        self._cached_vision: Optional[VisionResult] = None\n        self._cached_visible_food: List[Tuple[float, float]] = []\n        self._cached_can_see_food: bool = False\n        self._cached_plant_proximity: float = 0.0\n        \n        # Vision update timer - updates worker with squid state\n        self._vision_update_timer = QtCore.QTimer()\n        self._vision_update_timer.timeout.connect(self._update_vision_worker)\n        self._vision_update_timer.start(50)  # 20 Hz updates\n        \n        # Scene object cache for vision worker\n        self._scene_objects_dirty = True\n        self._cached_scene_objects: List[SceneObject] = []\n        \n\n        if personality is None:\n            self.personality = random.choice(list(Personality))\n        else:\n            self.personality = personality\n            \n        self.uuid = uuid.uuid4()\n\n    @property\n    def carrying_rock(self):\n        return hasattr(self, 'is_carrying_rock') and self.is_carrying_rock\n    \n    @carrying_rock.setter\n    def carrying_rock(self, value):\n        self.is_carrying_rock = value\n    \n    @property \n    def current_rock(self):\n        return getattr(self, 'carried_rock', None)\n    \n    @current_rock.setter\n    def current_rock(self, value):\n        self.carried_rock = value\n\n    @property\n    def hunger(self):\n        return self._hunger\n\n    @hunger.setter\n    def hunger(self, value):\n        old_value = getattr(self, '_hunger', 50)\n        self._hunger = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._hunger and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_hunger_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._hunger\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"hunger\",\n                    old_value=old_value,\n                    new_value=self._hunger\n                )\n\n    @property\n    def happiness(self):\n        return self._happiness\n\n    @happiness.setter\n    def happiness(self, value):\n        old_value = getattr(self, '_happiness', 100)\n        self._happiness = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._happiness and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_happiness_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._happiness\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"happiness\",\n                    old_value=old_value,\n                    new_value=self._happiness\n                )\n\n    @property\n    def cleanliness(self):\n        return self._cleanliness\n\n    @cleanliness.setter\n    def cleanliness(self, value):\n        old_value = getattr(self, '_cleanliness', 100)\n        self._cleanliness = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._cleanliness and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_cleanliness_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._cleanliness\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"cleanliness\",\n                    old_value=old_value,\n                    new_value=self._cleanliness\n                )\n\n    @property\n    def sleepiness(self):\n        return self._sleepiness\n\n    @sleepiness.setter\n    def sleepiness(self, value):\n        old_value = getattr(self, '_sleepiness', 30)\n        self._sleepiness = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._sleepiness and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_sleepiness_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._sleepiness\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"sleepiness\",\n                    old_value=old_value,\n                    new_value=self._sleepiness\n                )\n\n    @property\n    def satisfaction(self):\n        return self._satisfaction\n\n    @satisfaction.setter\n    def satisfaction(self, value):\n        old_value = getattr(self, '_satisfaction', 50)\n        self._satisfaction = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._satisfaction and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_satisfaction_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._satisfaction\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"satisfaction\",\n                    old_value=old_value,\n                    new_value=self._satisfaction\n                )\n\n    @property\n    def anxiety(self):\n        return self._anxiety\n\n    @anxiety.setter\n    def anxiety(self, value):\n        old_value = getattr(self, '_anxiety', 10)\n        self._anxiety = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._anxiety and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_anxiety_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._anxiety\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"anxiety\",\n                    old_value=old_value,\n                    new_value=self._anxiety\n                )\n\n    @property\n    def curiosity(self):\n        return self._curiosity\n\n    @curiosity.setter\n    def curiosity(self, value):\n        old_value = getattr(self, '_curiosity', 50)\n        self._curiosity = max(0, min(100, value))\n        \n        # Trigger hook if value changed and tamagotchi_logic exists\n        if old_value != self._curiosity and hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_curiosity_change\", \n                    squid=self, \n                    old_value=old_value, \n                    new_value=self._curiosity\n                )\n                \n                # General state change hook\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_squid_state_change\",\n                    squid=self,\n                    attribute=\"curiosity\",\n                    old_value=old_value,\n                    new_value=self._curiosity\n                )\n\n    def _on_food_visibility_changed(self, can_see: bool, food_positions: list):\n        \"\"\"Handle food visibility change from vision worker\"\"\"\n        old_can_see = self._cached_can_see_food\n        self._cached_can_see_food = can_see\n        self._cached_visible_food = food_positions\n        \n        # React to visibility change\n        if can_see and not old_can_see:\n            # Food just became visible\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n                if hasattr(self.tamagotchi_logic, 'brain_window'):\n                    self.tamagotchi_logic.brain_window.add_thought(\"I see food!\")\n        \n        elif not can_see and old_can_see:\n            # Food just became invisible\n            self.pursuing_food = False\n            self.target_food = None\n\n    def _update_scene_objects(self):\n        \"\"\"Update the cached scene objects for vision worker\"\"\"\n        if not self.tamagotchi_logic:\n            return\n        \n        try:\n            objects = []\n            \n            # Add food items\n            for item in self.tamagotchi_logic.food_items:\n                try:\n                    pos = item.pos()\n                    rect = item.boundingRect()\n                    objects.append(SceneObject(\n                        x=pos.x(),\n                        y=pos.y(),\n                        width=rect.width(),\n                        height=rect.height(),\n                        category='food',\n                        is_sushi=getattr(item, 'is_sushi', False)\n                    ))\n                except:\n                    pass\n            \n            # Add decorations from scene\n            if hasattr(self.ui, 'scene'):\n                for item in self.ui.scene.items():\n                    # Check if it's a decoration item\n                    if hasattr(item, 'category') and item.category in ('plant', 'rock', 'poop'):\n                        try:\n                            pos = item.pos()\n                            rect = item.boundingRect()\n                            objects.append(SceneObject(\n                                x=pos.x(),\n                                y=pos.y(),\n                                width=rect.width(),\n                                height=rect.height(),\n                                category=item.category\n                            ))\n                        except:\n                            pass\n            \n            # Send to vision worker\n            self._vision_worker.update_scene_objects(objects)\n            self._cached_scene_objects = objects\n            self._scene_objects_dirty = False\n            \n        except Exception as e:\n            print(f\"Error updating scene objects: {e}\")\n\n    def mark_scene_objects_dirty(self):\n        \"\"\"Call when objects are added/removed from scene\"\"\"\n        self._scene_objects_dirty = True\n\n    def _update_vision_worker(self):\n        \"\"\"Periodically update vision worker with current state\"\"\"\n        if not hasattr(self, '_vision_worker') or not self._vision_worker:\n            return\n        \n        # Update squid state\n        try:\n            squid_state = create_squid_vision_state(self)\n            self._vision_worker.update_squid_state(squid_state)\n        except Exception as e:\n            print(f\"Error updating squid vision state: {e}\")\n        \n        # FIX: Always update scene objects if food exists (because food moves constantly!)\n        # The dirty flag is only set when items are added/removed, but we need \n        # fresh coordinates for sinking food items every cycle.\n        has_food = self.tamagotchi_logic and getattr(self.tamagotchi_logic, 'food_items', False)\n        \n        if self._scene_objects_dirty or has_food:\n            self._update_scene_objects()\n\n    def _on_plant_proximity_changed(self, proximity: float, plant_positions: list):\n        \"\"\"Handle plant proximity change from vision worker\"\"\"\n        self._cached_plant_proximity = proximity\n        \n        # Could trigger calming effects when near plants\n        if proximity > 50 and hasattr(self, 'tamagotchi_logic'):\n            # Near a plant - slight anxiety reduction\n            if hasattr(self.tamagotchi_logic, 'plant_calming_effect_counter'):\n                self.tamagotchi_logic.plant_calming_effect_counter += 1\n    \n    def _on_visibility_update(self, result: VisionResult):\n        \"\"\"Handle full visibility update from vision worker\"\"\"\n        # Discard stale updates if scene has changed since this calculation started\n        if getattr(self, '_scene_objects_dirty', False):\n            return\n\n        self._cached_vision = result\n        self._cached_visible_food = result.visible_food\n        self._cached_can_see_food = result.can_see_food\n        self._cached_plant_proximity = result.plant_proximity_value\n\n    def _has_personality_starter_neuron(self) -> bool:\n        \"\"\"Return True if any starter neuron for this personality already exists.\"\"\"\n        if not hasattr(self, 'brain_widget') or not self.brain_widget:\n            return True          # skip creation if brain not ready\n        if not self.personality:\n            return True\n\n        # Map enum -> prefix used in neurogenesis.py\n        prefix_map = {\n            Personality.TIMID:      \"timid_caution\",\n            Personality.ADVENTUROUS: \"explorer_drive\",\n            Personality.LAZY:        \"energy_conservation\",\n            Personality.ENERGETIC:   \"restless_activity\",\n            Personality.INTROVERT:   \"solitude_preference\",\n            Personality.GREEDY:      \"insatiable_hunger\",\n            Personality.STUBBORN:    \"sushi_preference\",\n        }\n        prefix = prefix_map.get(self.personality)\n        if not prefix:               # unknown personality → skip\n            return True\n\n        existing = self.brain_widget.neuron_positions.keys()\n        return any(n.startswith(prefix) for n in existing)\n    \n    def update_view_direction(self):\n        \"\"\"\n        Called every 1 second by a QTimer.\n        Makes the squid actively scan its environment by changing gaze direction.\n        Includes smart hunger-based food bias + instant brain refresh.\n        \"\"\"\n        # Don't interrupt important states\n        if (getattr(self, 'is_sleeping', False) or \n            getattr(self, 'is_fleeing', False) or \n            getattr(self, 'pursuing_food', False)):\n            return\n\n        if not hasattr(self, 'tamagotchi_logic') or not self.tamagotchi_logic:\n            return\n\n        old_angle = self.current_view_angle\n\n        # === Normal scanning: random direction ===\n        self.current_view_angle = random.uniform(0, 2 * math.pi)\n\n        # === Hunger override: high chance to look toward nearest food ===\n        if self.hunger > 65 and self.tamagotchi_logic.food_items:\n            nearest_food = min(\n                self.tamagotchi_logic.food_items,\n                key=lambda f: self.distance_to(f.pos().x(), f.pos().y()),\n                default=None\n            )\n            if nearest_food:\n                fx = nearest_food.pos().x() + nearest_food.boundingRect().width() / 2\n                fy = nearest_food.pos().y() + nearest_food.boundingRect().height() / 2\n                sx = self.squid_x + self.squid_width / 2\n                sy = self.squid_y + self.squid_height / 2\n\n                target_angle = math.atan2(fy - sy, fx - sx)\n\n                # The hungrier, the more likely to snap gaze directly at food\n                hunger_factor = (self.hunger - 65) / 35.0  # 0.0 -> 1.0 as hunger goes 65->100\n                if random.random() < hunger_factor:\n                    self.current_view_angle = target_angle\n                    \n                    # FIX: Force immediate vision sync when locked on\n                    # This bridges the gap between \"looking\" (angle set) and \"seeing\" (worker detection)\n                    # ensuring the squid immediately pursues what it just decided to look at.\n                    self._force_sync_vision_until = time.time() + 0.5 \n                    \n                else:\n                    # Look near the food (curious glancing)\n                    self.current_view_angle = target_angle + random.uniform(-0.8, 0.8)\n\n        # === Force immediate brain update so 'can_see_food' neuron reacts instantly ===\n        if old_angle != self.current_view_angle:\n            # Option 1: Direct brain hook refresh (most reliable)\n            if hasattr(self.tamagotchi_logic, 'brain_hooks'):\n                # This recalculates all input neurons including can_see_food\n                QtCore.QTimer.singleShot(10, lambda: self.tamagotchi_logic.apply_input_neurons_to_brain())\n\n            # Option 2: Trigger full brain tick (fallback, also works)\n            if hasattr(self.tamagotchi_logic, 'update_squid_brain'):\n                QtCore.QTimer.singleShot(20, self.tamagotchi_logic.update_squid_brain)\n\n            # Optional: Visual feedback in VisionWindow\n            if hasattr(self.tamagotchi_logic, 'vision_window') and self.tamagotchi_logic.vision_window:\n                self.tamagotchi_logic.vision_window.update_view()\n\n    def apply_tint(self, color):\n        \"\"\"Squid can change colour!\"\"\"\n        self.tint_color = color\n        self.update_squid_image()\n\n    def set_animation_speed(self, speed):\n        self.animation_speed = speed\n\n    def finish_eating(self):\n        \"\"\"Reset status after eating\"\"\"\n        # Check if status contains \"eating\" (handles \"eating cheese\", \"eating sushi\", \"eating greedily\", etc.)\n        if \"eating\" in self.status.lower():\n            # Reset to personality-appropriate default status\n            if self.personality == Personality.TIMID:\n                self.status = \"cautiously exploring\"\n            elif self.personality == Personality.ADVENTUROUS:\n                self.status = \"boldly exploring\"\n            else:\n                self.status = \"roaming\"\n        \n        # Always clear the is_eating flag\n        self.is_eating = False\n            \n        # Make sure the brain is updated\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            self.tamagotchi_logic.update_squid_brain()\n\n\n    def load_images(self):\n        \"\"\"Load images with cache to reduce memory usage and apply resolution scaling\"\"\"\n        from .display_scaling import DisplayScaling\n        \n        # Get current screen resolution\n        screen = QtWidgets.QApplication.primaryScreen()\n        screen_size = screen.size()\n        \n        # Determine resolution-specific scaling\n        if screen_size.width() <= 1920 and screen_size.height() <= 1080:\n            # For 1080p resolution, reduce size by 30%\n            image_scale = 0.7\n            print(\"Applying 70% squid size scaling for 1080p resolution\")\n        else:\n            # For higher resolutions, use normal scaling\n            image_scale = 1.0\n        \n        # Load original images from cache\n        original_images = {\n            \"left1\": ImageCache.get_pixmap(os.path.join(\"images\", \"left1.png\")),\n            \"left2\": ImageCache.get_pixmap(os.path.join(\"images\", \"left2.png\")),\n            \"right1\": ImageCache.get_pixmap(os.path.join(\"images\", \"right1.png\")),\n            \"right2\": ImageCache.get_pixmap(os.path.join(\"images\", \"right2.png\")),\n            \"up1\": ImageCache.get_pixmap(os.path.join(\"images\", \"up1.png\")),\n            \"up2\": ImageCache.get_pixmap(os.path.join(\"images\", \"up2.png\")),\n            \"sleep1\": ImageCache.get_pixmap(os.path.join(\"images\", \"sleep1.png\")),\n            \"sleep2\": ImageCache.get_pixmap(os.path.join(\"images\", \"sleep2.png\")),\n        }\n        \n        # Store original dimensions for reference\n        self.original_width = original_images[\"left1\"].width()\n        self.original_height = original_images[\"left1\"].height()\n        \n        # Scale images for current resolution\n        self.images = {}\n        for name, pixmap in original_images.items():\n            # Calculate scaled size\n            scaled_width = int(pixmap.width() * image_scale)\n            scaled_height = int(pixmap.height() * image_scale)\n            \n            # Create scaled pixmap\n            self.images[name] = pixmap.scaled(\n                scaled_width, scaled_height,\n                QtCore.Qt.KeepAspectRatio, \n                QtCore.Qt.SmoothTransformation\n            )\n        \n        # Scale startled image\n        original_startled = ImageCache.get_pixmap(os.path.join(\"images\", \"startled.png\"))\n        startled_width = int(original_startled.width() * image_scale)\n        startled_height = int(original_startled.height() * image_scale)\n        self.startled_image = original_startled.scaled(\n            startled_width, startled_height,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n\n        # Scale neurogenesis icon image\n        original_ng = ImageCache.get_pixmap(os.path.join(\"images\", \"ng.png\"))\n        ng_width = int(original_ng.width() * image_scale)\n        ng_height = int(original_ng.height() * image_scale)\n        self.ng_image = original_ng.scaled(\n            ng_width, ng_height,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        \n        # Update squid dimensions to match scaled size\n        self.squid_width = self.images[\"left1\"].width()\n        self.squid_height = self.images[\"left1\"].height()\n        \n        print(f\"Squid scaled to {self.squid_width}x{self.squid_height} pixels\")\n\n    def show_startled_icon(self):\n        \"\"\"Show the startled icon above the squid's head\"\"\"\n        if self.startled_icon is None:\n            self.startled_icon = QtWidgets.QGraphicsPixmapItem(self.startled_image)\n            self.startled_icon.setZValue(500)  # Below DIRTY text (z=1000) but above decorations\n            self.ui.scene.addItem(self.startled_icon)\n        self.update_startled_icon_position()\n\n    def hide_startled_icon(self):\n        \"\"\"Remove the startled icon\"\"\"\n        if self.startled_icon is not None:\n            self.ui.scene.removeItem(self.startled_icon)\n            self.startled_icon = None\n\n    def show_neurogenesis_icon(self):\n        \"\"\"Show the neurogenesis (ng.png) icon above the squid's head\"\"\"\n        if self.ng_icon is None:\n            self.ng_icon = QtWidgets.QGraphicsPixmapItem(self.ng_image)\n            self.ng_icon.setZValue(500)  # Below DIRTY text (z=1000) but above decorations\n            self.ui.scene.addItem(self.ng_icon)\n        self.update_neurogenesis_icon_position()\n\n    def hide_neurogenesis_icon(self):\n        \"\"\"Remove the neurogenesis icon\"\"\"\n        if self.ng_icon is not None:\n            self.ui.scene.removeItem(self.ng_icon)\n            self.ng_icon = None\n\n    def update_neurogenesis_icon_position(self):\n        \"\"\"Position the neurogenesis icon above the squid\"\"\"\n        if self.ng_icon is not None:\n            self.ng_icon.setPos(\n                self.squid_x + self.squid_width // 2 - self.ng_icon.pixmap().width() // 2 + self.ng_icon_offset.x(),\n                self.squid_y + self.ng_icon_offset.y()\n            )\n\n    def update_startled_icon_position(self):\n        \"\"\"Position the startled icon above the squid\"\"\"\n        if self.startled_icon is not None:\n            self.startled_icon.setPos(\n                self.squid_x + self.squid_width // 2 - self.startled_icon.pixmap().width() // 2 + self.startled_icon_offset.x(),\n                self.squid_y + self.startled_icon_offset.y()\n            )\n\n    def load_poop_images(self):\n        self.poop_images = [\n            QtGui.QPixmap(os.path.join(\"images\", \"poop1.png\")),\n            QtGui.QPixmap(os.path.join(\"images\", \"poop2.png\"))\n        ]\n        self.poop_width = self.poop_images[0].width()\n        self.poop_height = self.poop_images[0].height()\n\n    def initialize_attributes(self):\n        self.base_squid_speed = 90  # pixels per update at 1x speed\n        self.base_vertical_speed = self.base_squid_speed // 2\n        self.center_x = self.ui.window_width // 2\n        self.center_y = self.ui.window_height // 2\n        self.squid_x = self.center_x\n        self.squid_y = self.center_y\n        self.squid_direction = \"left\"\n        self.current_frame = 0\n        self.update_preferred_vertical_range()\n\n        self.hunger = 25\n        self.sleepiness = 30\n        self.happiness = 100\n        self.cleanliness = 100\n        self.is_sleeping = False\n\n        self.health = 100\n        self.is_sick = False\n\n    def update_preferred_vertical_range(self):\n        self.preferred_vertical_range = (self.ui.window_height // 4, self.ui.window_height // 4 * 3)\n\n    def handle_window_resize(self, event):\n        self.ui.window_width = event.size().width()\n        self.ui.window_height = event.size().height()\n        self.center_x = self.ui.window_width // 2\n        self.center_y = self.ui.window_height // 2\n        self.update_preferred_vertical_range()\n        self.squid_x = max(50, min(self.squid_x, self.ui.window_width - 50 - self.squid_width))\n        self.squid_y = max(50, min(self.squid_y, self.ui.window_height - 120 - self.squid_height))\n        self.squid_item.setPos(self.squid_x, self.squid_y)\n        self.update_view_cone()\n        if self.startled_icon is not None:\n            self.update_startled_icon_position()\n        if self.ng_icon is not None:\n            self.update_neurogenesis_icon_position()\n\n    def update_needs(self):\n        # This method was moved to TamagotchiLogic 26/07/2024\n        pass\n\n    def make_decision(self):\n        \"\"\"Delegate to the decision engine for emergent behavior\"\"\"\n        if not hasattr(self, '_decision_engine'):\n            from .decision_engine import DecisionEngine\n            self._decision_engine = DecisionEngine(self)\n        \n        return self._decision_engine.make_decision()\n    \n    def handle_squid_click(self, event):\n        \"\"\"Handle mouse click on the squid\"\"\"\n        if self.is_sleeping:\n            self.startle_awake()\n        event.accept()\n\n    def startle_awake(self):\n        \"\"\"Startle the squid awake with an anxiety spike\"\"\"\n        if not self.is_sleeping:\n            return\n\n        # Wake up the squid\n        self.is_sleeping = False\n        # self.sleepiness = 0  # Waking up this way doesn't remove all tiredness\n        self.happiness = max(0, self.happiness - 25)  # Increased happiness decrease\n        self.anxiety = min(100, self.anxiety + 60)    # Increased anxiety spike\n        self.statistics_window.award(-100)\n\n        # Visual feedback\n        self.show_startled_icon()  # Show the startled icon\n        self.tamagotchi_logic.show_message(\"Squid was rudely startled awake!\")\n        self.status = \"startled\"\n\n        # Instead of immediately changing direction, set a transitional state\n        self.startled_transition = True\n        self.startled_transition_frames = 5  # Show startled animation for 5 frames\n\n        if random.random() < 0.25:\n            self.tamagotchi_logic.create_ink_cloud()          # spawns the cloud\n            self.current_speed = self.base_speed * 2          # double speed\n            self.status = \"fleeing from ink cloud\"\n\n            # create the requested memory\n            self.memory_manager.add_short_term_memory(\n                'behaviour', 'ink_cloud', 'Ink Cloud!'\n            )\n\n            # return to normal speed after 5 s\n            QtCore.QTimer.singleShot(5000, self.end_ink_flee)\n\n        # Start timers\n        self.anxiety_cooldown_timer = QtCore.QTimer()\n        self.anxiety_cooldown_timer.timeout.connect(self.reduce_startle_anxiety)\n        self.anxiety_cooldown_timer.start(5000)  # Reduce anxiety after 5 seconds\n\n        # Hide startled icon after 2 seconds\n        QtCore.QTimer.singleShot(2000, self.hide_startled_icon)\n\n        # End transition after a short delay (about half a second)\n        QtCore.QTimer.singleShot(500, self.end_startled_transition)\n\n    def end_ink_flee(self):\n        \"\"\"Called 5 s after an ink-cloud flee starts.\"\"\"\n        self.current_speed = self.base_speed\n        # fall back to a sensible status\n        if self.anxiety > 60:\n            self.status = \"nervous\"\n        else:\n            self.status = \"roaming\"\n\n    def end_startled_transition(self):\n        \"\"\"End the startled transition and set a natural direction\"\"\"\n        self.startled_transition = False\n        # Choose a random direction that makes sense for waking up\n        self.squid_direction = random.choice([\"left\", \"right\"])\n        self.update_squid_image()\n\n    def reduce_startle_anxiety(self):\n        \"\"\"Gradually reduce the startle anxiety\"\"\"\n        self.anxiety = max(20, self.anxiety - 15)  # Reduce anxiety but don't go below a higher baseline\n\n        if self.anxiety <= 35:  # When back to near-normal levels\n            if hasattr(self, 'anxiety_cooldown_timer'):\n                self.anxiety_cooldown_timer.stop()\n            self.tamagotchi_logic.show_message(\"Squid has calmed down... mostly.\")\n    \n    def check_boundary_exit(self):\n        \"\"\"\n        Comprehensive boundary exit detection with robust network node handling\n        \"\"\"\n        try:\n            # Check basic prerequisites\n            if not hasattr(self, 'tamagotchi_logic') or not self.tamagotchi_logic:\n                return False\n            \n            # Check plugin manager and multiplayer status\n            pm = self.tamagotchi_logic.plugin_manager\n            multiplayer_enabled = 'multiplayer' in pm.get_enabled_plugins()\n            \n            if not multiplayer_enabled:\n                return False\n            \n            # Attempt to get network node with multiple fallback strategies\n            network_node = None\n            \n            # Strategy 1: Direct attribute on tamagotchi_logic\n            if hasattr(self.tamagotchi_logic, 'network_node'):\n                network_node = self.tamagotchi_logic.network_node\n            \n            # Strategy 2: Find in multiplayer plugin\n            if network_node is None:\n                try:\n                    multiplayer_plugin = pm.plugins.get('multiplayer', {}).get('instance')\n                    if multiplayer_plugin and hasattr(multiplayer_plugin, 'network_node'):\n                        network_node = multiplayer_plugin.network_node\n                        # Attempt to set on tamagotchi_logic for future use\n                        self.tamagotchi_logic.network_node = network_node\n                except Exception as plugin_error:\n                    print(f\"Error finding network node in plugin: {plugin_error}\")\n            \n            # If still no network node, abort\n            if network_node is None or not network_node.is_connected:\n                print(\"No active network node found for boundary exit\")\n                return False\n            \n            # Advanced boundary detection logic\n            squid_right = self.squid_x + self.squid_width\n            squid_bottom = self.squid_y + self.squid_height\n            \n            exit_direction = None\n            \n            print(\"\\n===== BOUNDARY EXIT ANALYSIS =====\")\n            print(f\"Squid Position: ({self.squid_x}, {self.squid_y})\")\n            print(f\"Squid Dimensions: {self.squid_width}x{self.squid_height}\")\n            print(f\"Window Dimensions: {self.ui.window_width}x{self.ui.window_height}\")\n            \n            # Comprehensive boundary checks\n            if self.squid_x <= 0:\n                exit_direction = 'left'\n            elif squid_right >= self.ui.window_width:\n                exit_direction = 'right'\n            elif self.squid_y <= 0:\n                exit_direction = 'up'\n            elif squid_bottom >= self.ui.window_height:\n                exit_direction = 'down'\n            \n            if exit_direction:\n                print(f\"Exit Direction Detected: {exit_direction}\")\n                \n                # Prepare comprehensive exit data\n                exit_data = {\n                    'node_id': network_node.node_id,\n                    'direction': exit_direction,\n                    'position': {\n                        'x': self.squid_x,\n                        'y': self.squid_y\n                    },\n                    'color': self._get_squid_color(),\n                    'squid_width': self.squid_width,\n                    'squid_height': self.squid_height,\n                    'window_width': self.ui.window_width,\n                    'window_height': self.ui.window_height\n                }\n                \n                print(\"Exit Data Details:\")\n                for key, value in exit_data.items():\n                    print(f\"  {key}: {value}\")\n                \n                # Broadcast exit message\n                try:\n                    network_node.send_message(\n                        'squid_exit', \n                        {'payload': exit_data}\n                    )\n                    print(\"Exit message successfully broadcast\")\n                    return True\n                except Exception as broadcast_error:\n                    print(f\"Broadcast error: {broadcast_error}\")\n                    return False\n            \n            return False\n        \n        except Exception as e:\n            print(f\"Comprehensive boundary exit error: {e}\")\n            import traceback\n            traceback.print_exc()\n            return False\n        \n    def _get_squid_color(self):\n        \"\"\"Generate a persistent color for this squid\"\"\"\n        if not hasattr(self, '_squid_color'):\n            # Create stable color generation based on node_id\n            import hashlib\n            \n            # Try multiple fallback methods for generating a unique source\n            try:\n                # First try network node\n                if hasattr(self.tamagotchi_logic, 'network_node') and self.tamagotchi_logic.network_node:\n                    node_id_source = self.tamagotchi_logic.network_node.node_id\n                # Next try direct node_id attribute\n                elif hasattr(self.tamagotchi_logic, 'node_id'):\n                    node_id_source = self.tamagotchi_logic.node_id\n                # Final fallback is current timestamp\n                else:\n                    node_id_source = str(time.time())\n            except Exception:\n                # Ultimate fallback\n                node_id_source = str(time.time())\n            \n            # Generate color from hash\n            hash_val = hashlib.md5(node_id_source.encode()).hexdigest()\n            \n            r = int(hash_val[:2], 16)\n            g = int(hash_val[2:4], 16)\n            b = int(hash_val[4:6], 16)\n            \n            # Ensure minimum brightness\n            self._squid_color = (\n                max(r, 100), \n                max(g, 100), \n                max(b, 100)\n            )\n        \n        return self._squid_color\n    \n    def _notify_boundary_exit(self, direction):\n        \"\"\"\n        Enhanced notification of boundary exit with comprehensive logging\n        \"\"\"\n        print(\"\\n===== BOUNDARY EXIT NOTIFICATION =====\")\n        \n        try:\n            # Get plugin manager\n            pm = self.tamagotchi_logic.plugin_manager\n            \n            # Get multiplayer plugin\n            if 'multiplayer' in pm.get_enabled_plugins():\n                plugin_instance = pm.plugins['multiplayer'].get('instance')\n                \n                if plugin_instance and hasattr(plugin_instance, 'network_node'):\n                    # Prepare exit data with precise details\n                    exit_data = {\n                        'node_id': plugin_instance.network_node.node_id if plugin_instance.network_node else 'unknown',\n                        'direction': direction,\n                        'position': {\n                            'x': self.squid_x,\n                            'y': self.squid_y\n                        },\n                        'color': plugin_instance.get_squid_color() if hasattr(plugin_instance, 'get_squid_color') else (150, 150, 255),\n                        'squid_width': self.squid_width,\n                        'squid_height': self.squid_height,\n                        'window_width': self.ui.window_width,\n                        'window_height': self.ui.window_height\n                    }\n                    \n                    print(\"Exit Data:\")\n                    for key, value in exit_data.items():\n                        print(f\"  {key}: {value}\")\n                    \n                    # Broadcast exit message\n                    plugin_instance.network_node.send_message(\n                        'squid_exit', \n                        {'payload': exit_data}\n                    )\n                    \n                    print(f\"[MULTIPLAYER] Squid exiting through {direction} boundary\")\n                    \n                    # CHANGE: Completely hide the squid instead of reducing opacity\n                    self.squid_item.setVisible(False)\n                    \n                    # CHANGE: Set flag to indicate squid is away\n                    self.is_transitioning = True\n                    \n                    # CHANGE: Disable movement while away\n                    self.can_move = False\n                    \n                    # CHANGE: Update status\n                    self.status = \"visiting another tank\"\n                    \n                    # Optional: Show a message about the squid leaving\n                    if hasattr(self.tamagotchi_logic, 'show_message'):\n                        self.tamagotchi_logic.show_message(f\"Your squid left through the {direction} boundary!\")\n                else:\n                    print(\"[ERROR] No network node or plugin instance available\")\n            else:\n                print(\"[ERROR] Multiplayer plugin not enabled\")\n        \n        except Exception as e:\n            print(f\"[CRITICAL] Error in boundary exit notification:\")\n            import traceback\n            traceback.print_exc()\n        \n        print(\"===== BOUNDARY EXIT NOTIFICATION END =====\\n\")\n    \n    \n    def determine_startle_reason(self, current_state):\n        \"\"\"Determine why the squid is startled based on environment\"\"\"\n        # Check for sudden environmental changes\n        if self.tamagotchi_logic.environment_changed_recently():\n            return \"environment changed too quickly\"\n            \n        # Check for novel objects\n        if current_state.get('has_novelty_neurons', False):\n            visible_objects = []\n            if self.get_visible_food():\n                visible_objects.append(\"new food\")\n            if self.is_near_decorations('poop'):\n                visible_objects.append(\"poop\")\n            if self.is_near_decorations('plant'):\n                visible_objects.append(\"plant\")\n            if visible_objects:\n                return f\"new object sighted ({'/'.join(visible_objects)})\"\n        \n        # Check for emotional state\n        if current_state['anxiety'] > 80 and current_state['happiness'] < 30:\n            return \"emotional overwhelm\"\n        \n        # Check for sudden movement\n        if abs(self.rock_velocity_x) > 5 or abs(self.rock_velocity_y) > 5:\n            return \"fast moving object detected\"\n            \n        # Default reason if none specific found\n        return \"unknown cause\"\n    \n    def is_near_decorations(self, category):\n        \"\"\"Check if decorations of specified category are nearby\"\"\"\n        decorations = self.tamagotchi_logic.get_nearby_decorations(\n            self.squid_x, self.squid_y)\n        return any(getattr(d, 'category', None) == category \n                for d in decorations)\n\n    def search_for_favorite_food(self):\n        visible_food = self.get_visible_food()\n        if visible_food:\n            for food_x, food_y in visible_food:\n                if self.is_favorite_food(self.tamagotchi_logic.get_food_item_at(food_x, food_y)):\n                    self.move_towards(food_x, food_y)\n                    return\n            # If no favorite food is found, display a message and move randomly\n            self.tamagotchi_logic.show_message(\"Stubborn squid does not like that type of food!\")\n            self.move_randomly()\n        else:\n            self.move_randomly()\n\n    def get_favorite_food(self):\n        # Implement logic to find the squid's favorite food\n        for food_item in self.tamagotchi_logic.food_items:\n            if self.is_favorite_food(food_item):\n                return food_item.pos().x(), food_item.pos().y()\n        return None\n\n    def is_favorite_food(self, food_item):\n        return food_item is not None and getattr(food_item, 'is_sushi', False)\n\n    def load_state(self, state):\n        self.hunger = state['hunger']\n        self.sleepiness = state['sleepiness']\n        self.happiness = state['happiness']\n        self.cleanliness = state['cleanliness']\n        self.health = state['health']\n        self.is_sick = state['is_sick']\n        self.squid_x = state['squid_x']\n        self.squid_y = state['squid_y']\n        self.satisfaction = state['satisfaction']\n        self.anxiety = state['anxiety']\n        self.curiosity = state['curiosity']\n        self.personality = Personality(state['personality'])\n        \n        # Load the UUID if it exists in the saved state\n        if 'uuid' in state:\n            self.uuid = uuid.UUID(state['uuid'])  # Convert string back to UUID object\n        else:\n            # For backward compatibility with old saves\n            self.uuid = uuid.uuid4()\n\n        # Restore statistics if saved\n        if 'statistics' in state and hasattr(self, 'statistics'):\n            stats_data = state['statistics']\n            for key, value in stats_data.items():\n                if hasattr(self.statistics, key):\n                    setattr(self.statistics, key, value)\n\n        # Load the squid's name if it exists in the saved state\n        self.name = state.get('name', 'Squid')\n        if 'tint_color' in state and state['tint_color']:\n            self.tint_color = QtGui.QColor(*state['tint_color'])\n        else:\n            self.tint_color = None\n\n        self.squid_item.setPos(self.squid_x, self.squid_y)\n        self.update_squid_image()\n\n    def push_decoration(self, decoration, direction):\n        \"\"\"Push a decoration with proper animation handling\"\"\"\n        try:\n            push_distance = 80\n            current_pos = decoration.pos()\n            new_x = current_pos.x() + (push_distance * direction)\n\n            scene_rect = self.ui.scene.sceneRect()\n            new_x = max(scene_rect.left(), min(new_x, scene_rect.right() - decoration.boundingRect().width()))\n\n            if self.push_animation and self.push_animation.state() == QtCore.QAbstractAnimation.Running:\n                self.push_animation.stop()\n\n            self.push_animation = QtCore.QVariantAnimation()\n            self.push_animation.setDuration(300)\n            self.push_animation.setStartValue(current_pos)\n            self.push_animation.setEndValue(QtCore.QPointF(new_x, current_pos.y()))\n            self.push_animation.setEasingCurve(QtCore.QEasingCurve.OutCubic)\n\n            self.push_animation.valueChanged.connect(decoration.setPos)\n            self.push_animation.finished.connect(lambda: self._on_push_complete(decoration))\n            self.push_animation.start()\n\n        except Exception as e:\n            print(f\"Tried to push decoration but failed: {e}\")\n            decoration.setPos(new_x, current_pos.y())\n            self._on_push_complete(decoration)\n\n    def _on_push_complete(self, decoration):\n        \"\"\"Called when the push animation finishes.\"\"\"\n        self.happiness = min(100, self.happiness + 5)\n        self.curiosity = min(100, self.curiosity + 10)\n        self.anxiety = max(0, self.anxiety - 6)\n\n        # --- Plant-specific tracking & reward ---\n        if hasattr(decoration, 'category') and decoration.category == 'plant':\n            # Count this interaction\n            self.memory_manager.add_short_term_memory(\n                'interaction', 'plant_contact',\n                {'plant_key': decoration.filename, 'effect': 'calming'},\n                importance=2.0\n            )\n            # Tell the logic layer to track it\n            if hasattr(self.tamagotchi_logic, 'track_plant_interaction'):\n                self.tamagotchi_logic.track_plant_interaction()\n\n            # Mark as favourite after 3 touches\n            key = decoration.filename\n            self.memory_manager.plant_interaction_count[key] = self.memory_manager.plant_interaction_count.get(key, 0) + 1\n            if self.memory_manager.plant_interaction_count[key] >= 3:\n                self.memory_manager.add_long_term_memory('favourite_plant', key, {\n                    'reason': 'Repeated calming contact',\n                    'anxiety_reduction': True\n                })\n\n        if hasattr(self.tamagotchi_logic, 'brain_window') and hasattr(self.tamagotchi_logic.brain_window, 'statistics_tab'):\n            self.tamagotchi_logic.brain_window.statistics_tab.increment_stat('plants_interacted')\n\n        # --- Reward for RL ---\n        if hasattr(self.tamagotchi_logic, 'recent_positive_outcome'):\n            self.tamagotchi_logic.recent_positive_outcome = True\n\n        self.status = \"pushing decoration\"\n        self.tamagotchi_logic.show_message(\"Squid pushed a decoration\")\n\n        # Clean up animation reference\n        self.push_animation = None\n\n    def record_startle_reason(self, reason):\n        \"\"\"Record why the squid was startled for memory and display\"\"\"\n        self.memory_manager.add_short_term_memory('mental_state', 'startled', \n            f\"Startled because: {reason}\")\n        self.tamagotchi_logic.show_message(f\"Squid startled! ({reason})\")\n\n    def handle_rock_interaction(self, target_rock=None):\n        \"\"\"Unified rock interaction handler delegates to RockInteractionManager\"\"\"\n        if not hasattr(self.tamagotchi_logic, 'rock_interaction'):\n            return False\n            \n        return self.tamagotchi_logic.rock_interaction.start_rock_test(target_rock)\n\n    def move_erratically(self):\n        directions = [\"left\", \"right\", \"up\", \"down\"]\n        self.squid_direction = random.choice(directions)\n        self.move_squid()\n\n    def move_slowly(self):\n        self.base_squid_speed = self.base_squid_speed // 2\n        self.base_vertical_speed = self.base_vertical_speed // 2\n        self.move_squid()\n\n    def explore_environment(self):\n        if random.random() < 0.3:\n            self.change_direction()\n        self.move_squid()\n\n    def search_for_food(self):\n        visible_food = self.get_visible_food()\n        if visible_food:\n            # Only set pursuing_food to True if food is actually visible\n            self.pursuing_food = True\n            closest_food = min(visible_food, key=lambda f: self.distance_to(f[0], f[1]))\n            self.status = \"moving to food\"\n            self.move_towards(closest_food[0], closest_food[1])\n        else:\n            # Reset pursuing_food when no food is visible\n            self.pursuing_food = False\n            self.status = \"searching for food\"\n            self.move_randomly()\n\n    def get_visible_objects(self, object_list):\n        \"\"\"\n        Finds objects from a given list that are within the squid's vision cone.\n        This is a generic helper method.\n        \"\"\"\n        visible_objects = []\n        for item in object_list:\n            # FIX: Use the center of the item instead of the top-left position.\n            # This ensures detection works if the cone touches the object body\n            # but misses the specific (0,0) origin point.\n            if hasattr(item, 'sceneBoundingRect'):\n                center = item.sceneBoundingRect().center()\n                check_x, check_y = center.x(), center.y()\n            else:\n                # Fallback for items not fully initialized in scene\n                check_x, check_y = item.pos().x(), item.pos().y()\n\n            if self.is_in_vision_cone(check_x, check_y):\n                visible_objects.append(item)\n        return visible_objects\n\n    def get_visible_food(self):\n        \"\"\"\n        Returns the positions of visible food items.\n        Includes a circuit breaker to prevent 'ghost' food detection.\n        \"\"\"\n        # 1. Circuit Breaker: If the logic system says there is no food, \n        # we absolutely cannot see any, regardless of what the vision cache says.\n        # This fixes the \"stuck at top\" bug where the worker thread returns old data.\n        if self.tamagotchi_logic and not self.tamagotchi_logic.food_items:\n            return []\n\n        # 2. Force sync check if recently ate (handling worker latency)\n        # This ensures we don't act on stale worker data immediately after eating\n        if time.time() < getattr(self, '_force_sync_vision_until', 0):\n            return self._get_visible_food_sync()\n\n        # 3. If scene is dirty (items recently added/removed), the cache is invalid.\n        # Fall back to synchronous check which uses the authoritative list.\n        if getattr(self, '_scene_objects_dirty', False):\n            return self._get_visible_food_sync()\n\n        # 4. Use cached result if available and valid\n        if hasattr(self, '_cached_visible_food') and self._cached_visible_food is not None:\n            return self._cached_visible_food\n        \n        # 5. Fallback to synchronous calculation\n        return self._get_visible_food_sync()\n\n    def _get_visible_food_sync(self):\n        \"\"\"Synchronous fallback for get_visible_food\"\"\"\n        if self.tamagotchi_logic is None:\n            return []\n        \n        all_visible_food_items = self.get_visible_objects(self.tamagotchi_logic.food_items)\n        \n        if not all_visible_food_items:\n            return []\n        \n        # Sort visible food to prioritize sushi\n        sushi_items = [item for item in all_visible_food_items if getattr(item, 'is_sushi', False)]\n        other_food_items = [item for item in all_visible_food_items if not getattr(item, 'is_sushi', False)]\n        \n        sorted_positions = [(food.pos().x(), food.pos().y()) for food in sushi_items]\n        sorted_positions.extend([(food.pos().x(), food.pos().y()) for food in other_food_items])\n        \n        return sorted_positions\n\n    def can_see_food(self) -> bool:\n        \"\"\"\n        Quick check if food is visible.\n        Uses cached result from vision worker.\n        \"\"\"\n        if hasattr(self, '_cached_can_see_food'):\n            return self._cached_can_see_food\n        return len(self.get_visible_food()) > 0\n\n    def get_plant_proximity(self) -> float:\n        \"\"\"\n        Get the current plant proximity value (0-100).\n        Uses cached result from vision worker.\n        \"\"\"\n        if hasattr(self, '_cached_plant_proximity'):\n            return self._cached_plant_proximity\n        return 0.0\n    \n    def get_visible_plants(self):\n        \"\"\"\n        Finds plant decorations that are within the squid's vision cone.\n        Uses cached result from vision worker when available.\n        \"\"\"\n        if hasattr(self, '_cached_vision') and self._cached_vision:\n            return self._cached_vision.visible_plants\n        \n        # Fallback to synchronous calculation\n        if self.tamagotchi_logic is None:\n            return []\n        \n        all_plants = []\n        for item in self.tamagotchi_logic.user_interface.scene.items():\n            if hasattr(item, 'category') and item.category == 'plant':\n                all_plants.append(item)\n        \n        return self.get_visible_objects(all_plants)\n\n    def is_in_vision_cone(self, x, y):\n        \"\"\"\n        Check if a point (x,y) is inside the squid's vision cone\n        \n        Args:\n            x (float): X coordinate to check\n            y (float): Y coordinate to check\n            \n        Returns:\n            bool: True if the point is in vision cone, False otherwise\n        \"\"\"\n        # Get squid center position\n        squid_center_x = self.squid_x + self.squid_width // 2\n        squid_center_y = self.squid_y + self.squid_height // 2\n        \n        # Calculate vector to target\n        dx = x - squid_center_x\n        dy = y - squid_center_y\n        \n        # Calculate distance\n        distance = math.sqrt(dx**2 + dy**2)\n        \n        # Define vision cone length\n        cone_length = max(self.ui.window_width, self.ui.window_height)\n        \n        # If target is beyond detection range, return false\n        if distance > cone_length:\n            return False\n        \n        # Calculate angle to target point\n        angle_to_target = math.atan2(dy, dx)\n        \n        # Get current view angle (use current_view_angle if available, otherwise derive from direction)\n        if hasattr(self, 'current_view_angle'):\n            current_angle = self.current_view_angle\n        else:\n            direction_map = {\n                'right': 0,\n                'up': math.pi * 1.5,\n                'left': math.pi,\n                'down': math.pi * 0.5\n            }\n            current_angle = direction_map.get(self.squid_direction, 0)\n        \n        # Get cone angle (half of the total view cone angle)\n        if hasattr(self, 'view_cone_angle'):\n            cone_angle = self.view_cone_angle / 2\n        else:\n            cone_angle = math.pi / 5  # Default 36-degree half-angle\n        \n        # Calculate angle difference (accounting for wrap-around)\n        angle_diff = abs(angle_to_target - current_angle)\n        while angle_diff > math.pi:\n            angle_diff = 2 * math.pi - angle_diff\n        \n        # Check if the target is within the cone angle\n        return angle_diff <= cone_angle\n    \n    def change_view_cone_direction(self):\n        self.current_view_angle = random.uniform(0, 2 * math.pi)\n\n    def cleanup_vision_worker(self):\n        \"\"\"Clean up vision worker - call before squid destruction\"\"\"\n        if hasattr(self, '_vision_update_timer') and self._vision_update_timer:\n            self._vision_update_timer.stop()\n        \n        if hasattr(self, '_vision_worker') and self._vision_worker:\n            self._vision_worker.stop()\n            self._vision_worker.wait(1000)\n            self._vision_worker = None\n\n    def move_squid(self):\n        \"\"\"\n        Move the squid with comprehensive debug logging and multiplayer boundary check\n        \"\"\"\n        # Check if movement is allowed\n        if not getattr(self, 'can_move', True):\n            return\n        \n        # Check if multiplayer is available and enabled\n        if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n            pm = self.tamagotchi_logic.plugin_manager\n            multiplayer_enabled = 'multiplayer' in pm.get_enabled_plugins()\n        else:\n            multiplayer_enabled = False\n        \n        if self.animation_speed == 0:\n            #print(\"Animation speed is 0, no movement\")\n            return\n\n        if self.is_sleeping:\n            #print(\"Squid is sleeping, limited movement\")\n            if self.squid_y < self.ui.window_height - 120 - self.squid_height:\n                self.squid_y += self.base_vertical_speed * self.animation_speed\n                self.squid_item.setPos(self.squid_x, self.squid_y)\n            self.current_frame = (self.current_frame + 1) % 2\n            self.update_squid_image()\n            return\n        \n\n        current_time = QtCore.QTime.currentTime().msecsSinceStartOfDay()\n\n        visible_food = self.get_visible_food()\n\n        if visible_food:\n            closest_food = min(visible_food, key=lambda f: self.distance_to(f[0], f[1]))\n            self.pursuing_food = True\n            self.target_food = closest_food\n            self.move_towards(closest_food[0], closest_food[1])\n        elif self.pursuing_food:\n            self.pursuing_food = False\n            self.target_food = None\n            self.move_randomly()\n        else:\n            if current_time - self.last_view_cone_change > self.view_cone_change_interval:\n                self.change_view_cone_direction()\n                self.last_view_cone_change = current_time\n            self.move_randomly()\n\n        # Store previous position for distance calculation\n        prev_x, prev_y = self.squid_x, self.squid_y\n\n        # Calculate new position\n        squid_x_new = self.squid_x\n        squid_y_new = self.squid_y\n\n        if self.squid_direction == \"left\":\n            squid_x_new -= self.base_squid_speed * self.animation_speed\n        elif self.squid_direction == \"right\":\n            squid_x_new += self.base_squid_speed * self.animation_speed\n        elif self.squid_direction == \"up\":\n            squid_y_new -= self.base_vertical_speed * self.animation_speed\n        elif self.squid_direction == \"down\":\n            squid_y_new += self.base_vertical_speed * self.animation_speed\n\n        # Boundary handling for single-player and multiplayer modes\n        if not multiplayer_enabled:\n            # Original boundary restrictions for single-player mode\n            if squid_x_new < 50:\n                squid_x_new = 50\n                self.change_direction()\n            elif squid_x_new > self.ui.window_width - 50 - self.squid_width:\n                squid_x_new = self.ui.window_width - 50 - self.squid_width\n                self.change_direction()\n\n            if squid_y_new < 50:\n                squid_y_new = 50\n                self.change_direction()\n            elif squid_y_new > self.ui.window_height - 120 - self.squid_height:\n                squid_y_new = self.ui.window_height - 120 - self.squid_height\n                self.change_direction()\n        else:\n            # Extended boundary check for multiplayer\n            print(\"Multiplayer mode: Extended boundary check\")\n            squid_right = squid_x_new + self.squid_width\n            squid_bottom = squid_y_new + self.squid_height\n\n        # Update squid position\n        self.squid_x = squid_x_new\n        self.squid_y = squid_y_new\n\n        # Track distance - 80 pixels per movement\n        if self.squid_x != prev_x or self.squid_y != prev_y:\n            if hasattr(self.tamagotchi_logic, 'brain_window') and hasattr(self.tamagotchi_logic.brain_window, 'statistics_tab'):\n                self.tamagotchi_logic.brain_window.statistics_tab.track_distance(80)\n\n        # Update animation frame and image\n        if self.squid_direction in [\"left\", \"right\", \"up\", \"down\"]:\n            self.current_frame = (self.current_frame + 1) % 2\n            self.squid_item.setPixmap(self.current_image())\n\n        # Set new position and update related elements\n        self.squid_item.setPos(self.squid_x, self.squid_y)\n        self.update_view_cone()\n        self.update_sick_icon_position()\n\n        # Comprehensive boundary exit check in multiplayer mode\n        if multiplayer_enabled:\n            #print(\"Triggering boundary exit check in multiplayer mode\")\n            exit_result = self.check_boundary_exit()\n            #print(f\"Boundary Exit Result: {exit_result}\")\n\n    def move_towards(self, x, y):\n        dx = x - (self.squid_x + self.squid_width // 2)\n        dy = y - (self.squid_y + self.squid_height // 2)\n\n        if abs(dx) > abs(dy):\n            self.squid_direction = \"right\" if dx > 0 else \"left\"\n        else:\n            self.squid_direction = \"down\" if dy > 0 else \"up\"\n\n    def move_towards_position(self, target_pos):\n        dx = target_pos.x() - (self.squid_x + self.squid_width // 2)\n        dy = target_pos.y() - (self.squid_y + self.squid_height // 2)\n\n        if abs(dx) > abs(dy):\n            self.squid_direction = \"right\" if dx > 0 else \"left\"\n        else:\n            self.squid_direction = \"down\" if dy > 0 else \"up\"\n\n        self.move_squid()\n\n    def eat(self, food_item):\n        # FIX: Force synchronous vision for a short time to prevent \"ghost\" food\n        # This prevents the vision worker from reporting the just-eaten food as visible\n        # before it has processed the scene update.\n        self._force_sync_vision_until = time.time() + 0.5\n\n        effects = {}\n\n        # Basic effects for all food types\n        effects['hunger'] = max(-20, -self.hunger)\n        effects['happiness'] = min(10, 100 - self.happiness)\n\n        # Determine food type and effects\n        is_sushi = getattr(food_item, 'is_sushi', False)\n        food_name = \"sushi\" if is_sushi else \"cheese\"\n        \n        # Satisfaction boost (stronger for sushi)\n        effects['satisfaction'] = min(15 if is_sushi else 10, 100 - self.satisfaction)\n\n        # Personality-based reward points\n        reward_points = 2  # Default for all food\n        if is_sushi and self.personality in [Personality.GREEDY, Personality.STUBBORN]:\n            reward_points = 3  # Extra reward for favorite food\n            effects['satisfaction'] = min(20, 100 - self.satisfaction)  # Even bigger boost\n\n        # Trigger hook if tamagotchi_logic exists\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_feed\", \n                    squid=self,\n                    food_item=food_item,\n                    food_type=food_name,\n                    effects=effects\n                )\n        \n        # Set eating state BEFORE applying effects\n        self.is_eating = True\n        self.status = f\"eating {food_name}\"  # Use lowercase for consistency\n        self.statistics_window.award(75)\n        \n        # Apply all stat changes\n        for attr, change in effects.items():\n            setattr(self, attr, getattr(self, attr) + change)\n\n        # Start a timer to reset the status after 1 second\n        QTimer.singleShot(1000, self.finish_eating)\n\n        # Memory system\n        formatted_effects = ', '.join(f\"{attr.capitalize()} {'+' if val >= 0 else ''}{val:.2f}\" \n                            for attr, val in effects.items())\n        self.memory_manager.add_short_term_memory('food', food_name, \n            f\"Ate {food_name}: {formatted_effects}\")\n\n        # Neurogenesis tracking\n        if hasattr(self.tamagotchi_logic, 'neurogenesis_triggers'):\n            current = self.tamagotchi_logic.neurogenesis_triggers['positive_outcomes']\n            self.tamagotchi_logic.neurogenesis_triggers['positive_outcomes'] = min(current + reward_points, 5)\n\n        # Visual/behavioral effects\n        self.tamagotchi_logic.remove_food(food_item)\n\n        # --- FIX Clear vision cache immediately ---\n        # This prevents the squid from \"seeing\" the ghost of the food it just ate\n        # and getting stuck in 'pursuing_food' mode at the top of the screen.\n        self._cached_visible_food = []\n        self._cached_can_see_food = False\n        # --- FIX END ---\n\n        self.show_eating_effect()\n        self.start_poop_timer()\n        self.pursuing_food = False\n        self.target_food = None\n\n        # Add tracking (added in 2.4.4)\n        if hasattr(self.tamagotchi_logic, 'track_food_consumed'):\n            self.tamagotchi_logic.track_food_consumed(food_item)\n\n        # Personality reactions (with enhanced messages)\n        if self.personality == Personality.GREEDY:\n            self.eat_greedily(food_item)\n            if is_sushi:\n                self.tamagotchi_logic.show_message(\"Greedy squid devours sushi voraciously!\")\n        elif self.personality == Personality.STUBBORN:\n            if not is_sushi:\n                self.react_stubborn_eating()\n            else:\n                self.tamagotchi_logic.show_message(\"Stubborn squid happily accepts sushi\")\n\n    def eat_greedily(self, food_item):\n        # Update status to show greedy eating (lowercase for consistency)\n        self.status = \"eating greedily\"\n        food_type = \"sushi\" if getattr(food_item, 'is_sushi', False) else \"cheese\"\n\n        # Reduce hunger more than usual\n        self.hunger = max(0, self.hunger - 25)\n\n        # Increase happiness more\n        self.happiness = min(100, self.happiness + 15)\n\n        # Increase satisfaction significantly\n        self.satisfaction = min(100, self.satisfaction + 20)\n\n        # Slightly increase anxiety (from overeating)\n        self.anxiety = min(100, self.anxiety + 5)\n\n        # Note: Food is already removed in eat() method, don't remove again\n        # self.tamagotchi_logic.remove_food(food_item)  # REMOVED - already done\n        #print(f\"The greedy squid enthusiastically ate the {food_type}\")\n        \n        # These are already called in eat() method, but safe to call again\n        self.show_eating_effect()\n        # Poop timer already started in eat(), don't restart\n        # self.start_poop_timer()  # Already called\n        \n        # These should already be set in eat(), but ensure they're cleared\n        self.pursuing_food = False\n        self.target_food = None\n\n        # Occasionally show a message\n        if random.random() < 0.2:  # 20% chance to show a message\n            self.tamagotchi_logic.show_message(\"Nom nom! Greedy squid devours the food!\")\n\n        # Check if there's more food nearby\n        if self.check_for_more_food():\n            if random.random() < 0.1:  # 10% chance to show this message\n                self.tamagotchi_logic.show_message(\"Greedy squid looks around for more food...\")\n        else:\n            if random.random() < 0.1:  # 10% chance to show this message\n                self.tamagotchi_logic.show_message(\"Greedy squid is satisfied... for now.\")\n\n    def react_stubborn_eating(self):\n        if self.hunger > 80:  # Extremely hungry\n            self.happiness = max(0, self.happiness - 5)\n            self.tamagotchi_logic.show_message(\"Stubborn squid reluctantly eats non-sushi food.\")\n        else:\n            self.tamagotchi_logic.show_message(\"Stubborn squid ignores non-sushi food.\")\n\n    def check_for_more_food(self):\n        for food_item in self.tamagotchi_logic.food_items:\n            if self.is_food_nearby(food_item):\n                return True\n        return False\n\n    def is_food_nearby(self, food_item):\n        food_x, food_y = food_item.pos().x(), food_item.pos().y()\n        squid_center_x = self.squid_x + self.squid_width // 2\n        squid_center_y = self.squid_y + self.squid_height // 2\n        distance = math.sqrt((squid_center_x - food_x)**2 + (squid_center_y - food_y)**2)\n        return distance < 100  # Adjust the distance threshold as needed\n    \n    def process_squid_detection(self, remote_node_id, is_visible=True):\n        \"\"\"\n        Process the detection of another squid in this squid's vision cone\n        \n        Args:\n            remote_node_id (str): ID of the detected squid\n            is_visible (bool): Whether the squid is currently visible\n        \"\"\"\n        # Only react if the squid is not sleeping\n        if self.is_sleeping:\n            return\n        \n        if is_visible:\n            # Detected a new squid or is continuing to see it\n            \n            # Increase curiosity when first detected\n            if not hasattr(self, '_seen_squids') or remote_node_id not in self._seen_squids:\n                # First time seeing this squid\n                self.curiosity = min(100, self.curiosity + 15)\n                \n                # Small anxiety spike from the surprise\n                self.anxiety = min(100, self.anxiety + 10)\n                \n                # Add memory\n                self.memory_manager.add_short_term_memory(\n                    'social', 'squid_detection',\n                    f\"Detected another squid (ID: {remote_node_id[-4:]})\"\n                )\n                \n                # Initialize tracking of seen squids if needed\n                if not hasattr(self, '_seen_squids'):\n                    self._seen_squids = set()\n                \n                # Add to seen squids\n                self._seen_squids.add(remote_node_id)\n                \n                # Chance to get startled\n                if random.random() < 0.3:  # 30% chance\n                    # Try to use the startle function if it exists\n                    if hasattr(self.tamagotchi_logic, 'startle_squid'):\n                        self.tamagotchi_logic.startle_squid(source=\"detected_squid\")\n            else:\n                # Already seen this squid before, smaller reaction\n                self.curiosity = min(100, self.curiosity + 5)\n        else:\n            # Lost sight of a squid\n            # Nothing special happens, just note it\n            if hasattr(self, '_seen_squids') and remote_node_id in self._seen_squids:\n                self.memory_manager.add_short_term_memory(\n                    'social', 'squid_lost',\n                    f\"Lost sight of squid (ID: {remote_node_id[-4:]})\"\n                )\n\n    def react_to_rock_throw(self, source_node_id, is_target=False):\n        \"\"\"\n        React to a rock being thrown by another squid\n        \n        Args:\n            source_node_id (str): ID of the squid that threw the rock\n            is_target (bool): Whether this squid is the apparent target\n        \"\"\"\n        # Only react if the squid is not sleeping\n        if self.is_sleeping:\n            return\n        \n        # Base reaction - increase anxiety\n        self.anxiety = min(100, self.anxiety + 5)\n        \n        # Add memory\n        self.memory_manager.add_short_term_memory(\n            'observation', 'rock_throw',\n            f\"Observed squid {source_node_id[-4:]} throw a rock\"\n        )\n        \n        # Strong reaction if targeted\n        if is_target:\n            # Get startled\n            if hasattr(self.tamagotchi_logic, 'startle_squid'):\n                self.tamagotchi_logic.startle_squid(source=\"targeted_by_rock\")\n            \n            # Significantly increase anxiety\n            self.anxiety = min(100, self.anxiety + 20)\n            \n            # Decrease happiness\n            self.happiness = max(0, self.happiness - 10)\n            \n            # Add memory of being targeted\n            self.memory_manager.add_short_term_memory(\n                'social', 'targeted',\n                f\"Was targeted by rock from squid {source_node_id[-4:]}\"\n            )\n            \n            # Higher chance for this memory to go to long-term\n            if random.random() < 0.5:  # 50% chance\n                self.memory_manager.transfer_to_long_term_memory(\n                    'social', 'targeted'\n                )\n\n\n    def investigate_food(self, food_item):\n        self.status = \"Investigating food\"\n        self.tamagotchi_logic.show_message(\"Stubborn squid investigates the food...\")\n\n        # Move towards the food\n        food_pos = food_item.pos()\n        self.move_towards_position(food_pos)\n\n        # Wait for a moment (might need to implement a delay here)\n\n        self.tamagotchi_logic.show_message(\"Stubborn squid ignored the food\")\n        self.status = \"I don't like that food\"\n\n    def consume_food(self, food_item):\n        self.status = \"Ate food\"\n        self.hunger = max(0, self.hunger - 20)\n        self.happiness = min(100, self.happiness + 10)\n        self.satisfaction = min(100, self.satisfaction + 15)\n        self.anxiety = max(0, self.anxiety - 10)\n        self.tamagotchi_logic.remove_food(food_item)\n        #print(\"The squid ate the food\")\n        self.show_eating_effect()\n        self.start_poop_timer()\n        self.pursuing_food = False\n        self.target_food = None\n\n        # Occasionally show a message based on personality\n        if random.random() < 0.25:  # 25% chance to show a message\n            if self.personality == Personality.STUBBORN and getattr(food_item, 'is_sushi', False):\n                self.tamagotchi_logic.show_message(\"Nom nom! Stubborn squid enjoys the sushi!\")\n            elif self.personality == Personality.GREEDY:\n                food_type = \"sushi\" if getattr(food_item, 'is_sushi', False) else \"cheese\"\n                self.tamagotchi_logic.show_message(f\"Nom nom! Greedy squid gobbles up the {food_type}!\")\n            else:\n                self.tamagotchi_logic.show_message(\"Nom nom! Squid enjoys the meal!\")\n\n    def start_poop_timer(self):\n        poop_delay = random.randint(11000, 30000)\n        #print(\"Poop random timer started\")\n        self.poop_timer = QtCore.QTimer()\n        self.poop_timer.setSingleShot(True)\n        self.poop_timer.timeout.connect(self.create_poop)\n        self.poop_timer.start(poop_delay)\n\n    def create_poop(self):\n        self.tamagotchi_logic.spawn_poop(self.squid_x + self.squid_width // 2, self.squid_y + self.squid_height)\n        # Add tracking\n        if hasattr(self.tamagotchi_logic, 'track_poop_created'):\n            self.tamagotchi_logic.track_poop_created()\n\n    def show_eating_effect(self):\n        if not self.is_debug_mode():\n            return\n\n        effect_item = QtWidgets.QGraphicsEllipseItem(self.squid_x, self.squid_y, self.squid_width, self.squid_height)\n        effect_item.setBrush(QtGui.QBrush(QtGui.QColor(255, 255, 0, 100)))\n        effect_item.setPen(QtGui.QPen(QtCore.Qt.NoPen))\n        self.ui.scene.addItem(effect_item)\n\n        opacity_effect = QtWidgets.QGraphicsOpacityEffect()\n        effect_item.setGraphicsEffect(opacity_effect)\n\n        self.eating_animation = QtCore.QPropertyAnimation(opacity_effect, b\"opacity\")\n        self.eating_animation.setDuration(1000)\n        self.eating_animation.setStartValue(2.5)\n        self.eating_animation.setEndValue(0.0)\n        self.eating_animation.setEasingCurve(QtCore.QEasingCurve.InQuad)\n\n        self.eating_animation.finished.connect(lambda: self.ui.scene.removeItem(effect_item))\n\n        self.eating_animation.start()\n\n    def is_debug_mode(self):\n        return self.tamagotchi_logic.debug_mode\n    \n\n    def change_to_rps_image(self):\n        self.rps_image = QtGui.QPixmap(os.path.join(\"images\", \"squid_rps_frame.png\"))\n        self.squid_item.setPixmap(self.rps_image)\n\n    def restore_normal_image(self):\n        self.squid_item.setPixmap(self.current_image())\n\n    def should_hoard_decorations(self):\n        \"\"\"Check if this personality type should hoard items\"\"\"\n        return self.personality in [Personality.GREEDY, Personality.STUBBORN]\n\n    def organize_decorations(self):\n        target_corner = (50, 50)  # Top-left corner coordinates\n        \n        # Get nearby decorations (filter by rocks/plants if needed)\n        decorations = [\n            d for d in self.tamagotchi_logic.get_nearby_decorations(self.squid_x, self.squid_y) \n            if getattr(d, 'category', None) in ['rock', 'plant']\n        ]\n        \n        if decorations:\n            closest = min(decorations, key=lambda d: self.distance_to(d.pos().x(), d.pos().y()))\n            \n            # Move toward the decoration\n            if self.distance_to(closest.pos().x(), closest.pos().y()) > 50:\n                self.move_towards(closest.pos().x(), closest.pos().y())\n                return \"approaching_decoration\"\n            \n            # Push toward hoard corner\n            push_direction = 1 if target_corner[0] > closest.pos().x() else -1\n            self.push_decoration(closest, push_direction)\n            \n            # Personality-specific effects\n            self.satisfaction = min(100, self.satisfaction + 10)\n            if self.personality == Personality.GREEDY:\n                self.tamagotchi_logic.show_message(\"Greedy squid hoards treasures!\")\n            \n            return \"hoarding\"\n        return \"nothing_to_hoard\"\n\n    def go_to_sleep(self):\n        if not self.is_sleeping:\n            self.is_sleeping = True\n            self.squid_direction = \"down\"\n            self.status = \"sleeping\"\n            self.anxiety = max(0, self.anxiety - (1.5 * self.tamagotchi_logic.simulation_speed)) # Anxiety reduction test\n            \n            # Trigger hook if tamagotchi_logic exists\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n                if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                    self.tamagotchi_logic.plugin_manager.trigger_hook(\n                        \"on_sleep\", \n                        squid=self\n                    )\n            \n            # Clear all short-term memories when the squid goes to sleep\n            self.memory_manager.clear_short_term_memory()\n\n            self.tamagotchi_logic.show_message(\"Squid is sleeping...\")\n\n    def wake_up(self):\n        self.is_sleeping = False\n        self.sleepiness = 0\n        self.happiness = min(100, self.happiness + 20)\n        self.status = \"roaming\"\n        self.squid_direction = \"left\"\n        self.update_squid_image()\n        self.tamagotchi_logic.show_message(\"Squid woke up!\")\n\n    def update_squid_image(self):\n        self.squid_item.setPixmap(self.current_image())\n\n    def current_image(self):\n        \"\"\"\n        Return the current image of the squid, with tint applied using\n        CompositionMode_SourceAtop to tint all non-transparent pixels.\n        \"\"\"\n        # 1. Select the base image based on status/direction\n        if hasattr(self, 'startled_transition') and self.startled_transition:\n            base_image = self.startled_image\n        elif hasattr(self, 'status') and self.status == \"startled\" and not self.is_sleeping:\n            direction = \"left\" if random.random() < 0.5 else \"right\"\n            base_image = self.images[f\"{direction}{self.current_frame + 1}\"]\n        elif self.is_sleeping:\n            base_image = self.images[f\"sleep{self.current_frame + 1}\"]\n        elif self.squid_direction == \"left\":\n            base_image = self.images[f\"left{self.current_frame + 1}\"]\n        elif self.squid_direction == \"right\":\n            base_image = self.images[f\"right{self.current_frame + 1}\"]\n        elif self.squid_direction == \"up\":\n            base_image = self.images[f\"up{self.current_frame + 1}\"]\n        else:\n            base_image = self.images[\"left1\"]\n\n        # 2. If no tint, return immediately\n        if not self.tint_color:\n            return base_image\n\n        # 3. Apply robust tinting using QPainter\n        # Create a blank canvas the size of the image\n        result = QtGui.QPixmap(base_image.size())\n        result.fill(QtCore.Qt.transparent)\n        \n        painter = QtGui.QPainter(result)\n        \n        # Draw the base squid\n        painter.drawPixmap(0, 0, base_image)\n        \n        # Draw the tint color over it, keeping the squid's alpha channel\n        # CompositionMode_SourceAtop keeps destination alpha (squid shape) \n        # but paints source (color) over it.\n        painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceAtop)\n        \n        # Use a semi-transparent version of the tint so texture shows through\n        tint = QtGui.QColor(self.tint_color)\n        tint.setAlpha(120)  # Adjust 0-255 for tint strength (120 is usually good)\n        painter.fillRect(result.rect(), tint)\n        \n        painter.end()\n        \n        return result\n\n    def move_randomly(self):\n        if random.random() < 0.20:\n            self.change_direction()\n\n    def get_food_position(self):\n        if self.tamagotchi_logic.food_items:\n            closest_food = min(self.tamagotchi_logic.food_items,\n                               key=lambda food: self.distance_to(food.pos().x(), food.pos().y()))\n            return closest_food.pos().x(), closest_food.pos().y()\n        else:\n            return -1, -1\n\n    def distance_to(self, x, y):\n        return math.sqrt((self.squid_x - x)**2 + (self.squid_y - y)**2)\n\n    def change_direction(self):\n        directions = [\"left\", \"right\", \"up\", \"down\"]\n        new_direction = random.choice(directions)\n        while new_direction == self.squid_direction:\n            new_direction = random.choice(directions)\n        self.squid_direction = new_direction\n\n    def toggle_view_cone(self):\n        self.view_cone_visible = not self.view_cone_visible\n        if self.view_cone_visible:\n            self.update_view_cone()\n        else:\n            self.remove_view_cone()\n\n    def update_view_cone(self):\n        if self.view_cone_visible:\n            if self.view_cone_item is None:\n                self.view_cone_item = QtWidgets.QGraphicsPolygonItem()\n                self.view_cone_item.setPen(QtGui.QPen(QtCore.Qt.yellow))\n                self.view_cone_item.setBrush(QtGui.QBrush(QtGui.QColor(255, 255, 0, 50)))\n                self.ui.scene.addItem(self.view_cone_item)\n\n            squid_center_x = self.squid_x + self.squid_width // 2\n            squid_center_y = self.squid_y + self.squid_height // 2\n\n            if self.pursuing_food and self.target_food:\n                dx = self.target_food[0] - squid_center_x\n                dy = self.target_food[1] - squid_center_y\n                self.current_view_angle = math.atan2(dy, dx)\n\n            cone_length = max(self.ui.window_width, self.ui.window_height)\n\n            cone_points = [\n                QtCore.QPointF(squid_center_x, squid_center_y),\n                QtCore.QPointF(squid_center_x + math.cos(self.current_view_angle - self.view_cone_angle/2) * cone_length,\n                               squid_center_y + math.sin(self.current_view_angle - self.view_cone_angle/2) * cone_length),\n                QtCore.QPointF(squid_center_x + math.cos(self.current_view_angle + self.view_cone_angle/2) * cone_length,\n                               squid_center_y + math.sin(self.current_view_angle + self.view_cone_angle/2) * cone_length)\n            ]\n\n            cone_polygon = QtGui.QPolygonF(cone_points)\n            self.view_cone_item.setPolygon(cone_polygon)\n        else:\n            self.remove_view_cone()\n\n    def remove_view_cone(self):\n        if self.view_cone_item is not None:\n            self.ui.scene.removeItem(self.view_cone_item)\n            self.view_cone_item = None\n\n    def show_sick_icon(self):\n        if self.sick_icon_item is None:\n            sick_icon_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"sick.png\"))\n            self.sick_icon_item = QtWidgets.QGraphicsPixmapItem(sick_icon_pixmap)\n            self.sick_icon_item.setZValue(500)  # Below DIRTY text (z=1000) but above decorations\n            self.ui.scene.addItem(self.sick_icon_item)\n        self.update_sick_icon_position()\n\n    def hide_sick_icon(self):\n        if self.sick_icon_item is not None:\n            self.ui.scene.removeItem(self.sick_icon_item)\n            self.sick_icon_item = None\n\n    def update_sick_icon_position(self):\n        if self.sick_icon_item is not None:\n            self.sick_icon_item.setPos(self.squid_x + self.squid_width // 2 - self.sick_icon_item.pixmap().width() // 2 + self.sick_icon_offset.x(),\n                                       self.squid_y + self.sick_icon_offset.y())\n\n    def is_near_plant(self):\n        if self.tamagotchi_logic is None:\n            return False\n\n        nearby_decorations = self.tamagotchi_logic.get_nearby_decorations(self.squid_x, self.squid_y)\n        return any(decoration.category == 'plant' for decoration in nearby_decorations)\n\n    def move_towards_plant(self):\n        if self.tamagotchi_logic is None:\n            return\n\n        nearby_decorations = self.tamagotchi_logic.get_nearby_decorations(self.squid_x, self.squid_y)\n        plants = [d for d in nearby_decorations if d.category == 'plant']\n\n        if plants:\n            closest_plant = min(plants, key=lambda p: self.distance_to(p.pos().x(), p.pos().y()))\n            self.move_towards(closest_plant.pos().x(), closest_plant.pos().y())\n        else:\n            self.move_randomly()\n\n    def should_organize_decorations(self):\n        return (self.curiosity > 70 and\n                self.satisfaction < 80 and\n                self.personality in [Personality.ADVENTUROUS, Personality.ENERGETIC])\n\n    def organize_decorations(self):\n        target_corner = (50, 50)  # Top-left corner\n        decorations = self.tamagotchi_logic.get_nearby_decorations(self.squid_x, self.squid_y)\n\n        if decorations:\n            closest = min(decorations, key=lambda d: self.distance_to(d.pos().x(), d.pos().y()))\n            self.move_towards(closest.pos().x(), closest.pos().y())\n\n            if self.distance_to(closest.pos().x(), closest.pos().y()) < 50:\n                self.push_decoration(closest, direction=1 if random.random() < 0.5 else -1)\n                self.satisfaction = min(100, self.satisfaction + 5)\n                return \"organizing decorations\"\n        return \"searching for decorations\"\n\n    def interact_with_rocks(self):\n        rocks = [d for d in self.tamagotchi_logic.get_nearby_decorations(self.squid_x, self.squid_y)\n                 if d.category == 'rock']\n        if rocks:\n            self.push_decoration(random.choice(rocks), random.choice([-1, 1]))\n            self.satisfaction = min(100, self.satisfaction + 8)\n            self.happiness = min(100, self.happiness + 5)\n            return \"interacting with rocks\"\n        return \"no rocks nearby\"\n\n    def can_pick_up_rock(self, rock_item):\n        \"\"\"Check if squid can pick up this rock\"\"\"\n        rock_rect = rock_item.sceneBoundingRect()\n        rock_center = rock_rect.center()\n        \n        squid_rect = self.squid_item.sceneBoundingRect()\n        squid_center = squid_rect.center()\n        \n        distance = math.sqrt((rock_center.x() - squid_center.x())**2 + \n                            (rock_center.y() - squid_center.y())**2)\n        \n        can_pick = (not self.carrying_rock and \n                    not self.is_sleeping and\n                    distance < 50)\n        \n        print(f\"Can pick up rock check:\")\n        print(f\"- Rock center: ({rock_center.x():.1f}, {rock_center.y():.1f})\")\n        print(f\"- Squid center: ({squid_center.x():.1f}, {squid_center.y():.1f})\")\n        print(f\"- Distance: {distance:.1f}\")\n        print(f\"- Carrying: {self.carrying_rock}\")\n        print(f\"- Sleeping: {self.is_sleeping}\")\n        print(f\"- Result: {can_pick}\")\n        \n        return can_pick\n\n    def pick_up_rock(self, item):\n        \"\"\"Delegate to interaction manager with random carry duration\"\"\"\n        if not hasattr(self.tamagotchi_logic, 'rock_interaction'):\n            return False\n        return self.tamagotchi_logic.rock_interaction.attach_rock_to_squid(item)\n\n    def throw_rock(self, direction):\n        \"\"\"\n        Delegate to interaction manager BUT capture result for RL reward.\n        Returns:  bool  True = rock landed (success), False = fell out of bounds / no effect\n        \"\"\"\n        if not hasattr(self.tamagotchi_logic, 'rock_interaction'):\n            return False\n\n        # --- Call the existing manager ---\n        success = self.tamagotchi_logic.rock_interaction.throw_rock(direction)\n\n        # --- Compute RL reward ---\n        reward = 0.0\n        if success:\n            reward += 5.0                       # base success\n            reward += self.happiness * 0.05     # happier squid → bigger reward\n            reward += self.satisfaction * 0.05\n            if self.personality == Personality.GREEDY:\n                reward += 3.0                   # greedy squid LOVES throwing\n            # memory boost\n            rock_mem = self.memory_manager.get_short_term_memory('interaction', 'rock_throw')\n            if rock_mem and isinstance(rock_mem, dict) and rock_mem.get('is_positive'):\n                reward += 2.0\n        else:\n            reward -= 2.0                       # small penalty for miss\n\n        # --- Push reward into RL ---\n        if hasattr(self.tamagotchi_logic, 'give_rl_reward'):\n            self.tamagotchi_logic.give_rl_reward(reward)\n\n        # --- Flag for neurogenesis / stats ---\n        if hasattr(self.tamagotchi_logic, 'recent_positive_outcome'):\n            self.tamagotchi_logic.recent_positive_outcome = success\n\n        return success\n\n\n    def update_rock_throw(self):\n        if not self.rock_being_thrown or not self.rock_being_thrown.scene():\n            self.rock_animation_timer.stop()\n            return\n        \n        rock = self.rock_being_thrown\n        current_pos = rock.pos()\n        \n        # Heavy rock physics - sink quickly with minimal bouncing\n        self.rock_velocity_y += 2.0  # Strong gravity for quick sinking\n        \n        # Calculate new position\n        new_x = current_pos.x() + self.rock_velocity_x\n        new_y = current_pos.y() + self.rock_velocity_y\n        \n        # Get scene boundaries\n        scene_rect = self.ui.scene.sceneRect()\n        rock_rect = rock.boundingRect()\n        \n        # Stop at reachable depth (200px from bottom)\n        max_y = self.ui.window_height - 200 - rock_rect.height()\n        if new_y > max_y:\n            new_y = max_y\n            self.rock_animation_timer.stop()\n            self.rock_being_thrown = None\n            return\n        \n        # Minimal horizontal movement when sinking\n        if abs(self.rock_velocity_y) > 1.0:  # If sinking fast\n            self.rock_velocity_x *= 0.3  # Strong horizontal dampening\n        \n        # Basic wall collisions\n        if new_x < scene_rect.left():\n            new_x = scene_rect.left()\n            self.rock_velocity_x *= -0.5  # Weak wall bounce\n        elif new_x > scene_rect.right() - rock_rect.width():\n            new_x = scene_rect.right() - rock_rect.width()\n            self.rock_velocity_x *= -0.5\n        \n        # Update position\n        rock.setPos(new_x, new_y)\n\n\n    def check_rock_interaction(self):\n        if not hasattr(self, 'tamagotchi_logic') or self.tamagotchi_logic is None:\n            return False\n            \n        if not hasattr(self.tamagotchi_logic, 'config_manager'):\n            return False\n            \n        config = self.tamagotchi_logic.config_manager.get_rock_config()\n        \n        decorations = self.tamagotchi_logic.get_nearby_decorations(\n            self.squid_x, self.squid_y, 150)\n        interactables = [d for d in decorations if d.category in ['rock', 'poop']]\n\n        if (self.carrying_rock \n                and self.rock_throw_cooldown == 0 \n                and random.random() < config['throw_prob']):\n            direction = random.choice([\"left\", \"right\"])\n            if self.throw_rock(direction):\n                return\n\n        if (not self.carrying_rock \n                and self.rock_throw_cooldown == 0 \n                and interactables\n                and random.random() < config['pickup_prob']):\n            target_item = random.choice(interactables)\n            \n            if self.pick_up_rock(target_item):\n                mem_details = {\n                    \"item\": getattr(target_item, 'filename', f'unknown_{target_item.category}'),\n                    \"position\": (target_item.pos().x(), target_item.pos().y()),\n                    \"timestamp\": datetime.now().isoformat()\n                }\n                self.memory_manager.add_short_term_memory(\n                    'interaction', f'{target_item.category}_pickup', mem_details)\n            else:\n                print(f\"[DEBUG] {target_item.category.capitalize()} pickup failed\")\n\n    def check_poop_interaction(self):\n        \"\"\"Periodic poop interaction check similar to rock interaction\"\"\"\n        if not hasattr(self.tamagotchi_logic, 'poop_interaction'):\n            return False\n            \n        config = self.tamagotchi_logic.config_manager.get_poop_config()\n        \n        decorations = self.tamagotchi_logic.get_nearby_decorations(\n            self.squid_x, self.squid_y, 150)\n        interactables = [d for d in decorations if d.category == 'poop']\n\n        # If carrying poop and cooldown is done, potentially throw\n        if (self.carrying_poop \n                and self.poop_throw_cooldown == 0 \n                and random.random() < config['throw_prob']):\n            direction = random.choice([\"left\", \"right\"])\n            if self.throw_poop(direction):\n                return\n\n        # If not carrying poop and cooldown is done, potentially pick up\n        if (not self.carrying_poop \n                and self.poop_throw_cooldown == 0 \n                and interactables\n                and random.random() < config['pickup_prob']):\n            target_item = random.choice(interactables)\n            \n            if self.pick_up_poop(target_item):\n                mem_details = {\n                    \"item\": getattr(target_item, 'filename', 'unknown_poop'),\n                    \"position\": (target_item.pos().x(), target_item.pos().y()),\n                    \"timestamp\": datetime.now().isoformat()\n                }\n                self.memory_manager.add_short_term_memory(\n                    'interaction', 'poop_pickup', mem_details)\n            else:\n                print(f\"[DEBUG] Poop pickup failed\")\n\n    def get_center(self):\n        \"\"\"Return the center position of the squid\"\"\"\n        return (self.squid_x + self.squid_width/2, \n                self.squid_y + self.squid_height/2)\n    \n    def move_toward_position(self, target_pos):\n        \"\"\"Move directly toward a QPointF or (x,y) position with rock interaction support\"\"\"\n        # Handle both QPointF and tuple/position inputs\n        if isinstance(target_pos, QtCore.QPointF):\n            target_x, target_y = target_pos.x(), target_pos.y()\n        else:\n            target_x, target_y = target_pos\n        \n        # Get precise center positions using scene coordinates\n        squid_rect = self.squid_item.sceneBoundingRect()\n        squid_center_x = squid_rect.center().x()\n        squid_center_y = squid_rect.center().y()\n        \n        dx = target_x - squid_center_x\n        dy = target_y - squid_center_y\n        distance = math.hypot(dx, dy)  # More efficient than math.sqrt\n        \n        if distance > 5:  # Small threshold to prevent micro-movements\n            # Normalize and scale by speed (removed temporary 1.5x boost)\n            norm = max(distance, 0.1)  # Avoid division by zero\n            move_x = (dx/norm) * self.base_squid_speed * self.animation_speed\n            move_y = (dy/norm) * self.base_vertical_speed * self.animation_speed\n            \n            # Update position\n            self.squid_x += move_x\n            self.squid_y += move_y\n            \n            # Update direction - more precise handling\n            if abs(move_x) > abs(move_y):\n                self.squid_direction = \"right\" if move_x > 0 else \"left\"\n            else:\n                self.squid_direction = \"down\" if move_y > 0 else \"up\"\n            \n            # Enforce boundaries\n            self.squid_x = max(50, min(self.squid_x, self.ui.window_width - 50 - self.squid_width))\n            self.squid_y = max(50, min(self.squid_y, self.ui.window_height - 120 - self.squid_height))\n            \n            # Update graphics\n            self.squid_item.setPos(self.squid_x, self.squid_y)\n            self.current_frame = (self.current_frame + 1) % 2\n            self.update_squid_image()  # Changed to use method instead of direct pixmap set\n        \n        return distance\n\n\n"
  },
  {
    "path": "src/squid_facts.py",
    "content": "import random\r\n\r\nSQUID_FACTS = [\r\n    \"Dosidicus Gigas: scientific name for Humboldt squid\",\r\n    \"Humboldt squid can reach a mantle length of 1.5 m (5 ft)\",\r\n    \"Cephalopods have a hard beak like a parrot!\",\r\n    \"Cephalopod blood is blue because it is copper-based\",\r\n    \"These predators thrive in the `Oxygen Minimum Zone.`\",\r\n    \"A squid's beak is the only rigid part of its body.\",\r\n    \"Some squids have reached speeds of 25 km/h!\",\r\n    \"Cannibalism occurs among the species when food is scarce.\",\r\n    \"The Kraken is a legendary, ship-sinking gigantic squid\",\r\n    \"Cephalopods are the fastest growing marine invertebrate\",\r\n    \"Giant Squid can grow to 30-foot-long in just a few years\",\r\n    \"Squids use jet propulsion to move quickly through the water\",\r\n    \"Squids breathe using gills located inside their mantle\",\r\n]\r\n\r\ndef get_random_fact():\r\n    \"\"\"Return a squid fact\"\"\"\r\n\r\n\r\n    return random.choice(SQUID_FACTS)\r\n"
  },
  {
    "path": "src/squid_statistics.py",
    "content": "import time\r\nimport math\r\n\r\n# Distance tracking constants\r\nDISTANCE_ROLLOVER_LIMIT = 999_999_999  # ~1 billion pixels before rollover\r\n\r\nclass SquidStatistics:\r\n    def __init__(self, squid):\r\n\r\n        self.squid = squid\r\n        self.start_time = time.time()\r\n        self.total_age_seconds = 0\r\n        self.sushi_consumed = 0\r\n        self.cheese_consumed = 0\r\n        self.total_memories_formed = 0\r\n        self.highest_anxiety = 0\r\n        self.lowest_happiness = 100\r\n        self.highest_satisfaction = 0\r\n        self.distance_swam = 0\r\n        self.distance_swam_multiplier = 1\r\n        self.other_squids_encountered = 0\r\n        self.total_rocks_thrown = 0\r\n        self.total_poops_thrown = 0\r\n        self.total_env_interactions = 0\r\n        self.ink_clouds_created = 0\r\n        self.plants_interacted = 0\r\n        self.time_spent_asleep = 0\r\n        self.peak_novelty = 0\r\n        self.peak_stress = 0\r\n        self.peak_reward = 0\r\n        self.novelty_neurons_created = 0\r\n        self.stress_neurons_created = 0\r\n        self.reward_neurons_created = 0\r\n        self.poops_created = 0\r\n        self.max_poops_cleaned = 0\r\n        self.startles_experienced = 0\r\n        self.times_colour_changed = 0\r\n        self.sickness_episodes = 0\r\n        self.max_neurons_reached = 7\r\n        self.current_neurons = 7\r\n\r\n    def get_total_age_seconds(self):\r\n        \"\"\"Calculates the total persistent age in seconds.\"\"\"\r\n        current_session_age = time.time() - self.start_time\r\n        return self.total_age_seconds + current_session_age\r\n    \r\n    def update_distance(self, dx, dy):\r\n        '''Track distance traveled by squid with rollover protection'''\r\n        distance = math.sqrt(dx*dx + dy*dy)\r\n        self.distance_swam += distance\r\n        \r\n        # Check for rollover and reset with multiplier\r\n        if self.distance_swam >= DISTANCE_ROLLOVER_LIMIT:\r\n            self.distance_swam = self.distance_swam - DISTANCE_ROLLOVER_LIMIT\r\n            self.distance_swam_multiplier += 1\r\n            \r\n            # Log the rollover event\r\n            if hasattr(self.squid, 'tamagotchi_logic'):\r\n                self.squid.tamagotchi_logic.show_message(\r\n                    f\"🌊 Distance counter rolled over! Now at {self.distance_swam_multiplier}x\"\r\n                )\r\n    \r\n    def get_distance_display(self):\r\n        '''Get formatted distance string with multiplier if needed'''\r\n        if self.distance_swam_multiplier > 1:\r\n            return f\"{self.distance_swam_multiplier}x {int(self.distance_swam):,}\"\r\n        return f\"{int(self.distance_swam):,}\"\r\n\r\n    def get_squid_age(self):\r\n        \"\"\"\r\n        Return the squid’s age as a readable string:\r\n            1 min  – 59 min   → “<n> min”\r\n            60 min – 89 min   → “1 hr”\r\n            90 min – 119 min  → “1.5 hrs”\r\n            120 min – 149 min → “2 hrs”\r\n            150 min – 179 min → “2.5 hrs”\r\n            …and so on, stepping in 30-minute blocks.\r\n        \"\"\"\r\n        total_minutes = int(self.get_total_age_seconds() // 60)\r\n\r\n        if total_minutes < 60:                      # still in minutes\r\n            return f\"{total_minutes} min\" + (\"s\" if total_minutes != 1 else \"\")\r\n\r\n        # 60 min and above → switch to hours, 30-min steps\r\n        whole_hours = total_minutes // 60\r\n        half_hour   = (total_minutes % 60) // 30   # 0 or 1\r\n\r\n        hours_str = f\"{whole_hours}\" if half_hour == 0 else f\"{whole_hours}.5\"\r\n        return f\"{hours_str} hr\" + (\"s\" if whole_hours + half_hour != 1 else \"\")\r\n    \r\n    def load_statistics(self, data):\r\n        \"\"\"Load statistics from saved data dictionary\"\"\"\r\n        if not data:\r\n            return\r\n            \r\n        # Core stats\r\n        self.total_age_seconds = data.get('total_age_seconds', 0)\r\n        self.distance_swam = data.get('distance_swam', 0)\r\n        self.distance_swam_multiplier = data.get('distance_swam_multiplier', 1)\r\n        \r\n        # Food consumption\r\n        self.sushi_consumed = data.get('sushi_eaten', 0)\r\n        self.cheese_consumed = data.get('cheese_eaten', 0)\r\n        \r\n        # Mental states\r\n        self.highest_anxiety = data.get('highest_anxiety', 0)\r\n        self.lowest_happiness = data.get('lowest_happiness', 100)\r\n        \r\n        # Object interactions\r\n        self.total_rocks_thrown = data.get('rocks_thrown', 0)\r\n        self.total_poops_thrown = data.get('poops_thrown', 0)\r\n        self.ink_clouds_created = data.get('ink_clouds_created', 0)\r\n        self.plants_interacted = data.get('plants_interacted', 0)\r\n        \r\n        # Other tracked stats\r\n        self.times_colour_changed = data.get('times_colour_changed', 0)\r\n        self.poops_created = data.get('poops_created', 0)\r\n        self.max_poops_cleaned = data.get('max_poops_cleaned', 0)\r\n        self.startles_experienced = data.get('startles_experienced', 0)\r\n        \r\n        # Time and health\r\n        self.time_spent_asleep = data.get('total_sleep_time', 0)\r\n        self.sickness_episodes = data.get('sickness_episodes', 0)\r\n        \r\n        # Neurogenesis\r\n        self.novelty_neurons_created = data.get('novelty_neurons_created', 0)\r\n        self.stress_neurons_created = data.get('stress_neurons_created', 0)\r\n        self.reward_neurons_created = data.get('reward_neurons_created', 0)\r\n        \r\n        # --- FIX: Ensure these are loaded correctly, defaulting to 7 ---\r\n        self.max_neurons_reached = data.get('max_neurons_reached', 7)\r\n        self.current_neurons = data.get('current_neurons', 7)\r\n\r\n    def update(self):\r\n        # Update peak mental states\r\n        if self.squid.anxiety > self.highest_anxiety:\r\n            self.highest_anxiety = self.squid.anxiety\r\n        if self.squid.happiness < self.lowest_happiness:\r\n            self.lowest_happiness = self.squid.happiness\r\n        if self.squid.satisfaction > self.highest_satisfaction:\r\n            self.highest_satisfaction = self.squid.satisfaction\r\n            \r\n        if self.squid.is_sleeping:\r\n            self.time_spent_asleep += 1 # update is called every second\r\n\r\n        # --- Check and update peak neurogenesis values ---\r\n        if self.squid.tamagotchi_logic and self.squid.tamagotchi_logic.brain_window:\r\n            brain_widget = self.squid.tamagotchi_logic.brain_window.brain_widget\r\n            if brain_widget:\r\n                # 1. Update Neurogenesis Stats\r\n                if hasattr(brain_widget, 'neurogenesis_data'):\r\n                    neuro_data = brain_widget.neurogenesis_data\r\n                    current_novelty = neuro_data.get('novelty_counter', 0)\r\n                    current_stress = neuro_data.get('stress_counter', 0)\r\n                    current_reward = neuro_data.get('reward_counter', 0)\r\n\r\n                    if current_novelty > self.peak_novelty:\r\n                        self.peak_novelty = current_novelty\r\n                    if current_stress > self.peak_stress:\r\n                        self.peak_stress = current_stress\r\n                    if current_reward > self.peak_reward:\r\n                        self.peak_reward = current_reward\r\n                \r\n                # 2. Update Max Neurons Reached (FIX)\r\n                if hasattr(brain_widget, 'neuron_positions'):\r\n                    actual_count = len(brain_widget.neuron_positions)\r\n                    if actual_count > self.max_neurons_reached:\r\n                        self.max_neurons_reached = actual_count\r\n                    self.current_neurons = actual_count\r\n\r\n    def get_sleep_time(self):\r\n        hours = int(self.time_spent_asleep // 3600)\r\n        minutes = int((self.time_spent_asleep % 3600) // 60)\r\n        return f\"{hours}h {minutes}m\""
  },
  {
    "path": "src/statistics_window.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nimport time, json, math, random, os\r\nfrom .localisation import Localisation\r\n\r\n\r\nclass StatBox(QtWidgets.QWidget):\r\n    \"\"\"A single stat display box with label and value.\"\"\"\r\n    def __init__(self, label_key, parent=None):\r\n        super().__init__(parent)\r\n        self.loc = Localisation.instance()\r\n        self.label_key = label_key\r\n        \r\n        screen = QtWidgets.QApplication.primaryScreen()\r\n        screen_size = screen.size()\r\n        layout = QtWidgets.QVBoxLayout(self)\r\n        layout.setContentsMargins(5, 5, 5, 5)\r\n\r\n        if screen_size.width() <= 1920 and screen_size.height() <= 1080:\r\n            value_font_size, label_font_size = 18, 11\r\n            box_width, box_height = 85, 70\r\n        else:\r\n            value_font_size, label_font_size = 28, 16\r\n            box_width, box_height = 120, 100\r\n\r\n        self.value_label = QtWidgets.QLabel(\"0\")\r\n        self.value_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.value_label.setStyleSheet(\r\n            f\"font-size:{value_font_size}px;font-weight:bold;\"\r\n            \"border:2px solid black;background-color:white;\"\r\n        )\r\n        layout.addWidget(self.value_label)\r\n\r\n        self.value_edit = QtWidgets.QLineEdit()\r\n        self.value_edit.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.value_edit.setStyleSheet(\r\n            f\"font-size:{value_font_size}px;font-weight:bold;\"\r\n            \"border:2px solid black;background-color:white;\"\r\n        )\r\n        self.value_edit.hide()\r\n        layout.addWidget(self.value_edit)\r\n\r\n        # Use localised label\r\n        self.name_label = QtWidgets.QLabel(self.loc.get(label_key))\r\n        self.name_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.name_label.setStyleSheet(f\"font-size:{label_font_size}px;font-weight:bold;\")\r\n        layout.addWidget(self.name_label)\r\n        self.setFixedSize(box_width, box_height)\r\n\r\n    def set_value(self, value):\r\n        self.value_label.setText(str(int(value)))\r\n        self.value_edit.setText(str(int(value)))\r\n\r\n    def get_value(self):\r\n        return int(self.value_edit.text())\r\n\r\n    def set_editable(self, editable):\r\n        self.value_label.setVisible(not editable)\r\n        self.value_edit.setVisible(editable)\r\n    \r\n    def update_label(self):\r\n        \"\"\"Update the label text (for language changes)\"\"\"\r\n        self.name_label.setText(self.loc.get(self.label_key))\r\n\r\n\r\nclass StatisticsWindow(QtWidgets.QWidget):\r\n    def __init__(self, squid, arcade_font_path=None, show_decorations_callback=None):\r\n        super().__init__()\r\n        self.squid = squid\r\n        self.show_decorations_callback = show_decorations_callback\r\n        self.loc = Localisation.instance()\r\n\r\n        screen = QtWidgets.QApplication.primaryScreen()\r\n        screen_size = screen.size()\r\n\r\n        self.setWindowTitle(self.loc.get(\"statistics\"))\r\n        self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint)\r\n        self.move(0, 0)\r\n\r\n        # ---------------- score state ---------------------------------\r\n        self.score         = 0\r\n        self.display_score = 0\r\n        self.combo_level   = 1\r\n        self.combo_timer   = 0\r\n        self.last_food_time= 0\r\n\r\n        # ---------------- font ----------------------------------------\r\n        if arcade_font_path and os.path.isfile(arcade_font_path):\r\n            font_path = arcade_font_path\r\n        else:\r\n            font_path = os.path.join(os.path.dirname(__file__), \"arcade.ttf\")\r\n\r\n        if os.path.isfile(font_path):\r\n            font_id = QtGui.QFontDatabase.addApplicationFont(font_path)\r\n            families = QtGui.QFontDatabase.applicationFontFamilies(font_id)\r\n            font = QtGui.QFont(families[0], 40)\r\n        else:\r\n            font = QtGui.QFont(\"Arial\", 40, QtGui.QFont.Bold)\r\n\r\n        # ---------------- score label ---------------------------------\r\n        self.score_label = QtWidgets.QLabel(\"00000\")\r\n        self.score_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.score_label.setFont(font)\r\n        self.score_label.setStyleSheet(\"color:#000000;\")\r\n\r\n        # ---------------- window sizing -------------------------------\r\n        if screen_size.width() <= 1920 and screen_size.height() <= 1080:\r\n            window_width, window_height = 320, 420\r\n            main_spacing, grid_spacing = 6, 6\r\n            status_font_size, score_font_size = 18, 20\r\n        else:\r\n            window_width, window_height = 450, 600\r\n            main_spacing, grid_spacing = 10, 10\r\n            status_font_size, score_font_size = 24, 32\r\n\r\n        self.setFixedSize(window_width, window_height)\r\n\r\n        # ---------------- layout --------------------------------------\r\n        main_layout = QtWidgets.QVBoxLayout(self)\r\n        main_layout.setSpacing(main_spacing)\r\n\r\n        grid_layout = QtWidgets.QGridLayout()\r\n        grid_layout.setSpacing(grid_spacing)\r\n        main_layout.addLayout(grid_layout)\r\n\r\n        # Create stat boxes with localisation keys\r\n        self.stat_boxes = {\r\n            \"hunger\": StatBox(\"hunger\"),\r\n            \"happiness\": StatBox(\"happiness\"),\r\n            \"cleanliness\": StatBox(\"cleanliness\"),\r\n            \"sleepiness\": StatBox(\"sleepiness\"),\r\n            \"health\": StatBox(\"health\"),\r\n            \"satisfaction\": StatBox(\"satisfaction\"),\r\n            \"curiosity\": StatBox(\"curiosity\"),\r\n            \"anxiety\": StatBox(\"anxiety\"),\r\n        }\r\n\r\n        grid_layout.addWidget(self.stat_boxes[\"health\"],       0, 0)\r\n        grid_layout.addWidget(self.stat_boxes[\"hunger\"],       0, 1)\r\n        grid_layout.addWidget(self.stat_boxes[\"happiness\"],    0, 2)\r\n        grid_layout.addWidget(self.stat_boxes[\"cleanliness\"],  1, 0)\r\n        grid_layout.addWidget(self.stat_boxes[\"sleepiness\"],   1, 1)\r\n\r\n        separator = QtWidgets.QFrame()\r\n        separator.setFrameShape(QtWidgets.QFrame.HLine)\r\n        separator.setFrameShadow(QtWidgets.QFrame.Sunken)\r\n        main_layout.addWidget(separator)\r\n\r\n        new_neurons_layout = QtWidgets.QHBoxLayout()\r\n        new_neurons_layout.setSpacing(10 if window_width > 320 else 6)\r\n        main_layout.addLayout(new_neurons_layout)\r\n        new_neurons_layout.addWidget(self.stat_boxes[\"satisfaction\"])\r\n        new_neurons_layout.addWidget(self.stat_boxes[\"curiosity\"])\r\n        new_neurons_layout.addWidget(self.stat_boxes[\"anxiety\"])\r\n\r\n        self.status_label = QtWidgets.QLabel()\r\n        self.status_label.setAlignment(QtCore.Qt.AlignCenter)\r\n        self.status_label.setStyleSheet(f\"font-size:{status_font_size}px;\")\r\n        main_layout.addWidget(self.status_label)\r\n\r\n        # State indicator pills container\r\n        self.state_pills_container = QtWidgets.QWidget()\r\n        self.state_pills_layout = QtWidgets.QHBoxLayout(self.state_pills_container)\r\n        self.state_pills_layout.setSpacing(10)\r\n        self.state_pills_layout.setContentsMargins(0, 5, 0, 5)\r\n        self.state_pills_layout.addStretch()\r\n        \r\n        # Create up to 3 pill labels\r\n        self.state_pills = []\r\n        for i in range(3):\r\n            pill = QtWidgets.QLabel()\r\n            pill.setAlignment(QtCore.Qt.AlignCenter)\r\n            pill.setMinimumHeight(30 if window_width <= 320 else 40)\r\n            pill.setStyleSheet(\"\"\"\r\n                QLabel {\r\n                    color: white;\r\n                    font-size: 16px;\r\n                    font-weight: bold;\r\n                    border-radius: 5px;\r\n                    padding: 5px 10px;\r\n                }\r\n            \"\"\")\r\n            pill.hide()\r\n            self.state_pills.append(pill)\r\n            self.state_pills_layout.addWidget(pill)\r\n        \r\n        self.state_pills_layout.addStretch()\r\n        main_layout.addWidget(self.state_pills_container)\r\n\r\n        main_layout.addWidget(self.score_label)\r\n\r\n        self.apply_button = QtWidgets.QPushButton(self.loc.get(\"apply_changes\"))\r\n        self.apply_button.clicked.connect(self.apply_changes)\r\n        self.apply_button.hide()\r\n        main_layout.addWidget(self.apply_button)\r\n\r\n        # smooth ticker\r\n        self.roll_timer = QtCore.QTimer(self, timeout=self._tick, interval=16)\r\n        self.roll_timer.start()\r\n        \r\n        # Setup decorations window shortcut\r\n        self.setup_decorations_shortcut()\r\n        \r\n\r\n    # ------------------------------------------------------------------\r\n\r\n    def setup_decorations_shortcut(self):\r\n        \"\"\"Setup keyboard shortcut for decorations window (T key)\"\"\"\r\n        if self.show_decorations_callback:\r\n            self.decorations_shortcut = QtWidgets.QShortcut(\r\n                QtGui.QKeySequence(QtCore.Qt.Key_T), \r\n                self\r\n            )\r\n            self.decorations_shortcut.activated.connect(self.show_decorations_callback)\r\n    \r\n    def _tick(self):\r\n        # Update score display with smooth animation\r\n        diff = self.score - self.display_score\r\n        if abs(diff) > 0:\r\n            step = math.copysign(max(1, abs(diff) // 8), diff)\r\n            self.display_score += step\r\n            self.display_score = max(0, min(99999, self.display_score))\r\n            self.score_label.setText(f\"{int(self.display_score):05d}\")\r\n\r\n        # Update combo timer\r\n        if self.combo_timer > 0:\r\n            self.combo_timer -= 1\r\n        else:\r\n            self.combo_level = 1\r\n\r\n        # Continuously update all statistics from squid\r\n        self.update_statistics()\r\n\r\n    # -------------- external award -----------------------------------\r\n    def award(self, base):\r\n        now = time.time()\r\n        # food chaining\r\n        if base == 100:\r\n            if now - self.last_food_time < 2.0:\r\n                self.combo_level = min(3, self.combo_level + 1)\r\n            else:\r\n                self.combo_level = 1\r\n            self.last_food_time = now\r\n            self.combo_timer = 600\r\n        # poop - no combo change\r\n        elif base == 25:\r\n            pass\r\n\r\n        total = int(base * self.combo_level)\r\n        self.score += total\r\n        self.score = max(0, min(9999, self.score))\r\n\r\n    def set_score(self, value):\r\n        \"\"\"Directly set the score value (used for loading saves)\"\"\"\r\n        self.score = max(0, min(9999, int(value)))\r\n        self.display_score = self.score\r\n        self.score_label.setText(f\"{int(self.display_score):04d}\")\r\n\r\n    def update_score(self):\r\n        \"\"\"Update the score display (called by tamagotchi_logic)\"\"\"\r\n        self.score_label.setText(f\"{int(self.display_score):04d}\")\r\n\r\n    # -------------- specific helpers ---------------------------------\r\n    def add_score_for_food_eaten(self):\r\n        self.award(100)\r\n\r\n    def add_score_for_neuron_creation(self):\r\n        self.award(500)\r\n\r\n    def deduct_score_for_startle(self):\r\n        self.award(-50)\r\n\r\n    def add_score_for_poop_cleaned(self, count):\r\n        self.award(25 * count)\r\n\r\n    # -------------- original interface ---------------------------------\r\n    def update_statistics(self):\r\n        if self.squid is not None:\r\n            for key, box in self.stat_boxes.items():\r\n                if hasattr(self.squid, key):\r\n                    box.set_value(getattr(self.squid, key))\r\n            self.status_label.setText(f\"{self.loc.get('status')}: {self.squid.status}\")\r\n            self._update_state_pill()\r\n\r\n    def _update_state_pill(self):\r\n        \"\"\"Update the state indicator pills to match the brain network view states (up to 3)\"\"\"\r\n        if self.squid is None:\r\n            for pill in self.state_pills:\r\n                pill.hide()\r\n            return\r\n\r\n        # Collect all active states (matches brain_widget.py logic)\r\n        active_states = []\r\n        \r\n        # Check states in priority order with localised text\r\n        if getattr(self.squid, 'is_fleeing', False):\r\n            active_states.append((self.loc.get(\"fleeing\"), \"rgb(220, 20, 60)\"))\r\n        if getattr(self.squid, 'is_startled', False):\r\n            active_states.append((self.loc.get(\"startled\"), \"rgb(255, 165, 0)\"))\r\n        if getattr(self.squid, 'pursuing_food', False):\r\n            active_states.append((self.loc.get(\"pursuing_food\"), \"rgb(60, 179, 113)\"))\r\n        if getattr(self.squid, 'is_eating', False) or 'eating' in self.squid.status.lower():\r\n            active_states.append((self.loc.get(\"eating\"), \"rgb(46, 204, 113)\"))\r\n        if getattr(self.squid, 'is_sleeping', False):\r\n            active_states.append((self.loc.get(\"sleeping\"), \"rgb(142, 68, 173)\"))\r\n        if 'rock' in self.squid.status.lower() or 'play' in self.squid.status.lower():\r\n            active_states.append((self.loc.get(\"playing\"), \"rgb(241, 196, 15)\"))\r\n        if 'hiding' in self.squid.status.lower():\r\n            active_states.append((self.loc.get(\"hiding\"), \"rgb(22, 160, 133)\"))\r\n        if getattr(self.squid, 'anxiety', 0) > 70 or 'anxious' in self.squid.status.lower():\r\n            active_states.append((self.loc.get(\"anxious\"), \"rgb(231, 76, 60)\"))\r\n        if getattr(self.squid, 'curiosity', 0) > 80 or 'curious' in self.squid.status.lower():\r\n            active_states.append((self.loc.get(\"curious\"), \"rgb(52, 152, 219)\"))\r\n        \r\n        # Display up to 3 states\r\n        states_to_show = active_states[:3]\r\n        \r\n        # Update each pill\r\n        for i, pill in enumerate(self.state_pills):\r\n            if i < len(states_to_show):\r\n                state_text, state_color = states_to_show[i]\r\n                pill.setText(state_text)\r\n                pill.setStyleSheet(f\"\"\"\r\n                    QLabel {{\r\n                        color: white;\r\n                        background-color: {state_color};\r\n                        font-size: 14px;\r\n                        font-weight: bold;\r\n                        border-radius: 5px;\r\n                        padding: 5px 10px;\r\n                    }}\r\n                \"\"\")\r\n                pill.show()\r\n            else:\r\n                pill.hide()\r\n\r\n    def set_debug_mode(self, enabled):\r\n        for key, box in self.stat_boxes.items():\r\n            if key not in {\"satisfaction\", \"curiosity\", \"anxiety\"}:\r\n                box.set_editable(enabled)\r\n        self.apply_button.setVisible(enabled)\r\n\r\n    def apply_changes(self):\r\n        if self.squid is not None:\r\n            for key, box in self.stat_boxes.items():\r\n                if hasattr(self.squid, key):\r\n                    setattr(self.squid, key, box.get_value())\r\n        self.update_statistics()\r\n\r\n    def closeEvent(self, event):\r\n        event.accept()"
  },
  {
    "path": "src/tamagotchi_logic.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom PyQt5.QtGui import QPixmap\nimport random\nimport os\nimport time\nimport zipfile\nimport json\nimport math\nfrom .statistics_window import StatisticsWindow\nfrom .save_manager import SaveManager\nfrom .squid import Personality, Squid\nfrom .ui import ResizablePixmapItem\nfrom .brain_tool import SquidBrainWindow\nfrom .learning import HebbianLearning\nfrom .interactions import RockInteractionManager\nfrom .interactions2 import PoopInteractionManager\nfrom .config_manager import ConfigManager\nfrom .plugin_manager import PluginManager\nfrom .brain_neuron_hooks import BrainNeuronHooks\nfrom .brain_neuron_outputs import NeuronOutputMonitor\nfrom .vision_worker import VisionWorker, create_squid_vision_state, extract_scene_objects\n\nfrom .custom_brain_loader import (\n    get_custom_brain_save_data, \n    restore_custom_brain_from_save,\n    show_custom_brain_load_warning,\n    has_custom_brain\n)\n\n# Performance tracking for Task Manager\ntry:\n    from .task_manager import perf_tracker\n    _PERF_TRACKING_AVAILABLE = True\nexcept ImportError:\n    _PERF_TRACKING_AVAILABLE = False\n\nclass TamagotchiLogic:\n\n    def __init__(self, user_interface, squid, brain_window):\n        self.config_manager = ConfigManager()\n        self._propagating_debug_mode = False\n        self.user_interface = user_interface\n        self.mental_states_enabled = True\n        self.squid = squid\n        self.brain_window = brain_window\n        self.brain_hooks = BrainNeuronHooks(self)\n\n        # Initialize Vision Worker\n        self.vision_worker = VisionWorker()\n        self.vision_worker.visibility_update.connect(self.handle_vision_update)\n        self.vision_worker.start()\n        self.latest_vision_result = None\n        \n        # Initialize rock interaction manager with config\n        self.rock_interaction = RockInteractionManager(\n            squid=self.squid,\n            logic=self,\n            scene=self.user_interface.scene,\n            message_callback=self.show_message,\n            config_manager=self.config_manager\n        )\n\n        self.last_save_hash = None\n        self.save_count = 0\n        \n        self.window_resize_cooldown = 0\n        self.window_resize_cooldown_max = 20  # 20 updates before another resize can startle\n        self.has_been_resized = False\n        self.was_big = False\n        self.debug_mode = False\n        self.last_window_size = (1280, 900)  # Default size\n\n        # Age tracking\n        self.squid_birth_time = time.time()\n        self.age_update_timer = QtCore.QTimer()\n        self.age_update_timer.timeout.connect(self.update_squid_age)\n        self.age_update_timer.start(60000)  # 1 minute\n\n        # Decorations message system\n        self.decorations_message_count = 0\n        self.decorations_message_max = 3\n        self.last_decorations_message_time = 0\n        self.decorations_message_cooldown = 120  # 2 minutes in seconds\n        self.decorations_message_check_interval = 300  # 5 minutes in seconds\n        self.next_decorations_message_check = time.time() + random.randint(120, 300)  # 2-5 minutes from now\n\n        \n        self.mental_states_enabled = True \n        self.curious_cooldown = 0\n        self.curious_cooldown_max = 20\n        self.curious_interaction_cooldown = 1\n        self.curious_interaction_cooldown_max = 5\n        self.startle_cooldown = 1000\n        self.startle_cooldown_max = 20 \n        self.plant_calming_effect_counter = 0\n        self.sleep_frame = 0\n\n        # Add action tracking - new in 2.4.5.0\n        self.recent_actions = []\n\n        # Initialize save manager FIRST (before plugins)\n        self.save_manager = SaveManager()\n        \n        # Initialize plugin manager\n        self.plugin_manager = PluginManager()\n        self.plugin_manager.set_tamagotchi_logic(self)\n\n        # Initialize Monitor so handlers can register\n        self.neuron_output_monitor = NeuronOutputMonitor(self)\n\n        # Register all built-in sensors with the plugin manager\n        # This makes them discoverable via plugin_manager.get_all_neuron_handler_info()\n        self.plugin_manager.register_all_sensors(self)\n\n        # Store save data that will be loaded later (needed for achievements)\n        self._pending_save_data = None\n    \n        # Load save data FIRST (but don't apply it yet)\n        save_data = self.save_manager.load_game()\n        if save_data:\n            self._pending_save_data = save_data\n            print(\"✓ Save data loaded and cached for plugin initialization\")\n\n        # --- BLACKLIST MULTIPLAYER PLUGIN FROM AUTO-LOADING ---\n        blacklisted = {\"multiplayer\"}\n        discovered = self.plugin_manager.discover_plugins()\n        for name in list(discovered.keys()):\n            if name.lower() in blacklisted:\n                del discovered[name]\n                print(f\"[PluginManager] Blacklisted '{name}' from auto-loading.\")\n\n        # Manually populate plugins dict to skip blacklisted ones\n        self.plugin_manager.plugins.clear()\n        self.plugin_manager.enabled_plugins.clear()\n\n        for name, data in discovered.items():\n            success = self.plugin_manager.load_plugin(name)\n            if success and name != \"multiplayer\":\n                self.plugin_manager.enabled_plugins.add(name)\n\n        # Setup each loaded plugin (multiplayer is not in self.plugin_manager.plugins)\n        for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n            instance = plugin_data.get('instance')\n            if instance and hasattr(instance, 'setup') and not plugin_data.get('is_setup', False):\n                try:\n                    instance.setup(self.plugin_manager, self)\n                    plugin_data['is_setup'] = True\n                    print(f\"✓ Setup plugin: {plugin_name}\")\n                except Exception as e:\n                    print(f\"Error setting up plugin {plugin_name}: {e}\")\n\n        # Update status bar with plugin information\n        self.update_status_bar()\n                    \n        # Trigger startup hook\n        self.plugin_manager.trigger_hook(\"on_startup\",\n                                        tamagotchi_logic=self,\n                                        squid=self.squid,\n                                        user_interface=self.user_interface)\n\n        # Initialize core attributes first\n        self.simulation_speed = 1  # Default to 1x speed\n        self.base_interval = 1000  # 1000ms = 1 second base interval\n        self.base_food_speed = 90  # pixels per update at 1x speed\n\n        # Initialize game objects BEFORE load_game()\n        self.food_items = []\n        self.max_food = 3\n        self.food_width = 64\n        self.food_height = 64\n        self.poop_items = []\n        self.max_poop = 3\n        self.points = 0\n\n        # Caches scene items to avoid iterating the entire scene every tick\n        self._decoration_cache = []\n        self._decoration_cache_time = 0\n        self._decoration_cache_interval = 0.3  # Rebuild cache every 300ms max\n\n        # Flag to indicate the first instance of the application start\n        self.is_first_instance = True\n\n        # Initialize a timer for the initial delay if it's the first instance\n        if self.is_first_instance:\n            self.initial_delay_timer = QtCore.QTimer()\n            self.initial_delay_timer.setSingleShot(True)\n            self.initial_delay_timer.timeout.connect(self.allow_initial_startle)\n            self.initial_delay_timer.start(60000)  # 60000 ms = 1 minute\n            self.initial_startle_allowed = False\n\n        # Initialize neurogenesis triggers with all required keys\n        self.neurogenesis_triggers = {\n            'novel_objects': 0,\n            'high_stress_cycles': 0,\n            'positive_outcomes': 0\n        }\n        self.new_object_encountered = False\n        self.recent_positive_outcome = False\n\n        # Setup timers with required arguments\n        self.setup_timers()\n\n        # Initialize thought system\n        if hasattr(self.brain_window, 'add_thought'):\n            self.add_thought = self.brain_window.add_thought\n        else:\n            self.thought_log = []\n            self.add_thought = self._log_thought\n\n        # Connect menu actions\n        self.user_interface.feed_action.triggered.connect(self.feed_squid)\n        self.user_interface.clean_action.triggered.connect(self.clean_environment)\n        self.user_interface.connect_view_cone_action(self.squid.toggle_view_cone)\n        self.user_interface.medicine_action.triggered.connect(self.give_medicine)\n        self.user_interface.debug_action.triggered.connect(self.toggle_debug_mode)\n\n        # Window setup\n        self.user_interface.window.resizeEvent = self.handle_window_resize\n\n        # Initialize state tracking\n        self.last_clean_time = 0\n        self.clean_cooldown = 60\n        self.cleanliness_threshold_time = 0\n        self.hunger_threshold_time = 0\n        self.needle_item = None\n        self.lights_on = True\n\n        # Initialize statistics window\n        self.statistics_window = StatisticsWindow(squid)\n        squid.statistics_window = self.statistics_window\n        self.statistics_window.show()\n        \n        # NOW load the game after statistics_window exists AND plugins are initialized\n        self.load_game()\n\n        # Setup additional timers\n        self.score_update_timer = QtCore.QTimer()\n        self.score_update_timer.timeout.connect(self.update_score)\n        self.score_update_timer.start(5000)\n\n        self.brain_update_timer = QtCore.QTimer()\n        self.brain_update_timer.timeout.connect(self.update_squid_brain)\n        self.brain_update_timer.start(1000)\n\n        self.autosave_timer = QtCore.QTimer()\n        self.autosave_timer.timeout.connect(self.autosave)\n\n        # Initialize goal neurons\n        self.squid.satisfaction = 50\n        self.squid.anxiety = 10\n        self.squid.curiosity = 55\n\n        # Connect neurogenesis signal to show icon and store long-term memory\n        if hasattr(self.brain_window, 'brain_widget'):\n            self.brain_window.brain_widget.neuronCreated.connect(self._on_neurogenesis_icon_and_memory)\n\n    def handle_vision_update(self, result):\n        \"\"\"Cache the latest vision result and push to brain immediately.\"\"\"\n        self.latest_vision_result = result\n        # [FIX] Push to brain now. Waiting for the 1-second timer causes lag/flicker.\n        self.apply_input_neurons_to_brain()\n\n    def _on_neurogenesis_icon_and_memory(self, neuron_name: str):\n        \"\"\"Called when a new neuron is created via neurogenesis.\n        Shows images/ng.png above the squid's head for 4 seconds\n        and records a long-term memory of the event.\n        \"\"\"\n        # Show the ng icon above the squid's head for 4 seconds\n        if hasattr(self.squid, 'show_neurogenesis_icon'):\n            self.squid.show_neurogenesis_icon()\n            QtCore.QTimer.singleShot(4000, self.squid.hide_neurogenesis_icon)\n\n        # Store a long-term memory\n        if hasattr(self.squid, 'memory_manager'):\n            self.squid.memory_manager.add_long_term_memory(\n                'neurogenesis',\n                f'neurogenesis_{time.time():.0f}',\n                {\n                    'description': 'GREW A NEW NEURON VIA NEUROGENESIS!',\n                    'neuron_name': neuron_name,\n                    'timestamp': time.time(),\n                    'icon': 'images/ng.png'\n                }\n            )\n\n    def _initialize_plugins(self):\n        \"\"\"Initialize all plugins before loading game data\"\"\"\n        # --- BLACKLIST MULTIPLAYER PLUGIN FROM AUTO-LOADING ---\n        blacklisted = {\"multiplayer\"}  # lowercase plugin key\n        discovered = self.plugin_manager.discover_plugins()\n        \n        for name in list(discovered.keys()):\n            if name.lower() in blacklisted:\n                del discovered[name]\n                print(f\"[PluginManager] Blacklisted '{name}' from auto-loading.\")\n\n        # Clear existing plugins\n        self.plugin_manager.plugins.clear()\n        self.plugin_manager.enabled_plugins.clear()\n\n        # Load all discovered plugins (except multiplayer)\n        for name, data in discovered.items():\n            success = self.plugin_manager.load_plugin(name)\n            if success and name != \"multiplayer\":\n                self.plugin_manager.enabled_plugins.add(name)\n\n        # Setup each loaded plugin\n        for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n            instance = plugin_data.get('instance')\n            if instance and hasattr(instance, 'setup') and not plugin_data.get('is_setup', False):\n                try:\n                    instance.setup(self.plugin_manager, self)\n                    plugin_data['is_setup'] = True\n                    print(f\"✓ Setup plugin: {plugin_name}\")\n                except Exception as e:\n                    print(f\"Error setting up plugin {plugin_name}: {e}\")\n\n        # Update status bar with plugin information\n        self.update_status_bar()\n                    \n        # Trigger startup hook\n        self.plugin_manager.trigger_hook(\"on_startup\", \n                                        tamagotchi_logic=self,\n                                        squid=self.squid,\n                                        user_interface=self.user_interface)\n        \n\n    def update_squid_age(self):\n        if hasattr(self.brain_window, 'statistics_tab'):\n            age_min = int((time.time() - self.squid_birth_time) / 60)\n            self.brain_window.statistics_tab.statistics['squid_age_minutes'] = age_min\n            self.brain_window.statistics_tab.update_display()\n\n\n    def track_poop_thrown(self):\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('poops_thrown')\n\n    def update_highest_anxiety(self, value):\n        if hasattr(self.brain_window, 'statistics_tab'):\n            tab = self.brain_window.statistics_tab\n            if value > tab.statistics['highest_anxiety']:\n                tab.statistics['highest_anxiety'] = value\n                tab.update_display()\n\n    def update_lowest_happiness(self, value):\n        if hasattr(self.brain_window, 'statistics_tab'):\n            tab = self.brain_window.statistics_tab\n            if value < tab.statistics['lowest_happiness']:\n                tab.statistics['lowest_happiness'] = value\n                tab.update_display()\n\n    def update_max_memories(self):\n        if hasattr(self.brain_window, 'statistics_tab') and hasattr(self.squid, 'memory_manager'):\n            tab = self.brain_window.statistics_tab\n            stm = len(self.squid.memory_manager.short_term_memory)\n            ltm = len(self.squid.memory_manager.long_term_memory)\n            tab.statistics['max_short_term_memories'] = max(tab.statistics['max_short_term_memories'], stm)\n            tab.statistics['max_long_term_memories'] = max(tab.statistics['max_long_term_memories'], ltm)\n            tab.update_display()\n\n    \n\n    def set_squid(self, squid):\n        self.squid = squid\n\n    def set_brain_window(self, brain_window):\n        self.brain_window = brain_window\n\n    def set_mental_states_enabled(self, enabled):\n        self.mental_states_enabled = enabled\n        self.squid.mental_state_manager.set_mental_states_enabled(enabled)\n\n    def get_health_history(self, limit=100):\n        \"\"\"\n        Returns historical health data for the squid.\n        \n        Args:\n            limit (int): Maximum number of data points to return (default: 100)\n            \n        Returns:\n            list: A list of (timestamp, health_value) tuples, newest first\n        \"\"\"\n        # Initialize health history if it doesn't exist\n        if not hasattr(self, '_health_history'):\n            self._health_history = []\n        \n        # Return a copy of the history, limited to the requested number of points\n        return self._health_history[-limit:]\n\n    \n\n    def reset_squid_status(self):\n        \"\"\"Reset squid status to default state after temporary actions\"\"\"\n        if self.squid and \"eating\" in self.squid.status.lower():\n            # Choose default status based on personality\n            if self.squid.personality == Personality.TIMID:\n                self.squid.status = \"cautiously exploring\"\n            elif self.squid.personality == Personality.ADVENTUROUS:\n                self.squid.status = \"boldly exploring\"\n            else:\n                self.squid.status = \"roaming\"\n            \n            # Ensure is_eating flag is also cleared\n            self.squid.is_eating = False\n\n\n\n    def get_decision_data(self):\n        \"\"\"Package decision-making information for visualization based on DecisionEngine\"\"\"\n        # Default empty data structure\n        decision_data = {\n            'timestamp': time.strftime(\"%H:%M:%S\"),\n            'inputs': {},\n            'active_memories': [],\n            'possible_actions': [],\n            'final_decision': \"unknown\",\n            'confidence': 0.0,\n            'processing_time': 0,\n            'personality_influence': getattr(self.squid, 'personality', 'unknown'),\n            'weights': {},\n            'adjusted_weights': {},\n            'randomness': {}\n        }\n        \n        if not hasattr(self, 'squid') or not self.squid:\n            return decision_data\n            \n        try:\n            # Get current state for inputs\n            decision_data['inputs'] = {\n                \"hunger\": self.squid.hunger,\n                \"happiness\": self.squid.happiness,\n                \"cleanliness\": self.squid.cleanliness,\n                \"sleepiness\": self.squid.sleepiness,\n                \"satisfaction\": self.squid.satisfaction,\n                \"anxiety\": self.squid.anxiety,\n                \"curiosity\": self.squid.curiosity,\n                \"is_sick\": self.squid.is_sick,\n                \"is_sleeping\": self.squid.is_sleeping,\n                \"has_food_visible\": bool(self.squid.get_visible_food()),\n                \"carrying_rock\": getattr(self.squid, 'carrying_rock', False),\n            }\n            \n            # Capture decision engine logic before making the decision\n            decision_data['final_decision'] = self.squid.status\n            \n            # Get active memories\n            if hasattr(self.squid, 'memory_manager'):\n                active_memories = self.squid.memory_manager.get_active_memories_data(3)\n                decision_data['active_memories'] = [\n                    f\"{mem.get('category', 'memory')}: {str(mem.get('formatted_value', ''))[:50]}\"\n                    for mem in active_memories\n                ]\n            \n            # Simulate processing time (would be cool to measure actual time)\n            decision_data['processing_time'] = random.randint(20, 100)\n            \n            # Confidence of the decision (higher for more extreme weight differences)\n            # Here we're estimating based on the squid's state and randomizing a bit\n            base_confidence = 0.5\n            # Adjust confidence based on state extremes\n            for key, value in decision_data['inputs'].items():\n                if isinstance(value, (int, float)) and value > 80:\n                    base_confidence += 0.1  # More confident with extreme values\n                elif isinstance(value, (int, float)) and value < 20:\n                    base_confidence += 0.1\n            # Cap and add randomness\n            base_confidence = min(0.9, base_confidence)\n            decision_data['confidence'] = base_confidence + random.uniform(-0.1, 0.1)\n            \n            # Get brain network state if available\n            if hasattr(self, 'squid_brain_window') and self.squid_brain_window:\n                brain_state = self.squid_brain_window.brain_widget.state\n            else:\n                brain_state = {}\n            \n            # Calculate decision weights (replicating the logic from DecisionEngine)\n            weights = {\n                \"exploring\": brain_state.get(\"curiosity\", 50) * 0.8 * (1 - (brain_state.get(\"anxiety\", 50) / 100)),\n                \"eating\": brain_state.get(\"hunger\", 50) * 1.2 if self.squid.get_visible_food() else 0,\n                \"approaching_rock\": brain_state.get(\"curiosity\", 50) * 0.7 if not getattr(self.squid, 'carrying_rock', False) else 0,\n                \"throwing_rock\": brain_state.get(\"satisfaction\", 50) * 0.7 if getattr(self.squid, 'carrying_rock', False) else 0,\n                \"avoiding_threat\": brain_state.get(\"anxiety\", 50) * 0.9,\n                \"organizing\": brain_state.get(\"satisfaction\", 50) * 0.5\n            }\n            \n            decision_data['weights'] = weights\n            \n            # Apply personality modifiers (replicating logic from DecisionEngine)\n            adjusted_weights = weights.copy()\n            \n            # Personality modifiers from the DecisionEngine\n            if self.squid.personality.value == \"timid\":\n                adjusted_weights[\"avoiding_threat\"] *= 1.5\n                adjusted_weights[\"approaching_rock\"] *= 0.7\n            elif self.squid.personality.value == \"adventurous\":\n                adjusted_weights[\"exploring\"] *= 1.3\n                adjusted_weights[\"approaching_rock\"] *= 1.2\n            elif self.squid.personality.value == \"greedy\":\n                adjusted_weights[\"eating\"] *= 1.5\n                \n            decision_data['adjusted_weights'] = adjusted_weights\n            \n            # Generate random factors for each action\n            randomness = {}\n            for action in weights.keys():\n                randomness[action] = random.uniform(0.85, 1.15)\n            \n            decision_data['randomness'] = randomness\n            \n            # Possible actions\n            decision_data['possible_actions'] = [\n                action for action, weight in adjusted_weights.items()\n                if weight > 0\n            ]\n        except Exception as e:\n            print(f\"Error generating decision data: {str(e)}\")\n            import traceback\n            traceback.print_exc()\n        \n        return decision_data\n    \n    def give_rl_reward(self, reward):\n        \"\"\"\n        Feed a scalar reward into the DecisionEngine's Q-learning update.\n        Call this immediately after the action finishes.\n        \"\"\"\n        if hasattr(self, 'squid') and hasattr(self.squid, 'decision_engine'):\n            de = self.squid.decision_engine\n            if de.last_state is not None and de.last_action is not None:\n                # Build next state from current squid state\n                next_state = de.get_state_index({\n                    \"hunger\": self.squid.hunger,\n                    \"happiness\": self.squid.happiness,\n                    \"anxiety\": self.squid.anxiety,\n                    \"curiosity\": self.squid.curiosity\n                })\n                # Update Q-table\n                de.ql.update(de.last_state, de.last_action, reward, next_state)\n\n    def get_active_memories(self):\n        # Get raw memory objects instead of display strings\n        memories = self.squid.memory_manager.get_all_short_term_memories(raw=True)[:3]\n        return [f\"{m['category']}: {m['value']}\" for m in memories]\n\n    def get_available_actions(self):\n        return [\"search_for_food\", \"explore\", \"sleep\", \"move_randomly\", \"interact_object\"]\n\n    def get_recent_learning(self):\n        \"\"\"Get last 3 significant weight changes\"\"\"\n        if hasattr(self.squid, 'hebbian_learning') and self.squid.hebbian_learning:\n            try:\n                learning_data = self.squid.hebbian_learning.get_learning_data()\n                return [f\"{n1}-{n2}: {delta:.2f}\" for _,n1,n2,delta in learning_data[-3:]]\n            except AttributeError:\n                return [\"Learning system initializing\"]\n        return [\"No learning data available\"]\n    \n\n    def get_current_state(self):\n        \"\"\"Get current sensory inputs and status\"\"\"\n        return {\n            'hunger': self.squid.hunger,\n            'happiness': self.squid.happiness,\n            'cleanliness': self.squid.cleanliness,\n            'sleepiness': self.squid.sleepiness,\n            'satisfaction': self.squid.satisfaction,\n            'anxiety': self.squid.anxiety,\n            'curiosity': self.squid.curiosity,\n            'is_sick': self.squid.is_sick,\n            'near_food': len(self.squid.get_visible_food()) > 0,\n            'near_poop': len(self.poop_items) > 0\n        }\n\n    def get_active_memories(self):\n        memories = self.squid.memory_manager.get_active_memories_data(3)\n        return [f\"{m['category']}: {m['formatted_value']}\" for m in memories]\n\n    def get_available_actions(self):\n        \"\"\"List of currently available actions\"\"\"\n        actions = [\"explore\", \"sleep\", \"move_randomly\"]\n        if self.squid.hunger > 50:\n            actions.append(\"search_for_food\")\n        if self.squid.curiosity > 60:\n            actions.append(\"investigate_object\")\n        return actions\n\n    def update_from_brain(self, brain_state):   # Communication between brain tool and Squid\n        if self.squid is not None:\n            for key, value in brain_state.items():\n                if hasattr(self.squid, key):\n                    setattr(self.squid, key, value)\n\n            # Handle special cases\n            if brain_state['sleepiness'] >= 100 and not self.squid.is_sleeping:\n                self.squid.go_to_sleep()\n            elif brain_state['sleepiness'] < 50 and self.squid.is_sleeping:\n                self.squid.wake_up()\n\n            if brain_state['direction'] != self.squid.squid_direction:\n                self.squid.squid_direction = brain_state['direction']\n                self.squid.move_squid()\n\n        self.update_statistics()\n        self.user_interface.scene.update()\n\n\n    def update_decoration_learning(self, effects):\n        if not effects:\n            return\n\n        # Get the current state of the squid\n        current_state = {\n            \"hunger\": self.squid.hunger,\n            \"happiness\": self.squid.happiness,\n            \"cleanliness\": self.squid.cleanliness,\n            \"sleepiness\": self.squid.sleepiness,\n            \"satisfaction\": self.squid.satisfaction,\n            \"anxiety\": self.squid.anxiety,\n            \"curiosity\": self.squid.curiosity\n        }\n\n        # Update the brain based on the decoration effects\n        for stat, boost in effects.items():\n            if stat in current_state:\n                # Increase the connection strength between the affected stat and satisfaction\n                self.brain_window.brain_widget.strengthen_connection(stat, 'satisfaction', boost * 0.01)\n                \n                # If the boost is significant, also strengthen connection with happiness\n                if boost > 5:\n                    self.brain_window.brain_widget.strengthen_connection(stat, 'happiness', boost * 0.005)\n\n        # Update the squid's memory\n        decoration_memory = {\n            \"category\": \"decorations\",\n            \"effects\": effects,\n            \"timestamp\": time.time()\n        }\n        self.squid.memory_manager.add_short_term_memory('decorations', str(time.time()), decoration_memory)\n\n        # If this is a significant effect, consider transferring to long-term memory\n        if any(boost > 10 for boost in effects.values()):\n            self.squid.memory_manager.transfer_to_long_term_memory('decorations', str(time.time()))\n\n\n    def _log_thought(self, thought):\n        self.thought_log.append(thought)\n        print(f\"Squid thought: {thought}\")\n\n    def check_for_decoration_attraction(self):\n        squid_x = self.squid.squid_x\n        squid_y = self.squid.squid_y\n        \n        active_decorations = self.get_nearby_decorations(squid_x, squid_y)\n        \n        if active_decorations:\n            self.apply_decoration_effects(active_decorations)\n            \n            # NEW: Check for plant contact and update status\n            is_hiding = False\n            for decoration in active_decorations:\n                if hasattr(decoration, 'category') and decoration.category == 'plant':\n                    if decoration.collidesWithItem(self.squid.squid_item):\n                        self.squid.status = \"hiding behind plant\"\n                        is_hiding = True\n                        break  # Squid can only hide behind one plant at a time\n            \n            if not is_hiding and self.squid.status == \"hiding behind plant\":\n                self.squid.status = \"roaming\" # Or another default status\n                \n            # Move decorations\n            for decoration in active_decorations:\n                decoration_pos = decoration.pos()\n                if decoration_pos.x() < squid_x:\n                    self.move_decoration(decoration, 5)  # Move right\n                else:\n                    self.move_decoration(decoration, -5)  # Move left\n\n    def get_nearby_decorations(self, x, y, radius=100):\n        \"\"\"\n        Get decorations near a position.\n        \n        PERFORMANCE FIX: Uses cached decoration list to avoid\n        iterating through all scene items every frame.\n        \"\"\"\n        current_time = time.time()\n        \n        # Rebuild cache if stale (every 300ms or on first call)\n        if (current_time - self._decoration_cache_time > self._decoration_cache_interval\n            or not self._decoration_cache):\n            self._decoration_cache = []\n            for item in self.user_interface.scene.items():\n                if isinstance(item, ResizablePixmapItem):\n                    # Cache both the item and its center point\n                    center = item.sceneBoundingRect().center()\n                    self._decoration_cache.append((item, center.x(), center.y()))\n            self._decoration_cache_time = current_time\n        \n        # Filter from cache using squared distance (avoids expensive sqrt)\n        nearby_decorations = []\n        radius_sq = radius * radius\n        for item, cx, cy in self._decoration_cache:\n            dist_sq = (cx - x) ** 2 + (cy - y) ** 2\n            if dist_sq <= radius_sq:\n                nearby_decorations.append(item)\n        \n        return nearby_decorations\n    \n    def invalidate_decoration_cache(self):\n        \"\"\"\n        Call this when decorations are added/removed to force cache rebuild.\n        \"\"\"\n        self._decoration_cache_time = 0\n    \n    def investigate_object(self):\n        if self.detected_object_position:\n            self.move_towards(self.detected_object_position[0], self.detected_object_position[1])\n            \n            # Add thoughts\n            self.brain_window.add_thought(\"Investigating unknown object\")\n            \n            if self.distance_to(self.detected_object_position[0], self.detected_object_position[1]) < 20:\n                object_item = self.tamagotchi_logic.get_item_at_position(self.detected_object_position[0], self.detected_object_position[1])\n                if object_item:\n                    if isinstance(object_item, Food):\n                        self.brain_window.add_thought(\"Unknown object appears to be food\")\n                        self.eat() \n                        self.brain_window.add_thought(\"Ate the food!\")\n                    elif isinstance(object_item, Decoration):\n                        self.brain_window.add_thought(\"Unknown object appears to be a decoration\")\n                        self.interact_with_decoration(object_item)\n\n                self.object_visible = False\n                self.detected_object_position = None\n    \n    def check_collision_with_cheese(self, cheese_item):\n        if self.squid.personality == Personality.STUBBORN:\n            return False  # Stubborn squids never collide with cheese\n        \n        squid_rect = self.squid.boundingRect().translated(self.squid.squid_x, self.squid.squid_y)\n        cheese_rect = cheese_item.boundingRect().translated(cheese_item.pos())\n        \n        return squid_rect.intersects(cheese_rect)\n    \n    def move_decoration(self, decoration, dx):\n        current_pos = decoration.pos()\n        new_x = current_pos.x() + dx\n        \n        # Ensure the decoration stays within the scene boundaries\n        scene_rect = self.user_interface.scene.sceneRect()\n        new_x = max(scene_rect.left(), min(new_x, self.user_interface.window_width - decoration.boundingRect().width()))\n        \n        # Use QVariantAnimation because QGraphicsPixmapItem does not inherit from QObject.\n        # This animation will interpolate the position value for us.\n        animation = QtCore.QVariantAnimation()\n        animation.setStartValue(current_pos)\n        animation.setEndValue(QtCore.QPointF(new_x, current_pos.y()))\n        animation.setDuration(300)  # 300 ms duration\n        animation.setEasingCurve(QtCore.QEasingCurve.OutCubic)\n        \n        # Connect the animation's valueChanged signal to the item's setPos method\n        animation.valueChanged.connect(decoration.setPos)\n        \n        # Store the animation object to prevent it from being garbage collected\n        decoration._animation = animation\n        \n        # Start the animation and have it delete itself when finished\n        animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)\n\n    def apply_decoration_effects(self, active_decorations):\n        \"\"\"Apply effects from nearby decorations\"\"\"\n        if not active_decorations:\n            return\n\n        effects = {}\n        strongest_effect = 0\n        strongest_decoration = None\n\n        for decoration in active_decorations:\n            if not hasattr(decoration, 'filename') or decoration.filename is None:\n                continue\n            if not hasattr(decoration, 'stat_modifiers') or not decoration.stat_modifiers:\n                continue\n\n            # Find the decoration with the strongest effect (absolute value)\n            max_modifier = max(abs(val) for val in decoration.stat_modifiers.values())\n            if max_modifier > strongest_effect:\n                strongest_effect = max_modifier\n                strongest_decoration = decoration\n\n        if strongest_decoration:\n            # Apply stat modifiers additively\n            for stat, modifier in strongest_decoration.stat_modifiers.items():\n                if hasattr(self.squid, stat):\n                    current_value = getattr(self.squid, stat)\n                    new_value = current_value + modifier\n                    # Clamp the value between 0 and 100\n                    new_value = max(0, min(100, new_value))\n                    setattr(self.squid, stat, new_value)\n                    effects[stat] = modifier\n\n            # Apply category-specific effects\n            if strongest_decoration.category == 'plant':\n                old_cleanliness = self.squid.cleanliness\n                self.squid.cleanliness = min(self.squid.cleanliness + 5, 100)\n                effects['cleanliness'] = effects.get('cleanliness', 0) + (self.squid.cleanliness - old_cleanliness)\n            elif strongest_decoration.category == 'rock':\n                old_satisfaction = self.squid.satisfaction\n                self.squid.satisfaction = min(self.squid.satisfaction + 5, 100)\n                effects['satisfaction'] = effects.get('satisfaction', 0) + (self.squid.satisfaction - old_satisfaction)\n\n        if strongest_decoration and hasattr(strongest_decoration, 'filename') and strongest_decoration.filename is not None:\n            self.squid.memory_manager.add_short_term_memory('decorations', strongest_decoration.filename, effects)\n        else:\n            if effects:\n                self.squid.memory_manager.add_short_term_memory('decorations', 'nearby_decorations', effects)\n\n        if effects:\n            self.update_decoration_learning(effects)\n\n    def check_decoration_startle(self, active_decorations):\n        if not self.mental_states_enabled:\n            return\n\n        if self.startle_cooldown > 0:\n            return\n\n        # Very low chance of being startled by decorations\n        decoration_startle_chance = 0.001 * len(active_decorations)  # 0.1% chance per decoration\n\n        # Increase chance if anxiety is high\n        if self.squid.anxiety > 70:\n            decoration_startle_chance *= (self.squid.anxiety / 50)\n\n        if random.random() < decoration_startle_chance:\n            self.startle_squid()\n            self.show_message(\"Squid was startled by a decoration!\")\n            self.brain_window.add_thought(\"Startled by a decoration!\")\n\n    def show_decoration_message(self, decoration):\n        category = decoration.category\n        messages = {\n            'plant': [\n                \"Squid seems fascinated by the plants!\",\n                \"Your squid is enjoying the greenery.\",\n                \"The plant decoration is making your squid happy.\"\n            ],\n            'rock': [\n                \"Your squid is exploring the rocky terrain.\",\n                \"Squid seems intrigued by the rock formation.\",\n                \"The rock decoration provides a nice hiding spot for your squid.\"\n            ]\n        }\n\n        if category in messages:\n            message = random.choice(messages[category])\n        else:\n            message = \"Squid is interacting with the decoration.\"\n\n        self.user_interface.show_message(message)\n\n    def setup_speed_menu(self):\n        speed_menu = self.user_interface.menu_bar.addMenu('Speed')\n\n        speed_actions = {\n            \"Pause\": 0,\n            \"1x\": 1,\n            \"2x\": 2,\n            \"4x\": 4\n        }\n\n        for label, speed in speed_actions.items():\n            action = QtWidgets.QAction(label, self.user_interface.window)\n            action.triggered.connect(lambda checked, s=speed: self.set_simulation_speed(s))\n            speed_menu.addAction(action)\n\n    \n\n    def set_simulation_speed(self, speed):\n        \"\"\"Set the simulation speed and notify plugins of the change\"\"\"\n        # Store current speed before changing (default to 1 if not set)\n        previous_speed = getattr(self, 'simulation_speed', 1)\n        \n        # Apply new speed\n        self.simulation_speed = speed\n        self.update_timers()\n        \n        # Clear any pause messages if unpausing\n        if previous_speed == 0 and speed > 0:\n            # Remove any existing message items\n            if hasattr(self, 'user_interface') and hasattr(self.user_interface, 'scene'):\n                for item in self.user_interface.scene.items():\n                    if isinstance(item, QtWidgets.QGraphicsTextItem):\n                        self.user_interface.scene.removeItem(item)\n        \n        # Update dependent systems\n        if hasattr(self, 'squid'):\n            self.squid.set_animation_speed(speed)\n        \n        if hasattr(self, 'brain_window'):\n            self.brain_window.set_pause_state(speed == 0)\n\n        # Safety check before plugin hook\n        if not hasattr(self, 'plugin_manager'):\n            self.plugin_manager = PluginManager()  # Ensure plugin manager exists\n            \n        # Call plugin hook with both speeds\n        self.plugin_manager.trigger_hook(\n            \"on_speed_change\",\n            tamagotchi_logic=self,\n            old_speed=previous_speed,  # Now properly defined\n            new_speed=speed\n        )\n        \n        print(f\"\\033[38;5;208;1m >> Simulation speed: {speed}x\\033[0m\")\n\n\n    def setup_timers(self, scene=None, message_callback=None):\n        \"\"\"Setup all timers including rock interaction timers\"\"\"\n        # Initialize core timers\n        self.simulation_timer = QtCore.QTimer()\n        self.simulation_timer.timeout.connect(self.update_simulation)\n        \n        # Set default simulation speed\n        if not hasattr(self, 'simulation_speed'):\n            self.simulation_speed = 1\n        \n        # Configure initial timer intervals\n        self.update_timers()\n        \n        # Score update timer\n        self.score_update_timer = QtCore.QTimer()\n        self.score_update_timer.timeout.connect(self.update_score)\n        self.score_update_timer.start(5000)  # 5 seconds\n        \n        # Brain update timer\n        self.brain_update_timer = QtCore.QTimer()\n        self.brain_update_timer.timeout.connect(self.update_squid_brain)\n        self.brain_update_timer.start(1000)  # 1 second\n        \n        # Autosave timer\n        self.autosave_timer = QtCore.QTimer()\n        self.autosave_timer.timeout.connect(self.autosave)\n        \n        # Configure rock interaction timers\n        # ===== PERFORMANCE FIX: Increased interval from 100ms to 200ms =====\n        if hasattr(self, 'rock_interaction'):\n            self.rock_interaction.setup_timers(interval=200)\n            self.rock_interaction.rock_test_timer.timeout.connect(\n                self.rock_interaction.update_rock_test\n            )\n            self.rock_interaction.throw_animation_timer.timeout.connect(\n                self.rock_interaction.update_throw_animation\n            )\n\n        # Set up poop interaction\n        # ===== PERFORMANCE FIX: Increased interval from 100ms to 200ms =====\n        if hasattr(self, 'poop_interaction'):\n            self.poop_interaction.setup_timers(interval=200)\n        \n        # Start the simulation timer\n        self.simulation_timer.start()\n\n    def update_timers(self):\n        \"\"\"Update timer intervals based on current simulation speed\"\"\"\n        if not hasattr(self, 'base_interval'):\n            self.base_interval = 1000  # Ensure base_interval exists\n            \n        if not hasattr(self, 'simulation_speed'):\n            self.simulation_speed = 1\n            \n        if self.simulation_speed == 0:\n            self.simulation_timer.stop()\n            # Also stop brain-related timers when paused\n            if hasattr(self, 'brain_window') and self.brain_window:\n                if hasattr(self.brain_window, 'hebbian_timer'):\n                    self.brain_window.hebbian_timer.stop()\n                if hasattr(self.brain_window, 'countdown_timer'):\n                    self.brain_window.countdown_timer.stop()\n        else:\n            interval = max(10, self.base_interval // self.simulation_speed)  # Ensure minimum interval\n            self.simulation_timer.start(interval)\n            \n            # Update hebbian learning timer if it exists\n            if hasattr(self, 'brain_window') and self.brain_window:\n                if hasattr(self.brain_window, 'hebbian_timer'):\n                    # Get the base learning interval from config\n                    base_learning_interval = self.brain_window.config.hebbian.get('learning_interval', 30000)\n                    # Scale the interval inversely with simulation speed\n                    new_learning_interval = max(1000, int(base_learning_interval / self.simulation_speed))\n                    self.brain_window.hebbian_timer.setInterval(new_learning_interval)\n                    \n                    # Update the countdown timer's initial value\n                    if hasattr(self.brain_window, 'hebbian_countdown_seconds'):\n                        self.brain_window.hebbian_countdown_seconds = int(new_learning_interval / 1000)\n                    \n                    # Restart countdown timer if it was stopped\n                    if hasattr(self.brain_window, 'countdown_timer'):\n                        if not self.brain_window.countdown_timer.isActive():\n                            self.brain_window.countdown_timer.start(1000)\n                \n                # Update neurogenesis cooldown if the system exists\n                if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'enhanced_neurogenesis'):\n                    # The neurogenesis system uses time-based cooldowns, so we need to adjust how time passes\n                    # This is handled by the base cooldown time in the config\n                    neuro_system = self.brain_window.brain_widget.enhanced_neurogenesis\n                    if hasattr(neuro_system, 'config'):\n                        # Store the base cooldown if not already stored\n                        if not hasattr(neuro_system, 'base_cooldown'):\n                            neuro_system.base_cooldown = neuro_system.config.neurogenesis.get('cooldown', 120)\n                        # Scale cooldown inversely with simulation speed\n                        neuro_system.config.neurogenesis['cooldown'] = max(10, int(neuro_system.base_cooldown / self.simulation_speed))\n\n\n    def stop(self):\n        \"\"\"Stop all timers and clean up resources\"\"\"\n        # Stop Vision Worker\n        if hasattr(self, 'vision_worker') and self.vision_worker:\n            self.vision_worker.stop()\n            self.vision_worker.wait()\n\n        # Stop main timers\n        if hasattr(self, 'simulation_timer') and self.simulation_timer:\n            self.simulation_timer.stop()\n        \n        if hasattr(self, 'score_update_timer') and self.score_update_timer:\n            self.score_update_timer.stop()\n        \n        if hasattr(self, 'brain_update_timer') and self.brain_update_timer:\n            self.brain_update_timer.stop()\n        \n        if hasattr(self, 'autosave_timer') and self.autosave_timer:\n            self.autosave_timer.stop()\n        \n        if hasattr(self, 'age_update_timer') and self.age_update_timer:\n            self.age_update_timer.stop()\n        \n        if hasattr(self, 'initial_delay_timer') and self.initial_delay_timer:\n            self.initial_delay_timer.stop()\n        \n        # Stop rock interaction timers\n        if hasattr(self, 'rock_interaction') and self.rock_interaction:\n            if hasattr(self.rock_interaction, 'rock_test_timer') and self.rock_interaction.rock_test_timer:\n                self.rock_interaction.rock_test_timer.stop()\n            if hasattr(self.rock_interaction, 'throw_animation_timer') and self.rock_interaction.throw_animation_timer:\n                self.rock_interaction.throw_animation_timer.stop()\n        \n        # Stop poop interaction timers\n        if hasattr(self, 'poop_interaction') and self.poop_interaction:\n            if hasattr(self.poop_interaction, 'poop_timer') and self.poop_interaction.poop_timer:\n                self.poop_interaction.poop_timer.stop()\n        \n        # Stop brain window timers\n        if hasattr(self, 'brain_window') and self.brain_window:\n            if hasattr(self.brain_window, 'hebbian_timer') and self.brain_window.hebbian_timer:\n                self.brain_window.hebbian_timer.stop()\n            if hasattr(self.brain_window, 'countdown_timer') and self.brain_window.countdown_timer:\n                self.brain_window.countdown_timer.stop()\n\n    def check_for_startle(self):\n        if not self.mental_states_enabled:\n            return\n\n        # Only check for new startle if not currently startled and initial startle allowed\n        if self.squid.is_fleeing or not self.initial_startle_allowed:\n            return\n\n        if self.startle_cooldown > 0:\n            self.startle_cooldown -= 1\n            return\n\n        # Base chance of being startled\n        startle_chance = 0.002  # low base chance\n\n        # Increase chance if anxiety is high\n        if self.squid.anxiety > 70:\n            startle_chance *= (self.squid.anxiety / 50)  # Up to 2x more likely when anxiety is >70\n\n        # Check for startle\n        if random.random() < startle_chance:\n            self.startle_squid()\n\n    def startle_squid(self, source=\"unknown\"):\n        if not self.mental_states_enabled:\n            return\n\n        if not getattr(self, 'initial_startle_allowed', False):\n            return\n\n        try:\n            # --- existing speed init -------------------------------------------------\n            if not hasattr(self.squid, 'base_speed'):\n                self.squid.base_speed = 90\n            if not hasattr(self.squid, 'current_speed'):\n                self.squid.current_speed = self.squid.base_speed\n\n            # --- existing startled state / status block ------------------------------\n            self.squid.mental_state_manager.set_state(\"startled\", True)\n            self.startle_cooldown = self.startle_cooldown_max\n            previous_status = getattr(self.squid, 'status', \"roaming\")\n\n            if source == \"first_resize\":\n                self.squid.status = \"startled by environment change\"\n            elif source == \"incoming_rock\":\n                self.squid.status = \"startled by rock\"\n            elif source == \"detected_squid\":\n                self.squid.status = \"startled by other squid\"\n            elif source == \"targeted_by_rock\":\n                self.squid.status = \"fleeing\"\n            elif source in [\"environment\", \"decoration\"]:\n                self.squid.status = \"startled\"\n            else:\n                if self.squid.personality == Personality.TIMID:\n                    self.squid.status = \"hiding\"\n                else:\n                    self.squid.status = \"startled\"\n\n            self.squid.is_fleeing = True\n            self.statistics_window.award(-50)\n            self.squid.current_speed = 180\n            self.squid.direction = random.choice(['up', 'down', 'left', 'right'])\n\n            # ------------------------------------------------------------------\n            # NEW: 15 % ink-cloud trigger (skip if woken from sleep)\n            # ------------------------------------------------------------------\n            if source != \"startled_awake\" and random.random() < 0.25:\n                self.create_ink_cloud()\n            \n            # Track startle in statistics (was only in else block)\n            if hasattr(self.brain_window, 'statistics_tab'):\n                self.brain_window.statistics_tab.increment_stat('startles_experienced')\n                \n                self.squid.status = \"fleeing!\"\n                self.squid.memory_manager.add_short_term_memory(\n                    'behaviour', 'ink_cloud', 'Startled! Created an ink cloud'\n                )\n                QtCore.QTimer.singleShot(5000, self.squid.end_ink_flee)\n\n            if hasattr(self.brain_window, 'statistics_tab'):\n                self.brain_window.statistics_tab.increment_stat('startles_experienced')\n\n            # --- resilience / anxiety / ink logic --------------------------\n            stress_neuron_count = 0\n            if self.brain_window and hasattr(self.brain_window, 'brain_widget'):\n                stress_neuron_count = self.brain_window.brain_widget.get_stress_neuron_count()\n            resilience_factor = math.log(stress_neuron_count + 1)\n\n            if source == \"first_resize\":\n                base_anxiety_increase = 5\n                message = \"The squid noticed its environment changing!\"\n                base_ink_chance = 0.6\n            else:\n                base_anxiety_increase = 15\n                message = \"The squid was startled!\"\n                base_ink_chance = 0.6\n\n            anxiety_increase = max(0, base_anxiety_increase - (resilience_factor * 5))\n            self.squid.anxiety = min(100, self.squid.anxiety + anxiety_increase)\n\n            if stress_neuron_count > 0:\n                self.brain_window.add_thought(\n                    f\"Startled, but felt {resilience_factor:.1f}x more resilient. \"\n                    f\"Anxiety increased by only {anxiety_increase:.1f}.\"\n                )\n\n            # --- existing first-startle / ink / memory / timer ----------------------\n            is_first_startle = not hasattr(self, '_has_startled_before')\n            if is_first_startle:\n                self._has_startled_before = True\n\n            ink_chance = 0.9 if self.squid.anxiety > 60 else base_ink_chance\n            produce_ink = is_first_startle or random.random() < ink_chance\n\n            memory_value = (\n                f\"Startled! Status changed from {previous_status} to {self.squid.status}, \"\n                f\"Speed {self.squid.current_speed}px, Direction {self.squid.direction}\"\n            )\n            self.squid.memory_manager.add_short_term_memory(\n                'behavior', 'startle_response', memory_value\n            )\n\n            # Fear memory when anxiety is very high\n            if self.squid.anxiety >= 80:\n                self.squid.memory_manager.add_short_term_memory(\n                    'emotion', 'fear',\n                    f'Gripped by fear! Anxiety at {int(self.squid.anxiety)}.',\n                    importance=6\n                )\n\n            self.show_message(message)\n            if produce_ink:\n                self.create_ink_cloud()\n\n            QtCore.QTimer.singleShot(self.startle_cooldown_max * 100,\n                                    lambda: self.end_fleeing(previous_status))\n\n        except Exception as e:\n            print(f\"Error during startle: {str(e)}\")\n            self.show_message(\"The squid panicked!\")\n\n\n    def end_fleeing(self, previous_status=\"roaming\"):\n        \"\"\"Reset speed and status after fleeing ends\"\"\"\n        if hasattr(self, 'squid') and self.squid:\n            self.squid.is_fleeing = False\n            self.squid.current_speed = self.squid.base_speed\n            \n            # Set more descriptive status based on anxiety level\n            if self.squid.anxiety > 80:\n                self.squid.status = \"extremely anxious\"\n            elif self.squid.anxiety > 60:\n                self.squid.status = \"anxious\" \n            elif self.squid.anxiety > 40:\n                self.squid.status = \"nervous\"\n            elif self.squid.anxiety > 20:\n                self.squid.status = \"recovering from startle\"\n            else:\n                # Use a more specific status based on personality\n                if self.squid.personality == Personality.TIMID:\n                    self.squid.status = \"cautiously exploring\"\n                elif self.squid.personality == Personality.ADVENTUROUS:\n                    self.squid.status = \"boldly exploring\"\n                else:\n                    self.squid.status = previous_status\n                \n            self.squid.mental_state_manager.set_state(\"startled\", False)  # Explicitly clear startled state\n            \n            if self.debug_mode:\n                print(f\"Fleeing ended - status returned to {self.squid.status}\")\n            \n            self.squid.memory_manager.add_short_term_memory(\n                'behavior',\n                'calm_after_startle',\n                f\"Returned to {self.squid.status} status after fleeing\"\n            )\n\n\n    def create_ink_cloud(self):\n        \"\"\"Create an ink cloud with guaranteed fade-out after 10 seconds\"\"\"\n        ink_cloud_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"inkcloud.png\"))\n        ink_cloud_item = QtWidgets.QGraphicsPixmapItem(ink_cloud_pixmap)\n        \n        # Set the center of the ink cloud to match the center of the squid\n        squid = self.squid\n        squid_center_x = squid.squid_x + squid.squid_width // 2\n        squid_center_y = squid.squid_y + squid.squid_height // 2\n        ink_cloud_item.setPos(\n            squid_center_x - ink_cloud_pixmap.width() // 2, \n            squid_center_y - ink_cloud_pixmap.height() // 2\n        )\n        \n        squid.ui.scene.addItem(ink_cloud_item)\n        \n        # Create a QGraphicsOpacityEffect without a parent\n        opacity_effect = QtWidgets.QGraphicsOpacityEffect()\n        opacity_effect.setOpacity(1.0)  # Start fully visible\n        ink_cloud_item.setGraphicsEffect(opacity_effect)\n\n        # Create a QPropertyAnimation for the opacity effect\n        fade_out_animation = QtCore.QPropertyAnimation(opacity_effect, b\"opacity\")\n        fade_out_animation.setDuration(10000)  # 10 seconds duration\n        fade_out_animation.setStartValue(1.0)\n        fade_out_animation.setEndValue(0.0)\n        fade_out_animation.setEasingCurve(QtCore.QEasingCurve.InQuad)\n\n        # Connect the finished signal to remove the item\n        fade_out_animation.finished.connect(lambda: self.remove_ink_cloud(ink_cloud_item))\n\n        # Start the animation and deduct points\n        fade_out_animation.start()\n        self.statistics_window.award(-250)\n\n        # Update ink clouds counter in squid.statistics (for save/load persistence)\n        if hasattr(self, 'squid') and hasattr(self.squid, 'statistics'):\n            self.squid.statistics.ink_clouds_created = getattr(\n                self.squid.statistics, 'ink_clouds_created', 0\n            ) + 1\n        \n        # Update ink clouds in brain statistics tab (for UI display)\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('ink_clouds_created')\n        \n        \n        # Backup timer to force remove after 10 seconds in case animation fails\n        QtCore.QTimer.singleShot(10000, lambda: self.force_remove_ink_cloud(ink_cloud_item))\n\n    def force_remove_ink_cloud(self, ink_cloud_item):\n        \"\"\"Force remove the ink cloud if it still exists after timeout\"\"\"\n        if ink_cloud_item in self.squid.ui.scene.items():\n            self.squid.ui.scene.removeItem(ink_cloud_item)\n\n    def remove_ink_cloud(self, ink_cloud_item):\n        \"\"\"Remove the ink cloud from the scene\"\"\"\n        if ink_cloud_item in self.ui.scene.items():\n            self.ui.scene.removeItem(ink_cloud_item)\n        if ink_cloud_item.graphicsEffect():\n            ink_cloud_item.graphicsEffect().deleteLater()\n        ink_cloud_item.setGraphicsEffect(None)\n\n    def remove_ink_cloud_safety(self):\n        \"\"\"Safety method to ensure ink cloud is removed\"\"\"\n        if hasattr(self, '_current_ink_cloud') and self._current_ink_cloud:\n            if self._current_ink_cloud in self.user_interface.scene.items():\n                self.user_interface.scene.removeItem(self._current_ink_cloud)\n            self._current_ink_cloud = None\n            \n        if hasattr(self, '_ink_cloud_timer') and self._ink_cloud_timer:\n            self._ink_cloud_timer.stop()\n\n    def end_startle(self):\n        if self.mental_states_enabled:\n            self.squid.mental_state_manager.set_state(\"startled\", False)\n            # Add thoughts\n            self.brain_window.add_thought(\"No longer startled\")\n\n    def track_food_consumed(self, food_item):\n        \"\"\"Track when food is consumed\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            if getattr(food_item, 'is_sushi', False):\n                self.brain_window.statistics_tab.increment_stat('sushi_eaten')\n            else:\n                self.brain_window.statistics_tab.increment_stat('cheese_eaten')\n\n    def track_poop_created(self):\n        \"\"\"Track when poop is created\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('poops_created')\n            # Update max poops if needed\n            current_poops = len(self.poop_items)\n            if current_poops > self.brain_window.statistics_tab.statistics['max_poops_cleaned']:\n                self.brain_window.statistics_tab.statistics['max_poops_cleaned'] = current_poops\n                self.brain_window.statistics_tab.update_display()\n\n    def track_rock_thrown(self):\n        \"\"\"Track when rock is thrown\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('rocks_thrown')\n\n    def track_plant_interaction(self):\n        \"\"\"Track when squid interacts with plants\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('plants_interacted')\n\n    def track_startle(self):\n        \"\"\"Track when squid is startled\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.increment_stat('startles_experienced')\n\n\n            ######################################### UPDATE_SIMULATION METHOD BELOW\n\n\n    def update_simulation(self):\n        \"\"\"\n        Main simulation update loop.\n        \"\"\"\n        if _PERF_TRACKING_AVAILABLE:\n            _sim_start = time.perf_counter()\n            perf_tracker.increment(\"simulation_ticks\")\n        \n        # 1. Pre-update hook\n        self.plugin_manager.trigger_hook(\"pre_update\", tamagotchi_logic=self, squid=self.squid)\n        \n        # 2. Paused?\n        if self.simulation_speed == 0:\n            return\n        \n        # Check for decorations message\n        self._check_decorations_message()\n        \n        # 3. World updates\n        self.move_objects()\n        self.animate_poops()\n        self.update_statistics()\n        self.check_poop_interaction()\n        \n        # Vision Worker\n        if self.squid and hasattr(self, 'vision_worker') and self.vision_worker.isRunning():\n            vision_state = create_squid_vision_state(self.squid)\n            self.vision_worker.update_squid_state(vision_state)\n            decorations = self._get_cached_decorations()\n            scene_objects = extract_scene_objects(self.user_interface.scene, self.food_items, decorations)\n            self.vision_worker.update_scene_objects(scene_objects)\n\n        if self.squid:\n            # === FORCE SLEEP WHEN SLEEPINESS == 100 ===\n            if self.squid.sleepiness >= 100.0:\n                if not getattr(self.squid, 'is_sleeping', False):\n                    self.squid.is_sleeping = True\n                    self.squid.status = \"sleeping\"\n                    if hasattr(self.squid, 'current_action'):\n                        self.squid.current_action = \"sleep\"\n                    self.user_interface.show_message(\"💤 The squid is completely exhausted and fell asleep!\")\n                    print(\"💤 FORCED SLEEP: sleepiness hit 100%\")\n                    self.sleep_frame = 0\n\n            # === SLEEP ANIMATION (swaps between sleep1.png and sleep2.png) ===\n            if getattr(self.squid, 'is_sleeping', False):\n                self.sleep_frame = (self.sleep_frame + 1) % 2\n                \n                try:\n                    if self.sleep_frame == 0:\n                        pix = QPixmap(\"images/sleep1.png\")\n                    else:\n                        pix = QPixmap(\"images/sleep2.png\")\n                    \n                    if not pix.isNull() and hasattr(self.squid, 'squid_item'):\n                        self.squid.squid_item.setPixmap(pix)\n                except Exception as e:\n                    print(f\"Sleep animation error: {e}\")\n\n                # CONFIGURABLE RECOVERY (from original)\n                recovery_rate = 28.0 * (1 / 60.0)   # ~28 points per second at 60 FPS\n                self.squid.sleepiness = max(0.0, self.squid.sleepiness - recovery_rate)\n\n                # Nice side bonuses while sleeping (from original)\n                self.squid.happiness = min(100.0, self.squid.happiness + 0.45 * (1 / 60.0))\n                self.squid.satisfaction = min(100.0, self.squid.satisfaction + 0.30 * (1 / 60.0))\n\n                # Auto-wake when sufficiently rested (from original)\n                if self.squid.sleepiness <= 25.0:\n                    self.squid.is_sleeping = False\n                    self.squid.status = \"roaming\"\n                    self.user_interface.show_message(\"😴 The squid woke up feeling refreshed!\")\n                    self.sleep_frame = 0\n\n            # ── STUBBORN PERSONALITY: RESIST SLEEP WHEN OTHER NEEDS ARE STRONGER ── (from original)\n            elif self.squid.personality == Personality.STUBBORN:\n                hunger_urge = self.squid.hunger / 100.0\n                curiosity_urge = self.squid.curiosity / 100.0\n                anxiety_urge = self.squid.anxiety / 100.0\n\n                # If any competing need is > 55% and stronger than current sleepiness,\n                # stubborn squid will refuse to fall asleep (even if sleepiness is high)\n                if max(hunger_urge, curiosity_urge * 0.8, anxiety_urge * 0.6) > 0.55:\n                    if self.squid.sleepiness > 65 and random.random() < 0.22:  # 22% chance per frame to resist\n                        self.user_interface.show_message(\"😤 Stubborn squid refuses to sleep right now!\")\n                        # Tiny happiness penalty for fighting sleep\n                        self.squid.happiness = max(15.0, self.squid.happiness - 1.8)\n\n            # Normal behavior only when NOT sleeping\n            if not getattr(self.squid, 'is_sleeping', False):\n                self.squid.move_squid()\n                self.check_for_decoration_attraction()\n\n                if self.mental_states_enabled:\n                    self.check_for_startle()\n                    self.check_for_curiosity()\n\n            # Rest of the original code (neurogenesis, brain state, etc.)\n            self.track_neurogenesis_triggers()\n            \n            if self.squid.is_sleeping:\n                self.squid.memory_manager.review_and_transfer_memories()\n            else:\n                self.squid.memory_manager.periodic_memory_management()\n            \n            # brain_state building\n            is_startled_state = False\n            if hasattr(self.squid, 'mental_state_manager') and self.squid.mental_state_manager:\n                is_startled_state = self.squid.mental_state_manager.is_state_active('startled')\n            elif hasattr(self.squid, 'status'):\n                is_startled_state = \"startled\" in str(self.squid.status).lower()\n            \n            brain_state = {\n                \"hunger\": self.squid.hunger,\n                \"happiness\": self.squid.happiness,\n                \"cleanliness\": self.squid.cleanliness,\n                \"sleepiness\": self.squid.sleepiness,\n                \"satisfaction\": self.squid.satisfaction,\n                \"anxiety\": self.squid.anxiety,\n                \"curiosity\": self.squid.curiosity,\n                \"is_sick\": self.squid.is_sick,\n                \"is_sleeping\": self.squid.is_sleeping,\n                \"is_eating\": getattr(self.squid, 'is_eating', False) or (self.squid.status == \"eating\"),\n                \"pursuing_food\": self.squid.pursuing_food,\n                \"is_fleeing\": getattr(self.squid, 'is_fleeing', False),\n                \"is_startled\": is_startled_state,\n                \"direction\": self.squid.squid_direction,\n                \"position\": (self.squid.squid_x, self.squid.squid_y),\n                \"personality\": self.squid.personality.value if isinstance(self.squid.personality, Personality) else str(self.squid.personality),\n                \"status\": self.squid.status,\n                \"novelty_exposure\": self.neurogenesis_triggers['novel_objects'],\n                \"sustained_stress\": self.neurogenesis_triggers['high_stress_cycles'] / 10.0,\n                \"recent_rewards\": self.neurogenesis_triggers['positive_outcomes'],\n                'recent_actions': self.recent_actions[-10:],\n                'food_count': len(self.food_items),\n                'poop_count': len(self.poop_items),\n            }\n            \n            if hasattr(self, 'brain_hooks'):\n                input_values = self.brain_hooks.get_input_neuron_values()\n                brain_state.update(input_values)\n            \n            self.brain_window.update_brain(brain_state)\n            \n            if (hasattr(self.brain_window, 'brain_widget') and \n                hasattr(self.brain_window.brain_widget, 'enhanced_neurogenesis')):\n                neuro = self.brain_window.brain_widget.enhanced_neurogenesis\n                current_status = getattr(self.squid, 'status', 'roaming')\n                action_to_track = self._normalize_action_name(current_status)\n                neuro.track_action(action_to_track)\n                neuro.track_state_change(brain_state)\n                \n                environment = {\n                    'food_count': len(self.food_items),\n                    'poop_count': len(self.poop_items),\n                    'is_sick': self.squid.is_sick,\n                    'is_eating': brain_state.get('is_eating', False),\n                    'has_rock': len(getattr(self, 'rock_items', [])) > 0,\n                    'new_object_encountered': self.new_object_encountered,\n                    'recent_positive_outcome': self.recent_positive_outcome\n                }\n                neuro.check_and_capture_experience(brain_state, environment)\n            \n            if not getattr(self.squid, 'is_sleeping', False):\n                if hasattr(self, 'neuron_output_monitor'):\n                    self.neuron_output_monitor.process_outputs()\n            \n            self.new_object_encountered = False\n            self.recent_positive_outcome = False\n            \n            if self.squid.hunger <= 1 or self.squid.hunger >= 99:\n                self.statistics_window.update_score()\n        \n        # 15. Decay environmental stimulus trackers\n        if hasattr(self, 'brain_hooks'):\n            self.brain_hooks.update_decay()\n        \n        # 16. Post-update hook\n        self.plugin_manager.trigger_hook(\"post_update\", tamagotchi_logic=self, squid=self.squid)\n        \n        if _PERF_TRACKING_AVAILABLE:\n            _sim_elapsed = (time.perf_counter() - _sim_start) * 1000\n            perf_tracker.record(\"simulation_tick\", _sim_elapsed)\n\n    def _get_cached_decorations(self):\n        \"\"\"Get decorations with caching to avoid scanning scene every tick.\"\"\"\n        current_time = time.time()\n        if current_time - self._decoration_cache_time > self._decoration_cache_interval:\n            self._decoration_cache = []\n            for item in self.user_interface.scene.items():\n                if isinstance(item, ResizablePixmapItem) and hasattr(item, 'category'):\n                    # Cache both the item and its center point\n                    center = item.sceneBoundingRect().center()\n                    self._decoration_cache.append((item, center.x(), center.y()))\n            self._decoration_cache_time = current_time\n        return self._decoration_cache\n\n    def apply_input_neurons_to_brain(self):\n        \"\"\"\n        Apply real-time sensor values to the brain's input neurons.\n        Without this, can_see_food, plant_proximity, external_stimulus, etc. will NEVER update.\n        \"\"\"\n        if not self.brain_window or not self.brain_window.brain_widget:\n            return\n\n        brain_widget = self.brain_window.brain_widget\n\n        # Get fresh input neuron values (this calls calculate_can_see_food(), etc.)\n        input_values = self.brain_hooks.get_input_neuron_values()\n\n        # Override specific neurons with VisionWorker results if available\n        # This prioritizes the async worker result over the synchronous check\n        if self.latest_vision_result:\n            # Overwrite the hook's calculation with the worker's result\n            input_values['can_see_food'] = 100.0 if self.latest_vision_result.can_see_food else 0.0\n            \n            # Use worker for plant proximity if available\n            if hasattr(self.latest_vision_result, 'plant_proximity_value'):\n                 input_values['plant_proximity'] = self.latest_vision_result.plant_proximity_value\n\n        if not input_values:\n            return\n\n        # Directly inject into the brain state\n        # This overrides any propagation — input neurons are driven by the world, not the network\n        for neuron_name, activation in input_values.items():\n            # 1. Update visual/display state (what the user sees)\n            if neuron_name in brain_widget.state:\n                brain_widget.state[neuron_name] = activation\n            \n            # 2. CRITICAL FIX: Ensure internal neuron dict is also updated.\n            # If we don't do this, the local simulation logic might use an old value.\n            if hasattr(brain_widget, 'neurons') and neuron_name in brain_widget.neurons:\n                brain_widget.neurons[neuron_name]['activation'] = activation\n\n            # Optional: also update any cached activations if used in older logic/plugins\n            if hasattr(brain_widget, 'neuron_activations') and neuron_name in brain_widget.neuron_activations:\n                brain_widget.neuron_activations[neuron_name] = activation\n\n        # CRITICAL ADDITION: Push these new values to the worker thread immediately.\n        # Without this, the background worker has \"stale\" data (e.g., can_see_food=0) \n        # while the UI shows 100. This ensures Hebbian learning uses the correct values.\n        if hasattr(brain_widget, '_update_worker_cache'):\n            brain_widget._update_worker_cache()\n\n        # Force a repaint to show immediate change\n        brain_widget.update()\n\n    def _check_decorations_message(self):\n        \"\"\"Check if we should show the decorations window reminder message\"\"\"\n        current_time = time.time()\n        \n        # Only check if we haven't shown the max times and enough time has passed\n        if (self.decorations_message_count < self.decorations_message_max and \n            current_time >= self.next_decorations_message_check):\n            \n            # Show the message\n            self.show_message(\"Press D to open the Decorations window\")\n            self.decorations_message_count += 1\n            self.last_decorations_message_time = current_time\n            \n            # Schedule next check for 2-5 minutes from now\n            self.next_decorations_message_check = current_time + random.randint(\n                self.decorations_message_cooldown, \n                self.decorations_message_check_interval\n            )\n            \n\n\n    def update_squid_brain(self):\n        if not self.squid or not self.brain_window.isVisible():\n            return\n\n        # Robust startled detection\n        is_startled_state = False\n        if hasattr(self.squid, 'mental_state_manager') and self.squid.mental_state_manager:\n            is_startled_state = self.squid.mental_state_manager.is_state_active('startled')\n        elif hasattr(self.squid, 'status') and isinstance(self.squid.status, str):\n            is_startled_state = \"startled\" in self.squid.status.lower()\n\n        brain_state = {\n            \"hunger\": self.squid.hunger,\n            \"happiness\": self.squid.happiness,\n            \"cleanliness\": self.squid.cleanliness,\n            \"sleepiness\": self.squid.sleepiness,\n            \"anxiety\": self.squid.anxiety,\n            \"curiosity\": self.squid.curiosity,\n            \"satisfaction\": self.squid.satisfaction,\n            \"is_sick\": self.squid.is_sick,\n            \"is_eating\": self.squid.is_eating if hasattr(self.squid, 'is_eating') else (self.squid.status == \"eating\"),\n            \"is_sleeping\": self.squid.is_sleeping,\n            \"pursuing_food\": self.squid.pursuing_food,\n            \"is_fleeing\": getattr(self.squid, 'is_fleeing', False),\n            \"is_startled\": is_startled_state,\n            \"direction\": self.squid.squid_direction,\n            \"position\": (self.squid.squid_x, self.squid.squid_y),\n            \"personality\": self.squid.personality.value if isinstance(self.squid.personality, Personality) else str(self.squid.personality),\n            \"status\": self.squid.status,\n            'recent_actions': self.recent_actions[-10:],\n            'food_count': len(self.food_items),\n            'poop_count': len(self.poop_items),\n        }\n\n        # === DYNAMIC PERCEPTION ===\n        if hasattr(self, 'brain_hooks'):\n            input_values = self.brain_hooks.get_input_neuron_values()\n            \n            # Override with VisionWorker results\n            if self.latest_vision_result:\n                input_values['can_see_food'] = 100.0 if self.latest_vision_result.can_see_food else 0.0\n            \n            brain_state.update(input_values)\n\n            # --- Force-sync inputs to bw.neurons to prevent Sync Loop overwrite\n            if hasattr(self.brain_window, 'brain_widget'):\n                bw = self.brain_window.brain_widget\n                if hasattr(bw, 'neurons') and bw.neurons:\n                    for k, v in input_values.items():\n                        if k in bw.neurons:\n                            # Update the internal simulation prop directly\n                            bw.neurons[k]['activation'] = v\n\n            # Decay temporal sensors so they don't stick forever\n            self.brain_hooks.update_decay()\n\n        # Allow plugins to inject or modify brain state\n        self.plugin_manager.trigger_hook(\n            \"on_brain_state_update\",\n            brain_state=brain_state,\n            squid=self.squid,\n            logic=self\n        )\n\n        # Final push to brain visualizer\n        self.brain_window.update_brain(brain_state)\n        \n        # === PROCESS NEURON OUTPUTS (ACTUATORS) ===\n        # Also process here in case update_squid_brain runs on different timer\n        if hasattr(self, 'neuron_output_monitor') and self.neuron_output_monitor:\n            self.neuron_output_monitor.process_outputs()\n\n\n    def _normalize_action_name(self, status: str) -> str:\n        \"\"\"\n        Normalize squid status string to a standard action name for tracking.\n        \n        Args:\n            status: Raw status string from squid\n            \n        Returns:\n            Normalized action name\n        \"\"\"\n        if not status:\n            return 'idle'\n        \n        status_lower = status.lower()\n        \n        # Map various status strings to standard actions\n        if 'eat' in status_lower:\n            return 'eating'\n        elif 'sleep' in status_lower:\n            return 'sleeping'\n        elif 'flee' in status_lower or 'escap' in status_lower:\n            return 'fleeing'\n        elif 'startle' in status_lower:\n            return 'startled'\n        elif 'curious' in status_lower or 'investigat' in status_lower:\n            return 'curious'\n        elif 'rock' in status_lower:\n            if 'throw' in status_lower:\n                return 'throwing_rock'\n            elif 'carry' in status_lower:\n                return 'carrying_rock'\n            elif 'approach' in status_lower:\n                return 'approaching_rock'\n            return 'rock_interaction'\n        elif 'plant' in status_lower:\n            return 'near_plant'\n        elif 'sick' in status_lower or 'ill' in status_lower:\n            return 'sick'\n        elif 'roam' in status_lower or 'explor' in status_lower:\n            return 'roaming'\n        elif 'idle' in status_lower or 'rest' in status_lower:\n            return 'idle'\n        else:\n            return 'roaming'  # Default\n    \n    def check_for_curiosity(self):\n        if self.curious_cooldown > 0:\n            self.curious_cooldown -= 1\n            return\n\n        # Base chance of becoming curious\n        curious_chance = 0.02  # 2% base chance\n\n        # Increase chance if satisfaction is high and anxiety is low\n        if self.squid.satisfaction > 70 and self.squid.anxiety < 30:\n            curious_chance *= 2  # Double the chance\n\n        # Check for curiosity\n        if random.random() < curious_chance:\n            self.make_squid_curious()\n\n    def track_neuron_creation(self, neuron_type):\n        if hasattr(self.brain_window, 'statistics_tab'):\n            if neuron_type == 'novelty':\n                self.brain_window.statistics_tab.increment_stat('novelty_neurons_created')\n            elif neuron_type == 'stress':\n                self.brain_window.statistics_tab.increment_stat('stress_neurons_created')\n            elif neuron_type == 'reward':\n                self.brain_window.statistics_tab.increment_stat('reward_neurons_created')\n\n            current = self.brain_window.statistics_tab.statistics['current_neurons']\n            self.brain_window.statistics_tab.statistics['current_neurons'] = current + 1\n            self.brain_window.statistics_tab.update_display()\n            self.statistics_window.award(500)\n\n    def track_neuron_counts(self, current_count, max_count):\n        \"\"\"Track current and maximum neuron counts\"\"\"\n        if hasattr(self.brain_window, 'statistics_tab'):\n            self.brain_window.statistics_tab.update_current_neurons(current_count)\n            self.brain_window.statistics_tab.track_max_neurons(max_count)\n\n    def track_neurogenesis_triggers(self):\n        \"\"\"Update counters for neurogenesis triggers\"\"\"\n        # Novelty tracking\n        if self.new_object_encountered:\n            self.neurogenesis_triggers['novel_objects'] = min(\n                self.neurogenesis_triggers['novel_objects'] + 1,\n                10  # Max cap\n            )\n            # Add thought about novelty\n            self.add_thought(\"Encountered something new!\")\n        else:\n            # Gradual decay when no novelty\n            self.neurogenesis_triggers['novel_objects'] *= 0.95\n        \n        # Stress tracking\n        if self.squid.anxiety > 70:\n            self.neurogenesis_triggers['high_stress_cycles'] += 1\n            # Add thought about stress if threshold crossed\n            if self.neurogenesis_triggers['high_stress_cycles'] > 5:\n                self.add_thought(\"Feeling stressed for a while...\")\n        else:\n            self.neurogenesis_triggers['high_stress_cycles'] = max(\n                0,\n                self.neurogenesis_triggers['high_stress_cycles'] - 0.5\n            )\n        \n        # Reward tracking (positive outcomes like eating, playing)\n        if self.recent_positive_outcome:\n            self.neurogenesis_triggers['positive_outcomes'] = min(\n                self.neurogenesis_triggers['positive_outcomes'] + 1,\n                5  # Max cap\n            )\n            # Add thought about positive experience\n            if random.random() < 0.3:  # 30% chance to comment\n                self.add_thought(\"That was enjoyable!\")\n        else:\n            # Gradual decay when no rewards\n            self.neurogenesis_triggers['positive_outcomes'] = max(\n                0,\n                self.neurogenesis_triggers['positive_outcomes'] - 0.2\n            )\n        \n        # Debug output if in debug mode\n        if self.debug_mode:\n            print(f\"Neurogenesis triggers: {self.neurogenesis_triggers}\")\n\n    def _normalize_action_name(self, status_string):\n        \"\"\"\n        Convert verbose status strings into trackable action categories.\n        This prevents explosion of unique but semantically similar actions.\n        \"\"\"\n        if not status_string:\n            return 'idle'\n            \n        status_lower = status_string.lower()\n        \n        # Food-related behaviors\n        if 'eating' in status_lower:\n            return 'eating'\n        elif 'eyeing food' in status_lower or 'approaching food' in status_lower:\n            return 'pursuing_food'\n        elif 'moving toward food' in status_lower:\n            return 'pursuing_food'\n        \n        # Rock interactions\n        elif 'rock' in status_lower:\n            if 'throwing' in status_lower or 'thrown' in status_lower:\n                return 'throwing_rock'\n            elif 'approaching' in status_lower or 'eyeing' in status_lower:\n                return 'investigating_rock'\n            elif 'carrying' in status_lower:\n                return 'carrying_rock'\n        \n        # Poop interactions\n        elif 'poop' in status_lower:\n            if 'throwing' in status_lower:\n                return 'throwing_poop'\n            elif 'approaching' in status_lower:\n                return 'investigating_poop'\n        \n        # Plant interactions\n        elif 'plant' in status_lower:\n            if 'hiding' in status_lower:\n                return 'hiding'\n            elif 'comfort' in status_lower or 'approaching' in status_lower or 'seeking' in status_lower:\n                return 'seeking_plant'\n            elif 'noticing' in status_lower:\n                return 'noticing_plant'\n        \n        # Emotional/mental states\n        elif 'startled' in status_lower or 'fleeing' in status_lower:\n            return 'fleeing'\n        elif 'anxious' in status_lower or 'nervous' in status_lower:\n            return 'anxious'\n        elif 'sleeping' in status_lower or 'drowsy' in status_lower or 'feeling drowsy' in status_lower:\n            return 'sleeping'\n        elif 'sick' in status_lower or 'ill' in status_lower or 'suffering' in status_lower:\n            return 'sick'\n        elif 'distressed' in status_lower:\n            return 'distressed'\n        \n        # Exploration behaviors\n        elif 'exploring' in status_lower:\n            return 'exploring'\n        elif 'roaming' in status_lower or 'wandering' in status_lower or 'patrolling' in status_lower:\n            return 'roaming'\n        elif 'zooming' in status_lower or 'buzzing' in status_lower:\n            return 'energetic_movement'\n        elif 'resting' in status_lower or 'lounging' in status_lower:\n            return 'resting'\n        elif 'watching' in status_lower:\n            return 'watching'\n        elif 'seeking adventure' in status_lower:\n            return 'seeking_adventure'\n        elif 'searching' in status_lower or 'scouting' in status_lower:\n            return 'searching'\n        \n        # Default fallback\n        else:\n            return 'idle'\n\n    def make_squid_curious(self):\n        self.squid.mental_state_manager.set_state(\"curious\", True)\n        self.curious_cooldown = self.curious_cooldown_max\n        \n        # Use the new add_thought method\n        self.add_thought(\"Experiencing extreme curiosity\")\n        \n        # Increase curiosity\n        self.squid.curiosity = min(100, self.squid.curiosity + 20)\n\n        # Memory\n        self.squid.memory_manager.add_short_term_memory(\n            'emotion', 'intense_curiosity',\n            'Overwhelmed by intense curiosity!',\n            importance=4\n        )\n        \n        # Schedule the end of the curious state\n        QtCore.QTimer.singleShot(5000, self.end_curious)  # End curious after 5 seconds\n\n        # Start curious interactions\n        self.curious_interaction_timer = QtCore.QTimer()\n        self.curious_interaction_timer.timeout.connect(self.curious_interaction)\n        self.curious_interaction_timer.start(1000)  # Check for interactions every second\n\n    def end_curious(self):\n        if self.mental_states_enabled:\n            self.squid.mental_state_manager.set_state(\"curious\", False)\n        if hasattr(self, 'curious_interaction_timer'):\n            self.curious_interaction_timer.stop()\n\n    def curious_interaction(self):\n        if self.curious_interaction_cooldown > 0:\n            self.curious_interaction_cooldown -= 1\n            return\n\n        if random.random() < 0.6:  # Increased chance to 60% for more frequent interactions\n            decorations = self.user_interface.get_nearby_decorations(self.squid.squid_x, self.squid.squid_y)\n            if decorations:\n                decoration = random.choice(decorations)\n                if random.random() < 0.75:  # 75% chance to push decorations\n                    direction = random.choice([-1, 1])  # -1 for left, 1 for right\n                    self.squid.push_decoration(decoration, direction)\n                else:\n                    self.brain_window.add_thought(\"I am curious about a decoration item...\")\n                \n                self.curious_interaction_cooldown = self.curious_interaction_cooldown_max\n\n    def update_curiosity(self):\n        # Update curiosity based on satisfaction and anxiety\n        if self.squid.satisfaction > 70 and self.squid.anxiety < 30:\n            curiosity_change = 0.2 * self.simulation_speed\n        else:\n            curiosity_change = -0.1 * self.simulation_speed\n\n        # Adjust curiosity change based on personality\n        if self.squid.personality == Personality.TIMID:\n            curiosity_change *= 0.5  # Timid squids are less curious\n        elif self.squid.personality == Personality.ADVENTUROUS:\n            curiosity_change *= 1.5  # Adventurous squids are more curious\n\n        self.squid.curiosity += curiosity_change\n        self.squid.curiosity = max(0, min(100, self.squid.curiosity))\n\n        # Check if the squid should enter the curious state\n        if self.squid.curiosity > 80 and self.mental_states_enabled:\n            self.check_for_curiosity()\n\n    def move_objects(self):\n        self.move_foods()\n        self.move_poops()\n\n    def move_squid_to_bottom_left(self, callback):      # Force the squid to move to bottom left (buggy)\n        target_x = 150  # Left edge + margin\n        target_y = self.user_interface.window_height - 150 - self.squid.squid_height  # Bottom edge - margin - squid height\n\n        # Disable Squid's ability to move in any other direction - doesn't work 100% - he puts up a fight sometimes!!\n        self.squid.can_move = False\n\n        def step_movement():\n            dx = target_x - self.squid.squid_x\n            dy = target_y - self.squid.squid_y\n\n            if abs(dx) < 100 and abs(dy) < 100:\n                # If close enough, snap to final position and call callback\n                self.squid.squid_x = target_x\n                self.squid.squid_y = target_y\n                self.squid.squid_item.setPos(self.squid.squid_x, self.squid.squid_y)\n                self.squid.can_move = True  # Re-enable Squid's movement\n                callback()\n            else:\n                # Determine direction of movement\n                if abs(dx) > abs(dy):\n                    # Move horizontally\n                    self.squid.squid_x += 90 if dx > 0 else -90\n                else:\n                    # Move vertically\n                    self.squid.squid_y += 90 if dy > 0 else -90\n\n                # Update squid position\n                self.squid.squid_item.setPos(self.squid.squid_x, self.squid.squid_y)\n\n                # Schedule next movement in 1000 ms\n                QtCore.QTimer.singleShot(900, step_movement)\n\n        # Start the movement\n        step_movement()\n\n    def start_rps_game(self):\n        self.rps_game = RPSGame(self)\n        self.rps_game.start_game()\n\n    def give_medicine(self):\n        \n        # Get plugin results\n        results = self.plugin_manager.trigger_hook(\"on_medicine\", \n                                                tamagotchi_logic=self, \n                                                squid=self.squid)\n        \n        # Check if any plugin returned False to prevent default behavior\n        if False in results:\n            return\n        \n        if (self.squid is not None and \n            (self.squid.is_sick or \n            self.squid.mental_state_manager.is_state_active('sick'))):\n            \n            print(\"Debug: Applying medicine effects\")\n            self.squid.is_sick = False\n            self.squid.mental_state_manager.set_state(\"sick\", False)\n            \n            self.squid.happiness = max(0, self.squid.happiness - 30)\n            self.squid.sleepiness = min(100, self.squid.sleepiness + 50)\n            self.statistics_window.award(-100)\n            self.show_message(\"Medicine given. Squid didn't like that!\")\n            \n            # Add thoughts and set status\n            if hasattr(self.brain_window, 'add_thought'):\n                self.brain_window.add_thought(\"I am grumpy and anxious because I was forced to take medicine\")\n            \n            self.squid.status = \"taking medicine\"\n            \n            # Hide the sick icon immediately\n            self.squid.hide_sick_icon()\n\n            # Put Squid to sleep\n            QtCore.QTimer.singleShot(5000, lambda: self.delayed_sleep_after_medicine())\n\n            # Display the needle image\n            self.display_needle_image()\n        else:\n            self.show_message(\"Squid is not sick. Medicine not needed.\")\n\n    def delayed_sleep_after_medicine(self):\n        \"\"\"Put squid to sleep after a delay from taking medicine\"\"\"\n        if self.squid:\n            self.squid.go_to_sleep()\n            self.squid.status = \"recovering\"\n\n    def display_needle_image(self):\n        needle_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"needle.jpg\"))\n        self.needle_item = QtWidgets.QGraphicsPixmapItem(needle_pixmap)\n        self.needle_item.setPos(self.user_interface.window_width // 2 - needle_pixmap.width() // 2,\n                                self.user_interface.window_height // 2 - needle_pixmap.height() // 2)\n        self.needle_item.setZValue(10)  # Ensure the needle image is displayed on top of everything\n        self.user_interface.scene.addItem(self.needle_item)\n\n        # Create a QGraphicsOpacityEffect\n        opacity_effect = QtWidgets.QGraphicsOpacityEffect()\n        self.needle_item.setGraphicsEffect(opacity_effect)\n\n        # Create a QPropertyAnimation for the opacity effect\n        self.needle_animation = QtCore.QPropertyAnimation(opacity_effect, b\"opacity\")\n        self.needle_animation.setDuration(1000)  # 1 second duration\n        self.needle_animation.setStartValue(1.0)\n        self.needle_animation.setEndValue(0.0)\n        self.needle_animation.setEasingCurve(QtCore.QEasingCurve.InQuad)\n\n        # Connect the finished signal to remove the item\n        self.needle_animation.finished.connect(self.remove_needle_image)\n\n        # Start the animation\n        self.needle_animation.start()\n\n    def remove_needle_image(self):\n        if self.needle_item is not None:\n            self.user_interface.scene.removeItem(self.needle_item)\n            self.needle_item = None\n\n    def move_foods(self):\n        for food_item in self.food_items[:]:\n            if getattr(food_item, 'is_sushi', False):\n                self.move_sushi(food_item)\n            else:\n                self.move_cheese(food_item)\n\n    def get_food_item_at(self, x, y):\n        for food_item in self.food_items:\n            if food_item.pos().x() == x and food_item.pos().y() == y:\n                return food_item\n        return None\n\n    def move_cheese(self, cheese_item):\n        cheese_x = cheese_item.pos().x()\n        cheese_y = cheese_item.pos().y() + (self.base_food_speed * self.simulation_speed)\n\n        if cheese_y > self.user_interface.window_height - 120 - self.food_height:\n            cheese_y = self.user_interface.window_height - 120 - self.food_height\n\n        cheese_item.setPos(cheese_x, cheese_y)\n\n        # Directly check collision without redundant checks\n        if self.squid and cheese_item.collidesWithItem(self.squid.squid_item):\n            self.squid.eat(cheese_item)\n            \n            # Reset status after a short delay\n            QtCore.QTimer.singleShot(2000, self.reset_squid_status)\n\n    def move_sushi(self, sushi_item):\n        sushi_x = sushi_item.pos().x()\n        sushi_y = sushi_item.pos().y() + (self.base_food_speed * self.simulation_speed)\n\n        if sushi_y > self.user_interface.window_height - 120 - self.food_height:\n            sushi_y = self.user_interface.window_height - 120 - self.food_height\n\n        sushi_item.setPos(sushi_x, sushi_y)\n\n        if self.squid is not None and sushi_item.collidesWithItem(self.squid.squid_item):\n            self.squid.eat(sushi_item)  # Pass the sushi_item as an argument\n            self.remove_food(sushi_item)\n            \n            # Reset status after a short delay (matching cheese behavior)\n            QtCore.QTimer.singleShot(2000, self.reset_squid_status)\n\n    def is_sushi(self, food_item):\n        return getattr(food_item, 'is_sushi', False)     \n\n    def remove_food(self, food_item):\n        if food_item in self.food_items:\n            self.user_interface.scene.removeItem(food_item)\n            self.food_items.remove(food_item)\n\n            # Notify vision worker of scene change\n            if hasattr(self.squid, 'mark_scene_objects_dirty'):\n                self.squid.mark_scene_objects_dirty()\n\n    def move_poops(self):\n        for poop_item in self.poop_items[:]:\n            poop_x = poop_item.pos().x()\n            poop_y = poop_item.pos().y() + (self.base_food_speed * self.simulation_speed)\n\n            if poop_y > self.user_interface.window_height - 120 - self.squid.poop_height:\n                poop_y = self.user_interface.window_height - 120 - self.squid.poop_height\n\n            poop_item.setPos(poop_x, poop_y)\n\n    def update_statistics(self):\n        if self.squid is None:\n            return\n\n        # ------- GUARD ---------------------------------------------------------\n        if hasattr(self, 'statistics_window') and self.statistics_window is not None:\n            self.statistics_window.update_statistics()\n        # -----------------------------------------------------------------------\n\n        self.update_cleanliness_overlay()\n\n        if self.squid is not None:\n            # Update squid needs\n            if not self.squid.is_sleeping:\n                self.squid.hunger      = min(100, self.squid.hunger      + (0.1 * self.simulation_speed))\n                self.squid.sleepiness  = min(100, self.squid.sleepiness  + (0.12 * self.simulation_speed))\n                self.squid.happiness   = max(0,   self.squid.happiness   - (0.1 * self.simulation_speed))\n                self.squid.cleanliness = max(0,   self.squid.cleanliness - (0.1 * self.simulation_speed))\n\n                # Update new neurons\n                self.update_satisfaction()\n                self.update_anxiety()\n                self.update_curiosity()\n\n                # Check for special status effects on anxiety reduction.\n                if self.squid.status == \"hiding behind plant\":\n                    previous_anxiety = self.squid.anxiety\n                    self.squid.anxiety = max(0, self.squid.anxiety - (0.5 * self.simulation_speed))\n                    if self.squid.anxiety < previous_anxiety:\n                        memory_value = \"Being near plants is calming (Anxiety reduction)\"\n                        self.squid.memory_manager.add_short_term_memory(\n                            'environment',\n                            'plant_calming_effect',\n                            memory_value,\n                            importance=1.5\n                        )\n                        self.plant_calming_effect_counter += 1\n                        if self.plant_calming_effect_counter >= 5:\n                            self.squid.memory_manager.transfer_to_long_term_memory(\n                                'environment',\n                                'plant_calming_effect'\n                            )\n                            self.plant_calming_effect_counter = 0\n\n                # Check if cleanliness has been too low for too long\n                if self.squid.cleanliness < 20:\n                    self.cleanliness_threshold_time += 1\n                else:\n                    self.cleanliness_threshold_time = 0\n\n                # Check if hunger has been too high for too long\n                if self.squid.hunger > 80:\n                    self.hunger_threshold_time += 1\n                else:\n                    self.hunger_threshold_time = 0\n\n                # Check if squid becomes sick (80 % chance)\n                if ((self.cleanliness_threshold_time >= 10 * self.simulation_speed and\n                    self.cleanliness_threshold_time <= 60 * self.simulation_speed) or\n                    (self.hunger_threshold_time >= 10 * self.simulation_speed and\n                    self.hunger_threshold_time <= 50 * self.simulation_speed)):\n                    if random.random() < 0.8:\n                        self.squid.mental_state_manager.set_state(\"sick\", True)\n                else:\n                    self.squid.mental_state_manager.set_state(\"sick\", False)\n\n                # New logic for health decrease based on happiness and cleanliness\n                if self.squid.happiness < 20 and self.squid.cleanliness < 20:\n                    health_decrease = 0.2 * self.simulation_speed  # Rapid decrease\n                else:\n                    health_decrease = 0.1 * self.simulation_speed \n\n    def handle_window_resize(self, event):\n        new_width = event.size().width()\n        new_height = event.size().height()\n        \n        # Get current dimensions from user interface\n        current_width = self.user_interface.window_width\n        current_height = self.user_interface.window_height\n        \n        # Calculate size change\n        width_change = new_width - current_width\n        height_change = new_height - current_height\n        \n        # Create new_size tuple for brain hooks\n        new_size = (new_width, new_height)  # ADD THIS LINE\n        \n        # Update window dimensions in UI\n        self.user_interface.window_width = new_width\n        self.user_interface.window_height = new_height\n        \n        # Update UI elements through user interface\n        self.user_interface.handle_window_resize(event)\n        \n        # Notify logic about resize with size change info\n        self.brain_hooks.on_window_resize(width_change, height_change, new_size)\n        self.handle_window_resize_event(\n            width_change, \n            height_change,\n            new_size\n        )\n\n    def allow_initial_startle(self):\n        \"\"\"Allow the squid to be startled after the initial delay.\"\"\"\n        self.initial_startle_allowed = True\n        self.is_first_instance = False  # Reset the flag after the first instance\n        #print(\"Initial startle protection period ended\")\n    \n    def handle_window_resize_event(self, width_change, height_change, new_size):\n        \"\"\"Handle window resize events with specific effects\"\"\"\n        # First resize startles the squid (only once and after the initial delay)\n        if not self.has_been_resized and self.initial_startle_allowed:\n            self.startle_squid(source=\"first_resize\")\n            self.has_been_resized = True\n            self.add_thought(\"positive: My environment got bigger!\")\n            self.last_window_size = new_size\n            return\n\n        # Only startle for MAJOR size changes (increased threshold)\n        if (abs(width_change) > 200 or abs(height_change) > 200) and random.random() < 0.3:  # Added randomness\n            # self.startle_squid(source=\"major_resize\") # STARTLE WHEN WINDOW IS RESIZED\n            return\n\n        # Check if window got bigger\n        if new_size[0] > self.last_window_size[0] or new_size[1] > self.last_window_size[1]:\n            # Positive effect for enlargement\n            self.squid.happiness = min(100, self.squid.happiness + 5)\n            self.squid.satisfaction = min(100, self.squid.satisfaction + 3)\n            self.was_big = True\n\n            memory_msg = \"My environment got bigger!\"\n            self.squid.memory_manager.add_short_term_memory(\n                'environment',\n                'window_enlarged',\n                memory_msg\n            )\n            self.add_thought(\"More space to swim!\")\n\n        # Check if window got smaller after being big\n        elif self.was_big and (new_size[0] < self.last_window_size[0] or new_size[1] < self.last_window_size[1]):\n            # Negative effect for reduction\n            self.squid.happiness = max(0, self.squid.happiness - 5)\n            self.squid.anxiety = min(100, self.squid.anxiety + 5)\n\n            memory_msg = \"negative: decreased happiness and increased anxiety from less space\"\n            self.squid.memory_manager.add_short_term_memory(\n                'environment',\n                'window_reduced',\n                memory_msg\n            )\n            self.add_thought(\"The space is shrinking...\")\n\n        # Update last known size\n        self.last_window_size = new_size\n\n    def track_action(self, action_name):\n        \"\"\"Track what the squid is doing\"\"\"\n        self.recent_actions.append(action_name)\n        if len(self.recent_actions) > 10:\n            self.recent_actions.pop(0)\n\n    def feed_squid(self):\n        \"\"\"Modified to track action for neurogenesis\"\"\"\n        # Track for neurogenesis FIRST\n        if hasattr(self.brain_window, 'brain_widget') and \\\n           hasattr(self.brain_window.brain_widget, 'enhanced_neurogenesis'):\n            self.brain_window.brain_widget.enhanced_neurogenesis.track_action('user_feeding')\n        \n        self.track_action('feeding')  # Keep original tracking\n        self.brain_hooks.on_user_interaction('feed')\n        \n        # Get plugin results\n        results = self.plugin_manager.trigger_hook(\"on_feed\", \n                                                tamagotchi_logic=self, \n                                                squid=self.squid)\n        \n        # Check if any plugin returned False to prevent default behavior\n        if False in results:\n            return\n        \n        # Continue with original behavior\n        if len(self.food_items) >= self.max_food:\n            return\n        \n        # Only create one food item\n        is_sushi = random.random() < 0.5\n        self.spawn_food(is_sushi=is_sushi)\n\n        if self.squid.hunger < 50:  # Feeding was successful\n            self.brain_window.brain_widget.provide_outcome_feedback(1.0)\n\n    def spawn_food(self, is_sushi=False):\n        if len(self.food_items) >= self.max_food:\n            return\n        \n        # Create only one food item\n        if is_sushi:\n            food_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"sushi.png\"))\n            food_item = QtWidgets.QGraphicsPixmapItem(food_pixmap)\n            food_item.is_sushi = True\n        else:\n            food_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"cheese.png\"))\n            food_item = QtWidgets.QGraphicsPixmapItem(food_pixmap)\n            food_item.is_sushi = False\n\n        food_x = random.randint(50, self.user_interface.window_width - 50 - self.food_width)\n        food_item.setPos(food_x, 50)\n        \n        # Add to scene and tracking list\n        self.user_interface.scene.addItem(food_item)\n        self.food_items.append(food_item)  # Single addition\n        self.brain_hooks.on_object_spawned('food')\n\n        # Notify vision worker of scene change\n        if hasattr(self.squid, 'mark_scene_objects_dirty'):\n            self.squid.mark_scene_objects_dirty()\n\n    def clean_environment(self):\n        # Track for neurogenesis FIRST\n        if hasattr(self.brain_window, 'brain_widget') and \\\n        hasattr(self.brain_window.brain_widget, 'enhanced_neurogenesis'):\n            self.brain_window.brain_widget.enhanced_neurogenesis.track_action('user_cleaning')\n\n        self.brain_hooks.on_user_interaction('clean')\n        \n        self.track_action('cleaning')\n        current_time = time.time()\n        if current_time - self.last_clean_time < self.clean_cooldown:\n            remaining_cooldown = int(self.clean_cooldown - (current_time - self.last_clean_time))\n            self.show_message(f\"Cleaning is on cooldown. Please wait {remaining_cooldown} seconds.\")\n            return\n\n        # Get plugin results\n        results = self.plugin_manager.trigger_hook(\"on_clean\",\n                                                tamagotchi_logic=self,\n                                                squid=self.squid)\n\n        # Check if any plugin returned False to prevent default behavior\n        if False in results:\n            return\n\n        self.last_clean_time = current_time\n\n        # Create a cleaning line that extends beyond the window vertically\n        self.cleaning_line = QtWidgets.QGraphicsLineItem(self.user_interface.window_width, -500,\n                                                        self.user_interface.window_width, self.user_interface.window_height + 500)\n        self.cleaning_line.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 15))  # Thick black line\n        self.user_interface.scene.addItem(self.cleaning_line)\n\n        # Show a message that cleaning has started\n        self.show_message(\"Cleaning in progress...\")\n\n        # Set up animation parameters\n        self.cleaning_progress = 0\n        self.movement_rate = 200  # Movement rate in pixels per second\n        self.cleaning_timer = QtCore.QTimer()\n        self.cleaning_timer.timeout.connect(self.update_cleaning)\n        self.cleaning_timer.start(500)  # Update every 1000 ms (1 second)\n\n    def update_cleaning(self):\n        self.cleaning_progress += self.movement_rate  # Increment progress by movement rate each second\n        if self.cleaning_progress >= self.user_interface.window_width:\n            self.cleaning_timer.stop()\n            self.finish_cleaning()\n            return\n\n        new_x = self.user_interface.window_width - self.cleaning_progress\n        self.cleaning_line.setLine(new_x, -500, new_x, self.user_interface.window_height + 500)\n\n        items_removed = False\n\n        # Remove poops and food that the line has passed\n        for poop_item in self.poop_items[:]:\n            if poop_item.scenePos().x() > new_x:\n                self.user_interface.scene.removeItem(poop_item)\n                self.poop_items.remove(poop_item)\n                items_removed = True\n\n        for food_item in self.food_items[:]:\n            if food_item.pos().x() > new_x:\n                self.remove_food(food_item) # This calls mark_scene_objects_dirty internally\n                items_removed = True\n\n        # Force an update of the scene\n        self.user_interface.scene.update()\n\n        # Explicitly notify if we removed poop (food handled by remove_food)\n        if items_removed and hasattr(self.squid, 'mark_scene_objects_dirty'):\n            self.squid.mark_scene_objects_dirty()\n\n\n\n    def finish_cleaning(self):\n        # 1. remove the cleaning line (existing)\n        self.user_interface.scene.removeItem(self.cleaning_line)\n\n        # 2. count poops actually removed & award\n        poops_removed = 0\n        for poop_item in self.poop_items[:]:\n            if not poop_item.scene():   # already erased by the swipe\n                poops_removed += 1\n        if poops_removed:\n            self.statistics_window.award(25 * poops_removed)   # 25 per poop\n\n        # 3. rest unchanged …\n        if self.squid:\n            self.squid.cleanliness = 100\n            self.squid.happiness = min(100, self.squid.happiness + 20)\n            self.squid.memory_manager.add_short_term_memory(\n                'cleanliness', 'washed_clean',\n                'Washed clean! Feeling fresh.',\n                importance=3\n            )\n        \n        # Clear all DIRTY text immediately when cleaned\n        self.user_interface.clear_dirty_text()\n        \n        self.show_message(\"Environment cleaned! Squid is happier!\")\n        self.user_interface.scene.update()\n\n    def show_message(self, message):\n        # Call hook if available\n        if hasattr(self, 'plugin_manager'):\n            # Get modified message from plugins\n            results = self.plugin_manager.trigger_hook(\n                \"on_message_display\", \n                tamagotchi_logic=self,\n                original_message=message\n            )\n            \n            # Check if any plugin modified the message\n            for result in results:\n                if isinstance(result, str) and result:\n                    message = result\n                    break\n        \n        # Use the user_interface's scene instead of self.scene\n        if hasattr(self, 'user_interface') and hasattr(self.user_interface, 'scene'):\n            scene = self.user_interface.scene\n            # Remove any existing message items\n            for item in scene.items():\n                if isinstance(item, QtWidgets.QGraphicsTextItem):\n                    scene.removeItem(item)\n\n            # Create a new QGraphicsTextItem for the message\n            message_item = QtWidgets.QGraphicsTextItem(message)\n            message_item.setDefaultTextColor(QtGui.QColor(255, 255, 255))  # White text\n            message_item.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\n            message_item.setPos(0, self.user_interface.window_height - 75)  # Position the message higher\n            message_item.setTextWidth(self.user_interface.window_width)\n            message_item.setHtml(f'<div style=\"text-align: center; background-color: #000000; padding: 5px;\">{message}</div>')\n            message_item.setZValue(10)  # Ensure the message is on top\n            message_item.setOpacity(1)\n\n            # Add the new message item to the scene\n            scene.addItem(message_item)\n\n            # Fade out the message after 8 seconds\n            fade_out_animation = QtCore.QPropertyAnimation(message_item, b\"opacity\")\n            fade_out_animation.setDuration(8000)\n            fade_out_animation.setStartValue(1.0)\n            fade_out_animation.setEndValue(0.0)\n            fade_out_animation.finished.connect(lambda: scene.removeItem(message_item))\n            fade_out_animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)\n\n    def show_thought(self, text):\n        \"\"\"Display a squid 'thought' in yellow.\"\"\"\n        self.user_interface.show_message(f'<span style=\"color:#FFEB3B;\">{text}</span>')\n\n\n    def update_score(self):\n        if self.squid is not None:\n            if not self.squid.is_sick and self.squid.happiness >= 80 and self.squid.cleanliness >= 80:\n                self.points += 1\n            elif self.squid.is_sick or self.squid.hunger >= 80 or self.squid.happiness <= 20:\n                self.points -= 1\n\n            self.user_interface.update_points(self.points)\n\n    def toggle_debug_mode(self):\n        \"\"\"Toggle debug mode across all components without circular references\"\"\"\n        # Set the new state directly (don't toggle twice)\n        new_debug_mode = not self.debug_mode\n        self.debug_mode = new_debug_mode\n        \n        # Propagate to statistics window\n        if hasattr(self, 'statistics_window') and self.statistics_window:\n            self.statistics_window.set_debug_mode(new_debug_mode)\n        \n        # Propagate to brain window, WITHOUT triggering a callback\n        if hasattr(self, 'brain_window') and self.brain_window:\n            # Set a flag to indicate we're in the middle of propagating\n            self._propagating_debug_mode = True\n            \n            # Set brain window's debug mode\n            self.brain_window.set_debug_mode(new_debug_mode)\n            \n            # Set brain widget's debug mode directly \n            if hasattr(self.brain_window, 'brain_widget'):\n                self.brain_window.brain_widget.debug_mode = new_debug_mode\n            \n            # Clear the propagation flag\n            self._propagating_debug_mode = False\n        \n        # User interface components\n        if hasattr(self, 'user_interface'):\n            if hasattr(self.user_interface, 'debug_mode'):\n                self.user_interface.debug_mode = new_debug_mode\n        \n        print(f\"Debug mode {'enabled' if new_debug_mode else 'disabled'}\")\n\n    def update_cleanliness_overlay(self):\n        if self.squid is not None:\n            cleanliness = self.squid.cleanliness\n            # Use the new DIRTY text system instead of brown overlay\n            self.user_interface.update_dirty_text(cleanliness)\n            # Keep the overlay transparent (no more brown color)\n            self.user_interface.cleanliness_overlay.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)))\n    \n    def spawn_sushi(self):\n        if len(self.food_items) < self.max_food:\n            sushi_pixmap = QtGui.QPixmap(os.path.join(\"images\", \"sushi.png\"))\n            sushi_pixmap = sushi_pixmap.scaled(self.food_width, self.food_height)\n\n            sushi_item = QtWidgets.QGraphicsPixmapItem(sushi_pixmap)\n\n            sushi_x = random.randint(50, self.user_interface.window_width - 50 - self.food_width)\n            sushi_y = 50  # Start at the top of the screen\n            sushi_item.setPos(sushi_x, sushi_y)\n\n            self.user_interface.scene.addItem(sushi_item)\n            sushi_item = QtWidgets.QGraphicsPixmapItem(sushi_pixmap)\n            sushi_item.is_sushi = True\n            self.food_items.append(sushi_item)\n\n            # Notify vision worker of scene change\n            if hasattr(self.squid, 'mark_scene_objects_dirty'):\n                self.squid.mark_scene_objects_dirty()\n\n    def spawn_poop(self, x, y):\n        if len(self.poop_items) < self.max_poop and self.squid is not None:\n            poop_item = ResizablePixmapItem(self.squid.poop_images[0], category='poop')\n            poop_item.setPos(x - self.squid.poop_width // 2, y)\n            self.user_interface.scene.addItem(poop_item)\n            self.poop_items.append(poop_item)\n            self.brain_hooks.on_object_spawned('poop')\n\n            # Notify vision worker of scene change\n            if hasattr(self.squid, 'mark_scene_objects_dirty'):\n                self.squid.mark_scene_objects_dirty()\n\n    def animate_poops(self):\n        if self.squid is not None:\n            for poop_item in self.poop_items:\n                current_frame = self.poop_items.index(poop_item) % 2\n                poop_item.setPixmap(self.squid.poop_images[current_frame])\n\n    def game_over(self):\n        game_over_dialog = QtWidgets.QMessageBox()\n        game_over_dialog.setIcon(QtWidgets.QMessageBox.Critical)\n        game_over_dialog.setText(\"Game Over\")\n        game_over_dialog.setInformativeText(\"Your squid has died due to poor health.\")\n        game_over_dialog.setWindowTitle(\"Game Over\")\n        game_over_dialog.exec_()\n        self.user_interface.window.close()\n\n        # Add any additional game over logic or cleanup here\n        print(\"Game Over - Squid died due to poor health\")\n\n        # Save the game state before resetting\n        self.save_game()\n\n        # Reset the game state\n        self.reset_game()\n\n    def reset_game(self):\n        # Reset squid attributes\n        self.squid.hunger = 25\n        self.squid.sleepiness = 30\n        self.squid.happiness = 100\n        self.squid.cleanliness = 100\n        self.squid.is_sleeping = False\n        self.squid.health = 100\n        self.squid.is_sick = False\n\n        # Reset new neurons\n        self.squid.satisfaction = 50\n        self.squid.anxiety = 10\n        self.squid.curiosity = 70\n\n        # Reset game variables\n        self.cleanliness_threshold_time = 0\n        self.hunger_threshold_time = 0\n        self.last_clean_time = 0\n\n        # Clear food and poop items\n        for food_item in self.food_items:\n            self.user_interface.scene.removeItem(food_item)\n        self.food_items.clear()\n\n        for poop_item in self.poop_items:\n            self.user_interface.scene.removeItem(poop_item)\n        self.poop_items.clear()\n        \n        # Clear all DIRTY text on reset\n        self.user_interface.clear_dirty_text()\n\n        # Reset squid position\n        self.squid.squid_x = self.squid.center_x\n        self.squid.squid_y = self.squid.center_y\n        self.squid.squid_item.setPos(self.squid.squid_x, self.squid.squid_y)\n\n        # Notify vision worker of scene change (cleared objects)\n        if hasattr(self.squid, 'mark_scene_objects_dirty'):\n            self.squid.mark_scene_objects_dirty()\n\n        # Show a message\n        self.show_message(\"Game reset. Take better care of your squid!\")\n\n        # Force an update of the scene\n        self.user_interface.scene.update()\n\n        # Save the game state after resetting\n        self.save_game()\n\n    def load_game(self) -> bool:\n        # Use save_manager\n        latest = self.save_manager.get_latest_save()\n        if not latest:\n            print(\"[Load] No save file found\")\n            return False\n\n        try:\n            data = {}\n            squid_uuid = None\n\n            with zipfile.ZipFile(latest, 'r') as zf:\n                for fname in zf.namelist():\n                    if fname == \"uuid.txt\":\n                        raw = zf.read(fname).decode('utf-8').strip()\n                        if \"SquidSignature\" in raw:\n                            squid_uuid = raw.split(\"SquidSignature\")[-1].strip()\n                        else:\n                            squid_uuid = raw\n                        continue\n\n                    if fname.endswith('.json'):\n                        with zf.open(fname) as f:\n                            key = os.path.splitext(fname)[0]\n                            json_data = f.read().decode('utf-8')\n                            if json_data.strip():\n                                data[key] = json.loads(json_data)\n\n            # === RESTORE UUID INTO LIVE SQUID ===\n            if squid_uuid and self.squid:\n                try:\n                    import uuid\n                    restored_uuid = uuid.UUID(squid_uuid)\n                    old_uuid = self.squid.uuid\n                    self.squid.uuid = restored_uuid\n                    print(f\"[Load] Restored Squid UUID: {restored_uuid} (was {old_uuid})\")\n                except ValueError as e:\n                    print(f\"[Load] Invalid UUID format in save: {squid_uuid} → {e}\")\n\n            # === Load game state ===\n            game_state = data.get('game_state', {})\n            if not game_state:\n                print(\"[Load] No game_state found\")\n                return False\n\n            squid_data = game_state.get('squid', {})\n            if squid_data:\n                for attr, value in squid_data.items():\n                    if hasattr(self.squid, attr):\n                        setattr(self.squid, attr, value)\n\n            # Restore personality\n            if 'personality' in squid_data:\n                from .squid import Personality\n                try:\n                    self.squid.personality = Personality(squid_data['personality'])\n                except ValueError:\n                    pass\n\n            # Load custom brain (Logic patched to load output bindings)\n            custom_brain_data = data.get('custom_brain', {})\n            custom_brain_loaded = False\n            \n            if custom_brain_data and custom_brain_data.get('is_custom_brain'):\n                success, msg = restore_custom_brain_from_save(data, self.brain_window.brain_widget)\n                print(f\"[Load] Custom brain: {msg}\")\n                \n                # Manually load bindings from custom brain definition ---\n                if success and hasattr(self, 'neuron_output_monitor'):\n                    brain_def = custom_brain_data.get('brain_definition', {})\n                    if brain_def:\n                        self.neuron_output_monitor.load_bindings_from_brain(brain_def)\n                        custom_brain_loaded = True\n                        print(f\"     + Synced custom output bindings to monitor\")\n\n            # Load brain state (Standard)\n            brain_state = data.get('brain_state', {})\n            if brain_state and hasattr(self, 'brain_window'):\n                self.brain_window.set_brain_state(brain_state)\n                \n                # Manually load bindings if not using custom brain\n                # This handles standard saves where bindings are stored in 'brain_state'\n                if not custom_brain_loaded and hasattr(self, 'neuron_output_monitor'):\n                    self.neuron_output_monitor.load_bindings_from_brain(brain_state)\n                    print(f\"     + Synced standard output bindings to monitor\")\n\n            # Load memories\n            self.squid.memory_manager.short_term_memory = data.get('ShortTerm', [])\n            self.squid.memory_manager.long_term_memory = data.get('LongTerm', [])\n\n            # Load decorations\n            decorations_data = game_state.get('decorations', [])\n            self.user_interface.load_decorations_data(decorations_data)\n\n            # Load logic state\n            logic_data = game_state.get('tamagotchi_logic', {})\n            self.cleanliness_threshold_time = logic_data.get('cleanliness_threshold_time', 0)\n            self.hunger_threshold_time = logic_data.get('hunger_threshold_time', 0)\n            self.last_clean_time = logic_data.get('last_clean_time', 0)\n            self.points = logic_data.get('points', 0)\n            \n            # Restore achievements if available\n            if 'achievements' in data:\n                self._restore_achievements_data(data['achievements'])\n\n            if hasattr(self, 'statistics_window'):\n                self.statistics_window.set_score(self.points)\n                self.statistics_window.update_statistics()\n\n            print(f\"[Load] Successfully loaded: {os.path.basename(latest)}\")\n            return True\n\n        except Exception as e:\n            print(f\"[Load] Failed to load game: {e}\")\n            import traceback\n            traceback.print_exc()\n            return False\n\n\n    def save_game(self, is_autosave=False):\n        \"\"\"\n        Save the complete game state including decorations.\n\n        Args:\n            is_autosave: Whether this is an autosave (True) or manual save (False)\n        \"\"\"\n        # Import hashlib for duplicate detection\n        import hashlib\n        import json\n        import uuid\n        import time\n        \n        # Collect squid state\n        squid_data = {\n            'hunger': self.squid.hunger,\n            'sleepiness': self.squid.sleepiness,\n            'happiness': self.squid.happiness,\n            'cleanliness': self.squid.cleanliness,\n            'health': self.squid.health,\n            'is_sick': self.squid.is_sick,\n            'squid_x': self.squid.squid_x,\n            'squid_y': self.squid.squid_y,\n            'satisfaction': self.squid.satisfaction,\n            'anxiety': self.squid.anxiety,\n            'curiosity': self.squid.curiosity,\n            'personality': self.squid.personality.value,\n            'uuid': str(self.squid.uuid),  # Convert UUID to string\n            'name': getattr(self.squid, 'name', 'Squid'),  # Save squid name if it exists\n        }\n\n        # Collect statistics as separate file\n        # Primary source: brain_window.statistics_tab (where gameplay updates go)\n        # Fallback: squid.statistics (legacy)\n        tab_stats = {}\n        if hasattr(self, 'brain_window') and hasattr(self.brain_window, 'statistics_tab'):\n            tab_stats = getattr(self.brain_window.statistics_tab, 'statistics', {})\n\n        squid_stats = self.squid.statistics\n\n        # Save using exact keys from StatisticsTab\n        statistics_data = {\n            # Age tracking\n            'total_age_seconds': squid_stats.get_total_age_seconds(),\n            'squid_age_minutes': tab_stats.get('squid_age_minutes', 0),\n\n            # Movement\n            'distance_swam': tab_stats.get('distance_swam', 0),\n\n            # Food consumption\n            'cheese_eaten': tab_stats.get('cheese_eaten', 0),\n            'sushi_eaten': tab_stats.get('sushi_eaten', 0),\n\n            # Poop tracking\n            'poops_created': tab_stats.get('poops_created', 0),\n            'max_poops_cleaned': tab_stats.get('max_poops_cleaned', 0),\n\n            # Interactions\n            'startles_experienced': tab_stats.get('startles_experienced', 0),\n            'ink_clouds_created': tab_stats.get('ink_clouds_created', 0),\n            'times_colour_changed': tab_stats.get('times_colour_changed', 0),\n            'rocks_thrown': tab_stats.get('rocks_thrown', 0),\n            'plants_interacted': tab_stats.get('plants_interacted', 0),\n\n            # Health/Sleep\n            'total_sleep_time': tab_stats.get('total_sleep_time', 0),\n            'sickness_episodes': tab_stats.get('sickness_episodes', 0),\n\n            # Neurogenesis\n            'novelty_neurons_created': tab_stats.get('novelty_neurons_created', 0),\n            'stress_neurons_created': tab_stats.get('stress_neurons_created', 0),\n            'reward_neurons_created': tab_stats.get('reward_neurons_created', 0),\n            'max_neurons_reached': tab_stats.get('max_neurons_reached', 0),\n            'current_neurons': tab_stats.get('current_neurons', 7),\n        }\n\n        # Collect tamagotchi logic state\n        tamagotchi_logic_data = {\n            'cleanliness_threshold_time': self.cleanliness_threshold_time,\n            'hunger_threshold_time': self.hunger_threshold_time,\n            'last_clean_time': self.last_clean_time,\n            'points': self.statistics_window.score\n        }\n\n        # Collect brain state\n        brain_state = self.brain_window.get_brain_state()\n\n        # Collect memory state\n        short_term_memory = self.squid.memory_manager.short_term_memory\n        long_term_memory = self.squid.memory_manager.long_term_memory\n\n        # Collect decoration data from UI\n        decorations_data = self.user_interface.get_decorations_data()\n\n        # Collect achievement data from Achievements plugin (if enabled)\n        achievements_data = None\n        try:\n            if hasattr(self, 'plugin_manager') and 'achievements' in self.plugin_manager.plugins:\n                plugin_info = self.plugin_manager.plugins['achievements']\n                if 'instance' in plugin_info:\n                    instance = plugin_info['instance']\n                    if hasattr(instance, 'get_save_data'):\n                        achievements_data = instance.get_save_data()\n        except Exception as e:\n            # If plugin method fails, log warning and continue without achievements\n            print(f\"[Warning] Could not collect achievement data: {e}\")\n            achievements_data = None\n\n        # Assemble complete save data\n        save_data = {\n            'game_state': {\n                'squid': squid_data,\n                'tamagotchi_logic': tamagotchi_logic_data,\n                'decorations': decorations_data\n            },\n            'brain_state': brain_state,\n            'statistics': statistics_data,\n            'ShortTerm': short_term_memory,\n            'LongTerm': long_term_memory,\n            'custom_brain': get_custom_brain_save_data()\n        }\n\n        # Add achievements data if available (will be saved as achievements.json in the ZIP)\n        if achievements_data is not None:\n            save_data['achievements'] = achievements_data\n            \n        # === DUPLICATE DETECTION LOGIC ===\n        # Create hash of save data to detect duplicates\n        import time\n        \n        # Add timestamp for duplicate detection (not saved to file)\n        save_data_for_hash = save_data.copy()\n        # Remove any volatile fields that shouldn't affect hash\n        save_data_for_hash.pop('achievements', None)\n        \n        # Create a custom JSON encoder that handles UUID objects\n        class CustomJSONEncoder(json.JSONEncoder):\n            def default(self, obj):\n                if isinstance(obj, uuid.UUID):\n                    return str(obj)\n                # Let the base class default method raise the TypeError\n                return json.JSONEncoder.default(self, obj)\n        \n        # Create hash of the save content\n        save_hash = hashlib.md5(\n            json.dumps(save_data_for_hash, sort_keys=True, cls=CustomJSONEncoder).encode()\n        ).hexdigest()\n        \n        # Check if this is identical to the last save\n        if hasattr(self, 'last_save_hash') and save_hash == self.last_save_hash:\n            # Only skip for autosaves, allow manual saves even if identical\n            if is_autosave:\n                print(f\"     Autosave skipped: identical to previous save\")\n                return False\n            else:\n                print(f\"     Manual save detected as identical to previous save, but saving anyway\")\n        \n        # Update last save hash\n        self.last_save_hash = save_hash\n        \n        # Save using SaveManager\n        filepath = self.save_manager.save_game(save_data, is_autosave)\n\n        if filepath:\n            save_type = \"autosaved\" if is_autosave else \"saved\"\n            print(f\"Game {save_type} successfully to {filepath}\")\n            \n            # Update save count if tracking\n            if not hasattr(self, 'save_count'):\n                self.save_count = 0\n            self.save_count += 1\n            \n            # Track autosave count separately\n            if is_autosave:\n                if not hasattr(self, 'autosave_count'):\n                    self.autosave_count = 0\n                self.autosave_count += 1\n                print(f\"     Autosave #{self.autosave_count}\")\n                    \n            return True\n        else:\n            print(f\"Failed to save game\")\n            return False\n        \n    def _restore_achievements_data(self, achievements_data):\n        \"\"\"Restore achievement data to the achievements plugin after loading save.\"\"\"\n        if not achievements_data:\n            return\n        try:\n            if 'achievements' in self.plugin_manager.plugins:\n                plugin_info = self.plugin_manager.plugins['achievements']\n                instance = plugin_info.get('instance')\n                if instance and hasattr(instance, 'load_save_data'):\n                    instance.load_save_data(achievements_data)\n                    unlocked_count = len(achievements_data.get('unlocked', {}))\n                    print(f\"✓ Restored {unlocked_count} achievements\")\n        except Exception as e:\n            print(f\"[Warning] Could not restore achievements: {e}\")\n\n    def _apply_save_data(self, save_data):\n        \"\"\"Apply loaded save data to game state\"\"\"\n        if not save_data:\n            return False\n        \n        # Check custom brain warning\n        if not show_custom_brain_load_warning(self.user_interface, save_data):\n            return False\n        \n        try:\n            # Extract and apply game state\n            game_state = save_data['game_state']\n            squid_data = game_state['squid']\n            \n            self.squid.load_state(squid_data)\n            \n            # Load custom brain\n            custom_brain_data = save_data.get('custom_brain')\n            if custom_brain_data and custom_brain_data.get('is_custom_brain'):\n                success, msg = restore_custom_brain_from_save(save_data, self.brain_window.brain_widget)\n                print(f\"✅ {msg}\" if success else f\"⚠️ {msg}\")\n            \n            # Load brain state\n            brain_state = save_data.get('brain_state', {})\n            self.brain_window.set_brain_state(brain_state)\n            \n            # Load memories\n            self.squid.memory_manager.short_term_memory = save_data.get('ShortTerm', [])\n            self.squid.memory_manager.long_term_memory = save_data.get('LongTerm', [])\n\n            # Sync neurogenesis neuron counts with actual brain state\n            if hasattr(self.brain_window, 'brain_widget') and hasattr(self.brain_window.brain_widget, 'enhanced_neurogenesis'):\n                eng = self.brain_window.brain_widget.enhanced_neurogenesis\n                if hasattr(eng, 'functional_neurons'):\n                    # Count neurons by specialization type\n                    novelty_count = 0\n                    stress_count = 0\n                    reward_count = 0\n                    \n                    for neuron in eng.functional_neurons.values():\n                        if hasattr(neuron, 'specialization'):\n                            if neuron.specialization == 'novelty':\n                                novelty_count += 1\n                            elif neuron.specialization == 'stress':\n                                stress_count += 1\n                            elif neuron.specialization == 'reward':\n                                reward_count += 1\n                    \n                    # Update statistics tab with ground truth values\n                    if hasattr(self.brain_window, 'statistics_tab') and hasattr(self.brain_window.statistics_tab, 'statistics'):\n                        self.brain_window.statistics_tab.statistics['novelty_neurons_created'] = novelty_count\n                        self.brain_window.statistics_tab.statistics['stress_neurons_created'] = stress_count\n                        self.brain_window.statistics_tab.statistics['reward_neurons_created'] = reward_count\n                        self.brain_window.statistics_tab.update_display()\n                        print(f\"✓ Synced neuron counts: {novelty_count} novelty, {stress_count} stress, {reward_count} reward\")\n                    \n                    # Also sync with squid's internal statistics for consistency\n                    if hasattr(self, 'squid') and hasattr(self.squid, 'statistics'):\n                        self.squid.statistics.novelty_neurons_created = novelty_count\n                        self.squid.statistics.stress_neurons_created = stress_count\n                        self.squid.statistics.reward_neurons_created = reward_count\n            \n            # Load decorations\n            decorations_data = game_state.get('decorations', [])\n            self.user_interface.load_decorations_data(decorations_data)\n\n            # Notify vision worker of scene change (decorations loaded)\n            if hasattr(self.squid, 'mark_scene_objects_dirty'):\n                self.squid.mark_scene_objects_dirty()\n            \n            # Load game state\n            logic_data = game_state['tamagotchi_logic']\n            self.cleanliness_threshold_time = logic_data['cleanliness_threshold_time']\n            self.hunger_threshold_time = logic_data['hunger_threshold_time']\n            self.last_clean_time = logic_data['last_clean_time']\n            self.points = logic_data['points']\n            \n            # UPDATE SCORE DISPLAY\n            if hasattr(self, 'statistics_window'):\n                self.statistics_window.set_score(self.points)\n            \n            print(f\"✅ Loaded {len(decorations_data)} decorations\")\n            \n            # UPDATE STATISTICS DISPLAY\n            if hasattr(self, 'statistics_window'):\n                self.statistics_window.update_statistics()\n            \n            self.set_simulation_speed(1)\n            return True\n            \n        except Exception as e:\n            print(f\"❌ Error loading game: {e}\")\n            import traceback\n            traceback.print_exc()\n            return False\n        \n    def _get_pixmap_data(self, item):\n        \"\"\"Get pixmap data from a QGraphicsPixmapItem for serialization\"\"\"\n        if hasattr(item, 'pixmap'):\n            pixmap = item.pixmap()\n            if not pixmap.isNull():\n                # Convert pixmap to base64 encoded PNG data\n                byte_array = QtCore.QByteArray()\n                buffer = QtCore.QBuffer(byte_array)\n                buffer.open(QtCore.QIODevice.WriteOnly)\n                pixmap.save(buffer, \"PNG\")\n                return byte_array.toBase64().data().decode('utf-8')\n        return None\n\n    def start_autosave(self):\n        self.autosave_timer.start(300000)  # 300000 ms = 5 minutes\n\n    def autosave(self):\n        print(\"     Autosaving...\")\n        self.save_game(is_autosave=True)\n\n    # New methods to update the new neurons\n    def update_satisfaction(self):\n        # Update satisfaction based on hunger, happiness, and cleanliness\n        hunger_factor = max(0, 1 - self.squid.hunger / 100)\n        happiness_factor = self.squid.happiness / 100\n        cleanliness_factor = self.squid.cleanliness / 100\n\n        satisfaction_change = (hunger_factor + happiness_factor + cleanliness_factor) / 3\n        satisfaction_change = (satisfaction_change - 0.5) * 2  # Scale to range from -1 to 1\n\n        self.squid.satisfaction += satisfaction_change * self.simulation_speed\n        self.squid.satisfaction = max(0, min(100, self.squid.satisfaction))\n\n    def update_anxiety(self):\n        # Update anxiety based on hunger, cleanliness, and health\n        hunger_factor = self.squid.hunger / 100\n        cleanliness_factor = 1 - self.squid.cleanliness / 100\n        health_factor = 1 - self.squid.health / 100\n\n        if self.squid.personality == Personality.GREEDY:\n            hunger_factor *= 1.5  # Greedy squids get more anxious when hungry\n\n        anxiety_change = (hunger_factor + cleanliness_factor + health_factor) / 3\n\n        if self.squid.personality == Personality.TIMID and self.squid.is_near_plant():\n            anxiety_change *= 0.5  # Timid squids are less anxious near plants\n\n        self.squid.anxiety += anxiety_change * self.simulation_speed\n        self.squid.anxiety = max(0, min(100, self.squid.anxiety))\n\n    def update_curiosity(self):\n        # Update curiosity based on satisfaction and anxiety\n        if self.squid.satisfaction > 70 and self.squid.anxiety < 30:\n            curiosity_change = 0.2 * self.simulation_speed\n        else:\n            curiosity_change = -0.1 * self.simulation_speed\n\n        # Adjust curiosity change based on personality\n        if self.squid.personality == Personality.TIMID:\n            curiosity_change *= 0.5  # Timid squids are less curious\n        elif self.squid.personality == Personality.ADVENTUROUS:\n            curiosity_change *= 1.5  # Adventurous squids are more curious\n\n        self.squid.curiosity += curiosity_change\n        self.squid.curiosity = max(0, min(100, self.squid.curiosity))\n\n\n    def trigger_rock_test(self):\n        \"\"\"Trigger rock test from UI using the interaction manager\"\"\"\n        if not hasattr(self, 'rock_interaction'):\n            self.show_message(\"Rock interaction system not initialized!\")\n            return\n                \n        # Find all valid rocks in the scene using the interaction manager's checker\n        rocks = [item for item in self.user_interface.scene.items() \n                if isinstance(item, ResizablePixmapItem) \n                and self.rock_interaction.is_valid_rock(item)]\n        \n        if not rocks:\n            self.show_message(\"No rocks found in the tank!\")\n            return\n            \n        if not hasattr(self, 'squid'):\n            self.show_message(\"Squid not initialized!\")\n            return\n            \n        # Find nearest rock to squid\n        nearest_rock = min(rocks, key=lambda r: \n            math.hypot(\n                r.sceneBoundingRect().center().x() - self.squid.squid_x,\n                r.sceneBoundingRect().center().y() - self.squid.squid_y\n            )\n        )\n        \n        # Highlight the rock (visual feedback)\n        self.highlight_rock(nearest_rock)\n        \n        # Start the test through the interaction manager\n        self.rock_interaction.start_rock_test(nearest_rock)\n        \n        # Show status message\n        self.show_message(\"Rock test initiated\")\n\n    def is_valid_rock(self, item):\n        \"\"\"Check if an item is a valid rock decoration\"\"\"\n        if not isinstance(item, ResizablePixmapItem):\n            return False\n        # Check if it's a rock based on filename or category\n        if hasattr(item, 'category') and item.category == 'rock':\n            return True\n        if hasattr(item, 'filename') and 'rock' in item.filename.lower():\n            return True\n        return False\n\n    def update_rock_interaction(self):\n        \"\"\"Unified method used for both test and autonomous interactions\"\"\"\n        if not hasattr(self.squid, 'current_rock_target') or not self.squid.current_rock_target:\n            return\n        \n        rock = self.squid.current_rock_target\n        rock_rect = rock.sceneBoundingRect()\n        squid_rect = self.squid.squid_item.sceneBoundingRect()\n        \n        # Calculate precise distance between edges\n        dx = rock_rect.center().x() - squid_rect.center().x()\n        dy = rock_rect.center().y() - squid_rect.center().y()\n        distance = math.hypot(dx, dy)\n        \n        if self.squid.status == \"approaching_rock\":\n            if distance < 40:  # Close enough to pick up\n                if self.squid.pick_up_rock(rock):\n                    self.squid.status = \"carrying_rock\"\n                    self.squid.rock_carry_timer = random.randint(30, 50)  # 3-5 seconds\n                else:\n                    self.squid.current_rock_target = None\n            else:\n                # Move toward rock at normal speed\n                self.squid.move_toward_position(rock_rect.center())\n        \n        elif self.squid.status == \"carrying_rock\":\n            self.squid.rock_carry_timer -= 1\n            if self.squid.rock_carry_timer <= 0:\n                direction = \"left\" if random.random() < 0.5 else \"right\"\n                if self.squid.throw_rock(direction):\n                    self.squid.status = \"roaming\"\n                    self.squid.current_rock_target = None\n\n    def update_rock_test(self):\n        \"\"\"Delegate to interaction manager\"\"\"\n        if hasattr(self, 'rock_interaction'):\n            self.rock_interaction.update_rock_test()\n\n    def update_status_bar(self):\n            \"\"\"Update the status bar with the current plugin state\"\"\"\n            if hasattr(self.user_interface, 'status_bar'):\n                self.user_interface.status_bar.update_plugins_status(self.plugin_manager)\n                \n                # Check for multiplayer plugin specifically\n                if 'MultiplayerPlugin' in self.plugin_manager.enabled_plugins:\n                    # Try to get the plugin instance\n                    for plugin_name, plugin_data in self.plugin_manager.plugins.items():\n                        if plugin_name == 'MultiplayerPlugin' and 'instance' in plugin_data:\n                            plugin = plugin_data['instance']\n                            \n                            # Update network status if plugin has a network node\n                            if hasattr(plugin, 'network_node') and plugin.network_node:\n                                self.user_interface.status_bar.update_network_status(\n                                    plugin.network_node.is_connected,\n                                    plugin.network_node.node_id\n                                )\n                            \n                            # Update peers count\n                            if hasattr(plugin, 'network_node') and plugin.network_node:\n                                peers_count = len(plugin.network_node.known_nodes)\n                                self.user_interface.status_bar.update_peers_count(peers_count)\n\n    def setup_poop_interaction(self):\n        \"\"\"Initialize poop interaction manager\"\"\"\n        from .interactions2 import PoopInteractionManager\n        \n        # Check if config manager has poop config\n        if not hasattr(self.config_manager, 'get_poop_config'):\n            # Create a default poop config if not available\n            def get_poop_config():\n                return {\n                    'min_carry_duration': 3.0,\n                    'max_carry_duration': 9.0,\n                    'pickup_prob': 0.2,\n                    'throw_prob': 0.3,\n                    # Optional additional config parameters\n                    'happiness_penalty': 5,\n                    'anxiety_increase': 10\n                }\n            self.config_manager.get_poop_config = get_poop_config\n        \n        # Initialize poop interaction\n        self.poop_interaction = PoopInteractionManager(\n            squid=self.squid,\n            logic=self,\n            scene=self.user_interface.scene,\n            message_callback=self.show_message,\n            config_manager=self.config_manager\n        )\n\n    def check_poop_interaction(self):\n        \"\"\"Unified method for poop interaction checks\"\"\"\n        if not hasattr(self, 'poop_interaction'):\n            self.setup_poop_interaction()\n        \n        if not hasattr(self.squid, 'current_poop_target'):\n            return\n        \n        poop = self.squid.current_poop_target\n        poop_rect = poop.sceneBoundingRect()\n        squid_rect = self.squid.squid_item.sceneBoundingRect()\n        \n        # Calculate precise distance between edges\n        dx = poop_rect.center().x() - squid_rect.center().x()\n        dy = poop_rect.center().y() - squid_rect.center().y()\n        distance = math.hypot(dx, dy)\n        \n        if self.squid.status == \"approaching_poop\":\n            if distance < 40:  # Close enough to pick up\n                if self.squid.pick_up_poop(poop):\n                    self.squid.status = \"carrying_poop\"\n                    self.squid.poop_carry_timer = random.randint(30, 50)  # 3-5 seconds\n                else:\n                    self.squid.current_poop_target = None\n            else:\n                # Move toward poop at normal speed\n                self.squid.move_toward_position(poop_rect.center())\n        \n        elif self.squid.status == \"carrying_poop\":\n            self.squid.poop_carry_timer -= 1\n            if self.squid.poop_carry_timer <= 0:\n                direction = \"left\" if random.random() < 0.5 else \"right\"\n                if self.squid.throw_poop(direction):\n                    self.squid.status = \"roaming\"\n                    self.squid.current_poop_target = None"
  },
  {
    "path": "src/task_manager.py",
    "content": "from PyQt5.QtCore import Qt\r\nimport time\r\nimport threading\r\nfrom collections import deque\r\nfrom PyQt5.QtCore import QTimer, QMutex, QMutexLocker\r\nfrom PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,\r\n                             QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView)\r\n\r\nclass TaskManagerWindow(QWidget):\r\n    \"\"\"Simple at-a-glance thread and timer monitor.\"\"\"\r\n\r\n    def __init__(self, brain_worker, parent=None):\r\n        super().__init__(parent, flags=Qt.Window)\r\n        self.setWindowTitle(\"Task Monitor\")\r\n        self.resize(500, 300)\r\n        self._brain_worker_ref = brain_worker\r\n        self._parent_ref = parent\r\n\r\n        # Simple UI\r\n        layout = QVBoxLayout(self)\r\n        \r\n        # Thread status (simple table)\r\n        thread_group = QGroupBox(\"Threads\")\r\n        thread_layout = QVBoxLayout(thread_group)\r\n        \r\n        self.thread_table = QTableWidget(2, 3)\r\n        self.thread_table.setHorizontalHeaderLabels([\"Thread\", \"Status\", \"Queue\"])\r\n        self.thread_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)\r\n        self.thread_table.setEditTriggers(QTableWidget.NoEditTriggers)\r\n        thread_layout.addWidget(self.thread_table)\r\n        layout.addWidget(thread_group)\r\n\r\n        # Timers (simple list)\r\n        timer_group = QGroupBox(\"Active Timers\")\r\n        timer_layout = QVBoxLayout(timer_group)\r\n        self.timer_label = QLabel(\"No timers tracked\")\r\n        self.timer_label.setWordWrap(True)\r\n        timer_layout.addWidget(self.timer_label)\r\n        layout.addWidget(timer_group)\r\n\r\n        # Simple refresh timer\r\n        self.refresh_timer = QTimer(self)\r\n        self.refresh_timer.timeout.connect(self._refresh_display)\r\n        self.refresh_timer.start(1000)  # Update every second\r\n\r\n    @property\r\n    def brain_worker(self):\r\n        \"\"\"Get worker from parent if available (fallback for safety)\"\"\"\r\n        if self._parent_ref and hasattr(self._parent_ref, 'brain_worker'):\r\n            return self._parent_ref.brain_worker\r\n        return self._brain_worker_ref\r\n    \r\n    def update_worker_reference(self, new_worker):\r\n        \"\"\"Update the worker reference when it's restarted\"\"\"\r\n        self._brain_worker_ref = new_worker\r\n        print(f\"TaskManager: Updated worker reference to {id(new_worker)}\")\r\n\r\n    def _refresh_display(self):\r\n        \"\"\"Refresh the display with current status.\"\"\"\r\n        # Update threads\r\n        self._update_threads()\r\n        \r\n        # Update timers (if parent has timer info)\r\n        self._update_timers()\r\n\r\n    def _update_threads(self):\r\n        \"\"\"Update thread table using comprehensive health status\"\"\"\r\n        # Main thread\r\n        main_alive = threading.main_thread().is_alive()\r\n        \r\n        # Worker thread\r\n        worker = self.brain_worker\r\n        worker_alive = False\r\n        queue_size = 0\r\n        health_info = {}\r\n\r\n        if worker and hasattr(worker, 'isRunning'):\r\n            worker_alive = worker.isRunning()\r\n            \r\n            # Use worker's built-in health status if available\r\n            if hasattr(worker, 'get_health_status'):\r\n                health_info = worker.get_health_status()\r\n                queue_size = health_info.get('queue_size', '?')\r\n                # Override isRunning with more comprehensive check\r\n                worker_alive = health_info.get('is_healthy', worker_alive)\r\n        \r\n        # Update table\r\n        threads = [\r\n            (\"Main Thread\", \"✅ Running\" if main_alive else \"❌ Stopped\", \"-\"),\r\n            (\"BrainWorker\", \"✅ Healthy\" if worker_alive else \"❌ Dead/Unresponsive\", str(queue_size))\r\n        ]\r\n        \r\n        for row, (name, status, queue) in enumerate(threads):\r\n            self.thread_table.setItem(row, 0, QTableWidgetItem(name))\r\n            self.thread_table.setItem(row, 1, QTableWidgetItem(status))\r\n            self.thread_table.setItem(row, 2, QTableWidgetItem(queue))\r\n\r\n    def _update_timers(self):\r\n        \"\"\"Check for active timers in parent.\"\"\"\r\n        timers = []\r\n        parent = self._parent_ref\r\n        \r\n        if parent and hasattr(parent, 'brain_window'):\r\n            bw = parent.brain_window\r\n            timer_attrs = ['hebbian_timer', 'countdown_timer', 'update_timer', 'neurogenesis_timer']\r\n            \r\n            for attr in timer_attrs:\r\n                if hasattr(bw, attr):\r\n                    timer = getattr(bw, attr)\r\n                    if timer and timer.isActive():\r\n                        timers.append(attr.replace('_timer', ''))\r\n        \r\n        if timers:\r\n            self.timer_label.setText(f\"Active: {', '.join(timers)}\")\r\n        else:\r\n            self.timer_label.setText(\"No active timers\")\r\n\r\n    def closeEvent(self, event):\r\n        \"\"\"Clean up timer on close.\"\"\"\r\n        self.refresh_timer.stop()\r\n        super().closeEvent(event)"
  },
  {
    "path": "src/tutorial.py",
    "content": "# tutorial.py\nfrom PyQt5 import QtCore, QtGui, QtWidgets\nimport logging\nimport random\nfrom .localisation import Localisation\n\nclass TutorialManager:\n    \"\"\"Manages tutorial overlays and sequences\"\"\"\n    \n    def __init__(self, ui_reference, main_window):\n        self.ui = ui_reference\n        self.main_window = main_window\n        self.tutorial_elements = []\n        self.tutorial_timer = None\n        self.current_step = 0\n        self.loc = Localisation.instance()\n        logging.debug(\"TutorialManager initialized with ui_reference and main_window\")\n    \n    def get_tutorial_font_sizes(self, base_title_size=12, base_body_size=11):\n        \"\"\"Determine font sizes for tutorial text, increasing by 2 for ~1920x1080 resolution\"\"\"\n        from .display_scaling import DisplayScaling\n        screen_width = QtWidgets.QApplication.primaryScreen().size().width()\n        \n        if 1800 <= screen_width <= 2000:\n            title_font_size = DisplayScaling.font_size(base_title_size + 2)\n            body_font_size = DisplayScaling.font_size(base_body_size + 2)\n        else:\n            title_font_size = DisplayScaling.font_size(base_title_size)\n            body_font_size = DisplayScaling.font_size(base_body_size)\n        \n        return title_font_size, body_font_size\n    \n    def start_tutorial(self):\n        logging.debug(\"Starting tutorial\")\n        try:\n            if not hasattr(self, 'ui') or not self.ui:\n                logging.error(\"UI reference missing, cannot start tutorial\")\n                return\n                \n            if not hasattr(self.ui, 'scene') or not self.ui.scene:\n                logging.error(\"Scene not initialized, cannot start tutorial\")\n                return\n            \n            if (hasattr(self.ui, 'squid_brain_window') and \n                self.ui.squid_brain_window and \n                hasattr(self.ui.squid_brain_window, 'brain_widget') and\n                self.ui.squid_brain_window.brain_widget):\n                self.ui.squid_brain_window.brain_widget.is_tutorial_mode = True\n                logging.debug(\"Tutorial mode enabled in brain widget\")\n            \n            self.current_step = 0\n            self.show_first_tutorial()\n        except Exception as e:\n            logging.error(f\"Error starting tutorial: {str(e)}\")\n            self.end_tutorial()\n    \n    def show_first_tutorial(self):\n        \"\"\"Show the initial tutorial about basic squid care\"\"\"\n        self.clear_tutorial_elements()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(0, 0, 0, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"⚠️\")\n        title_text.setDefaultTextColor(QtGui.QColor(255, 215, 0))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        # Use localised text\n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step1_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"got_it\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #4CAF50;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 16px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #45a049;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.advance_to_next_step)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n    \n    def show_second_tutorial(self):\n        logging.debug(\"Showing second tutorial (NEURAL NETWORK)\")\n        try:\n            if not self.ui.scene:\n                logging.error(\"UI scene not initialized\")\n                self.end_tutorial()\n                return\n\n            self.clear_tutorial_elements()\n            \n            if (hasattr(self.ui, 'squid_brain_window') and \n                self.ui.squid_brain_window and \n                hasattr(self.ui.squid_brain_window, 'brain_widget') and\n                self.ui.squid_brain_window.brain_widget):\n                self.ui.squid_brain_window.brain_widget.start_tutorial_glow(duration_ms=3000)\n                logging.debug(\"Started tutorial glow on brain widget\")\n                self._flash_brain_window_background(self.ui.squid_brain_window)\n            \n            win_width = self.ui.window_width\n            win_height = self.ui.window_height\n            banner_height = 170\n            banner_y_offset = 40\n            banner_y = win_height - banner_height - banner_y_offset - 100\n\n            banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n            banner.setBrush(QtGui.QColor(25, 25, 112, 230))\n            banner.setPen(QtGui.QPen(QtGui.QColor(135, 206, 250, 150), 1))\n            banner.setZValue(2000)\n            setattr(banner, '_is_tutorial_element', True)\n            self.ui.scene.addItem(banner)\n            self.tutorial_elements.append(banner)\n\n            from .display_scaling import DisplayScaling\n            screen_width = QtWidgets.QApplication.primaryScreen().size().width()\n            if screen_width <= 1920:\n                title_base_size = 14\n                body_base_size = 13\n            else:\n                title_base_size = 12\n                body_base_size = 11\n            title_font_size, body_font_size = self.get_tutorial_font_sizes(title_base_size, body_base_size)\n\n            title_text = QtWidgets.QGraphicsTextItem(\"🧠\")\n            title_text.setDefaultTextColor(QtGui.QColor(135, 206, 250))\n            title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n            title_text.setPos(20, banner_y + 10)\n            title_text.setZValue(2001)\n            setattr(title_text, '_is_tutorial_element', True)\n            self.ui.scene.addItem(title_text)\n            self.tutorial_elements.append(title_text)\n\n            info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step2_text\"))\n            info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n            info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n            info_text.setPos(20, banner_y + 35)\n            info_text.setTextWidth(win_width - 150)\n            info_text.setZValue(2001)\n            setattr(info_text, '_is_tutorial_element', True)\n            self.ui.scene.addItem(info_text)\n            self.tutorial_elements.append(info_text)\n\n            dismiss_button = QtWidgets.QPushButton(self.loc.get(\"next\"))\n            dismiss_button.setStyleSheet(DisplayScaling.scale_css(\"\"\"\n                QPushButton {\n                    background-color: #1E90FF;\n                    color: white;\n                    border: none;\n                    padding: 8px 16px;\n                    font-size: 14px;\n                    border-radius: 4px;\n                }\n                QPushButton:hover {\n                    background-color: #4169E1;\n                }\n            \"\"\"))\n            dismiss_button.clicked.connect(self.advance_to_next_step)\n\n            dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n            dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n            dismiss_proxy.setZValue(2002)\n            setattr(dismiss_proxy, '_is_tutorial_element', True)\n            self.tutorial_elements.append(dismiss_proxy)\n\n            self.start_auto_dismiss_timer(15000)\n        except Exception as e:\n            logging.error(f\"Error in show_second_tutorial: {str(e)}\")\n            self.end_tutorial()\n    \n    def show_neurogenesis_tutorial(self):\n        \"\"\"Show the third tutorial about neurogenesis with example neurons\"\"\"\n        self.clear_tutorial_elements()\n        self.create_tutorial_example_neurons()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(70, 25, 110, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(200, 150, 255, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"🔄\")\n        title_text.setDefaultTextColor(QtGui.QColor(200, 150, 255))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step3_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"next\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #8A2BE2;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #9932CC;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.advance_to_next_step)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n    \n    def show_learning_tutorial(self):\n        \"\"\"Show the fourth tutorial about hebbian learning\"\"\"\n        if (hasattr(self.ui, 'squid_brain_window') and \n            self.ui.squid_brain_window and \n            hasattr(self.ui.squid_brain_window, 'brain_widget')):\n            self.ui.squid_brain_window.brain_widget.perform_hebbian_learning()\n            logging.debug(\"Forced Hebbian learning cycle before learning tutorial step\")\n        \n        if hasattr(self.ui, 'squid_brain_window') and self.ui.squid_brain_window:\n            if hasattr(self.ui.squid_brain_window, 'tabs'):\n                learning_tab_index = -1\n                for i in range(self.ui.squid_brain_window.tabs.count()):\n                    if self.ui.squid_brain_window.tabs.tabText(i) == \"Learning\":\n                        learning_tab_index = i\n                        break\n                \n                if learning_tab_index >= 0:\n                    self.ui.squid_brain_window.tabs.setCurrentIndex(learning_tab_index)\n        \n        self.clear_tutorial_elements()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(0, 100, 0, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(144, 238, 144, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"🧬\")\n        title_text.setDefaultTextColor(QtGui.QColor(144, 238, 144))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step4_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"next\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #228B22;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #32CD32;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.advance_to_next_step)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n    \n    def show_decisions_tutorial(self):\n        \"\"\"Show the fifth tutorial about decision making\"\"\"\n        if hasattr(self.ui, 'squid_brain_window') and self.ui.squid_brain_window:\n            if hasattr(self.ui.squid_brain_window, 'tabs'):\n                decisions_tab_index = -1\n                for i in range(self.ui.squid_brain_window.tabs.count()):\n                    if self.ui.squid_brain_window.tabs.tabText(i) == \"Decisions\":\n                        decisions_tab_index = i\n                        break\n                \n                if decisions_tab_index >= 0:\n                    self.ui.squid_brain_window.tabs.setCurrentIndex(decisions_tab_index)\n        \n        self.clear_tutorial_elements()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(139, 69, 19, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(222, 184, 135, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"🤔\")\n        title_text.setDefaultTextColor(QtGui.QColor(222, 184, 135))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step5_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"next\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #8B4513;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #A0522D;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.advance_to_next_step)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n    \n    def show_decorations_tutorial(self):\n        \"\"\"Show the sixth tutorial about decorations\"\"\"\n        if hasattr(self.ui, 'decoration_window') and self.ui.decoration_window:\n            self.main_window.position_and_show_decoration_window()\n            if hasattr(self.ui, 'decorations_action'):\n                self.ui.decorations_action.setChecked(True)\n        \n        if hasattr(self.ui, 'squid_brain_window') and self.ui.squid_brain_window:\n            if hasattr(self.ui.squid_brain_window, 'tabs'):\n                memory_tab_index = -1\n                for i in range(self.ui.squid_brain_window.tabs.count()):\n                    if self.ui.squid_brain_window.tabs.tabText(i) == \"Memory\":\n                        memory_tab_index = i\n                        break\n                \n                if memory_tab_index >= 0:\n                    self.ui.squid_brain_window.tabs.setCurrentIndex(memory_tab_index)\n        \n        self.clear_tutorial_elements()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(70, 130, 180, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(173, 216, 230, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"🌿 \")\n        title_text.setDefaultTextColor(QtGui.QColor(173, 216, 230))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step6_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"next\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #4682B4;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #5F9EA0;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.advance_to_next_step)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n    \n    def show_final_tutorial(self):\n        \"\"\"Show the final tutorial step with concluding message\"\"\"\n        self.clear_tutorial_elements()\n        \n        win_width = self.ui.window_width\n        win_height = self.ui.window_height\n        \n        banner_height = 170\n        banner_y_offset = 40\n        banner_y = win_height - banner_height - banner_y_offset - 100\n        \n        banner = QtWidgets.QGraphicsRectItem(0, banner_y, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(75, 0, 130, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(147, 112, 219, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.ui.scene.addItem(banner)\n        self.tutorial_elements.append(banner)\n        \n        title_font_size, body_font_size = self.get_tutorial_font_sizes(12, 11)\n        \n        title_text = QtWidgets.QGraphicsTextItem(\"✨\")\n        title_text.setDefaultTextColor(QtGui.QColor(147, 112, 219))\n        title_text.setFont(QtGui.QFont(\"Arial\", title_font_size, QtGui.QFont.Bold))\n        title_text.setPos(20, banner_y + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(title_text)\n        self.tutorial_elements.append(title_text)\n        \n        info_text = QtWidgets.QGraphicsTextItem(self.loc.get(\"tutorial_step7_text\"))\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", body_font_size))\n        info_text.setPos(20, banner_y + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.ui.scene.addItem(info_text)\n        self.tutorial_elements.append(info_text)\n        \n        dismiss_button = QtWidgets.QPushButton(self.loc.get(\"finish\"))\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #9370DB;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #8A2BE2;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.end_tutorial)\n        \n        dismiss_proxy = self.ui.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, banner_y + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_elements.append(dismiss_proxy)\n        \n        self.start_auto_dismiss_timer(15000)\n\n    def check_brain_window_ready(self):\n        if (hasattr(self.ui, 'squid_brain_window') and \n            self.ui.squid_brain_window and \n            hasattr(self.ui.squid_brain_window, 'tabs') and\n            self.ui.squid_brain_window.tabs):\n            self.show_second_tutorial()\n        else:\n            QtCore.QTimer.singleShot(500, self.check_brain_window_ready)\n    \n    def advance_to_next_step(self):\n        logging.debug(f\"Advancing to tutorial step {self.current_step + 1}\")\n        self.cancel_auto_dismiss_timer()\n        self.clear_tutorial_example_neurons()\n        self.clear_tutorial_elements()\n        self.current_step += 1\n\n        if self.current_step == 1:\n            if not hasattr(self.ui, 'squid_brain_window') or not self.ui.squid_brain_window:\n                logging.error(\"squid_brain_window not initialized or None\")\n                self.end_tutorial()\n                return\n\n            try:\n                if not hasattr(self.ui.squid_brain_window, 'brain_widget') or not self.ui.squid_brain_window.brain_widget:\n                    logging.error(\"squid_brain_window.brain_widget not initialized\")\n                    self.end_tutorial()\n                    return\n\n                logging.debug(\"Showing squid_brain_window\")\n                self.ui.squid_brain_window.show()\n                if hasattr(self.ui, 'brain_action'):\n                    self.ui.brain_action.setChecked(True)\n\n                QtCore.QTimer.singleShot(1000, self.check_brain_window_ready)\n            except Exception as e:\n                logging.error(f\"Error showing brain window: {str(e)}\")\n                self.end_tutorial()\n                return\n\n        elif self.current_step == 2:\n            QtCore.QTimer.singleShot(300, self.show_neurogenesis_tutorial)\n        elif self.current_step == 3:\n            QtCore.QTimer.singleShot(300, self.show_learning_tutorial)\n        elif self.current_step == 4:\n            QtCore.QTimer.singleShot(300, self.show_decisions_tutorial)\n        elif self.current_step == 5:\n            QtCore.QTimer.singleShot(300, self.show_decorations_tutorial)\n        elif self.current_step == 6:\n            QtCore.QTimer.singleShot(300, self.show_final_tutorial)\n        else:\n            self.end_tutorial()\n    \n    def _flash_brain_window_background(self, window, flash_colour=\"#00FFFF\", flashes=4, interval_ms=220):\n        \"\"\"Flash the entire background of the brain window with a stark colour.\"\"\"\n        original_style = window.styleSheet()\n        flash_style = f\"background-color: {flash_colour};\"\n        self._flash_state = False\n        self._flash_count = [0]\n        total_toggles = flashes * 2  # on + off per flash\n\n        def _toggle():\n            self._flash_count[0] += 1\n            self._flash_state = not self._flash_state\n            if self._flash_state:\n                window.setStyleSheet(flash_style)\n            else:\n                window.setStyleSheet(original_style)\n            if self._flash_count[0] >= total_toggles:\n                self._bg_flash_timer.stop()\n                window.setStyleSheet(original_style)\n\n        self._bg_flash_timer = QtCore.QTimer()\n        self._bg_flash_timer.timeout.connect(_toggle)\n        self._bg_flash_timer.start(interval_ms)\n\n    def end_tutorial(self):\n        \"\"\"End the tutorial sequence and clean up\"\"\"\n        self.cancel_auto_dismiss_timer()\n        self.clear_tutorial_elements()\n        self.current_step = 0\n        \n        if (hasattr(self.ui, 'squid_brain_window') and \n            self.ui.squid_brain_window and \n            hasattr(self.ui.squid_brain_window, 'brain_widget') and\n            self.ui.squid_brain_window.brain_widget):\n            self.ui.squid_brain_window.brain_widget.is_tutorial_mode = False\n            logging.debug(\"Tutorial mode disabled in brain widget\")\n    \n    def start_auto_dismiss_timer(self, ms_duration):\n        \"\"\"Start a timer to automatically dismiss the current tutorial step\"\"\"\n        self.cancel_auto_dismiss_timer()\n        \n        self.tutorial_timer = QtCore.QTimer()\n        self.tutorial_timer.timeout.connect(self.advance_to_next_step)\n        self.tutorial_timer.setSingleShot(True)\n        self.tutorial_timer.start(ms_duration)\n    \n    def cancel_auto_dismiss_timer(self):\n        \"\"\"Cancel the auto-dismiss timer if active\"\"\"\n        if self.tutorial_timer and self.tutorial_timer.isActive():\n            self.tutorial_timer.stop()\n            self.tutorial_timer = None\n    \n    def clear_tutorial_elements(self):\n        \"\"\"Remove all tutorial elements from the scene\"\"\"\n        self.clear_tutorial_example_neurons()\n        \n        for item in self.tutorial_elements[:]:\n            if item and item.scene():\n                item.scene().removeItem(item)\n        \n        self.tutorial_elements = []\n        self.ui.scene.update()\n\n    def create_tutorial_example_neurons(self):\n        \"\"\"Create 3 temporary example neurons in the brain widget\"\"\"\n        if not hasattr(self.ui, 'squid_brain_window') or not self.ui.squid_brain_window:\n            return\n        \n        brain_widget = self.ui.squid_brain_window.brain_widget\n        \n        tutorial_positions = [(300, 200), (500, 350), (700, 250)]\n        \n        for i, (x, y) in enumerate(tutorial_positions):\n            neuron_name = f'tutorial_neuron_{i+1}'\n            \n            brain_widget.neuron_positions[neuron_name] = (x, y)\n            brain_widget.state[neuron_name] = 80\n            brain_widget.state_colors[neuron_name] = (255, 255, 0)\n            \n            existing_neurons = [n for n in brain_widget.neuron_positions.keys() \n                            if not n.startswith('tutorial_')]\n            if existing_neurons and len(existing_neurons) >= 2:\n                targets = random.sample(existing_neurons, 2)\n                for target in targets:\n                    weight = random.uniform(0.5, 0.8)\n                    brain_widget.weights[(neuron_name, target)] = weight\n                    brain_widget.weights[(target, neuron_name)] = weight * 0.5\n        \n        brain_widget.update()\n\n    def clear_tutorial_example_neurons(self):\n        \"\"\"Remove tutorial example neurons from brain widget\"\"\"\n        if not hasattr(self.ui, 'squid_brain_window') or not self.ui.squid_brain_window:\n            return\n        \n        if not hasattr(self.ui.squid_brain_window, 'brain_widget') or not self.ui.squid_brain_window.brain_widget:\n            return\n        \n        brain_widget = self.ui.squid_brain_window.brain_widget\n        \n        tutorial_neurons = [n for n in list(brain_widget.neuron_positions.keys()) if n.startswith('tutorial_')]\n        \n        for neuron_name in tutorial_neurons:\n            if neuron_name in brain_widget.neuron_positions:\n                del brain_widget.neuron_positions[neuron_name]\n            if neuron_name in brain_widget.state:\n                del brain_widget.state[neuron_name]\n            if hasattr(brain_widget, 'state_colors') and neuron_name in brain_widget.state_colors:\n                del brain_widget.state_colors[neuron_name]\n            \n            keys_to_remove = [k for k in brain_widget.weights.keys() if neuron_name in k]\n            for key in keys_to_remove:\n                del brain_widget.weights[key]\n        \n        brain_widget.update()\n"
  },
  {
    "path": "src/ui.py",
    "content": "# UI Stuff\n\nimport os\nimport json\nimport math\nimport time\nimport random\nimport base64\nimport uuid\nimport traceback\nfrom PyQt5 import QtCore, QtGui, QtWidgets\nfrom PyQt5.QtCore import QObject, pyqtProperty\nfrom PyQt5.QtWidgets import QGraphicsPixmapItem\nfrom .compute_backend import get_backend\nfrom .brain_tool import SquidBrainWindow\nfrom .statistics_window import StatisticsWindow\nfrom .brain_tool import NeuronInspector as EnhancedNeuronInspector\nfrom .plugin_manager_dialog import PluginManagerDialog\nfrom .tutorial import TutorialManager\nfrom .vision import VisionWindow\nfrom .task_manager import TaskManagerWindow\nfrom .laboratory import NeuronLaboratory\nfrom .preferences import PreferencesWindow\nfrom .localisation import Localization\n\nclass ActionButton(QtWidgets.QPushButton):\n    \"\"\"Custom button with hover and press color states\"\"\"\n    def __init__(self, text, hover_color, pressed_color, font_size=16, parent=None):\n        super().__init__(text, parent)\n        self.hover_color = hover_color\n        self.pressed_color = pressed_color\n        self.font_size = font_size\n        self.is_pressed = False\n        self.is_hovered = False\n        self.update_style()\n    \n    def update_style(self):\n        \"\"\"Update button style based on current state\"\"\"\n        if self.is_pressed:\n            bg_color = self.pressed_color\n            text_color = \"white\"\n        elif self.is_hovered:\n            bg_color = self.hover_color\n            text_color = \"black\"\n        else:\n            bg_color = \"white\"\n            text_color = \"black\"\n        \n        self.setStyleSheet(f\"\"\"\n            QPushButton {{\n                background-color: {bg_color};\n                color: {text_color};\n                border: 2px solid black;\n                font-weight: bold;\n                font-size: {self.font_size}px;\n            }}\n        \"\"\")\n    \n    def enterEvent(self, event):\n        self.is_hovered = True\n        self.update_style()\n        super().enterEvent(event)\n    \n    def leaveEvent(self, event):\n        self.is_hovered = False\n        self.update_style()\n        super().leaveEvent(event)\n    \n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.is_pressed = True\n            self.update_style()\n        super().mousePressEvent(event)\n    \n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.is_pressed = False\n            self.update_style()\n        super().mouseReleaseEvent(event)\n\n\nclass DecorationItem(QtWidgets.QLabel):\n    def __init__(self, pixmap, filename):\n        super().__init__()\n        from .display_scaling import DisplayScaling\n        item_size = DisplayScaling.scale(128)\n        self.setPixmap(pixmap.scaled(item_size, item_size, \n                                     QtCore.Qt.KeepAspectRatio, \n                                     QtCore.Qt.SmoothTransformation))\n        self.filename = filename\n        self.setFixedSize(item_size, item_size)\n        self.setAlignment(QtCore.Qt.AlignCenter)\n        self.setToolTip(filename)\n        self.decoration_items = []\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            drag = QtGui.QDrag(self)\n            mime_data = QtCore.QMimeData()\n            mime_data.setUrls([QtCore.QUrl.fromLocalFile(self.filename)])\n            drag.setMimeData(mime_data)\n            drag.setPixmap(self.pixmap())\n            drag.setHotSpot(event.pos() - self.rect().topLeft())\n            drag.exec_(QtCore.Qt.CopyAction)\n\n\nclass ResizablePixmapItem(QtWidgets.QGraphicsPixmapItem):\n    def __init__(self, pixmap=None, filename=None, category=None, parent=None):\n        QtWidgets.QGraphicsPixmapItem.__init__(self, parent)\n        self.original_pixmap = pixmap\n        self.resize_mode = False\n        self.last_mouse_pos = None\n\n        if pixmap:\n            self.setPixmap(pixmap)\n\n        self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable |\n                    QtWidgets.QGraphicsItem.ItemIsSelectable |\n                    QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)\n\n        self.filename = filename\n        self.stat_multipliers = {'happiness': 1}\n        self.category = category if category else 'generic'\n\n        if filename:\n            multipliers, detected_category = self.get_decoration_info()\n            if multipliers:\n                self.stat_multipliers = multipliers\n            if 'rock' in filename.lower():\n                self.category = 'rock'\n            elif 'poop' in filename.lower():\n                self.category = 'poop'\n            else:\n                self.category = detected_category if detected_category else self.category\n\n        self.can_be_picked_up = filename and ('rock' in filename.lower() or 'poop' in filename.lower())\n        self.is_being_carried = False\n        self.original_scale = 1.0\n\n    def boundingRect(self):\n        return super().boundingRect()\n\n    def wheelEvent(self, event):\n        if self.filename and ('rock01' in self.filename.lower() or 'rock02' in self.filename.lower()):\n            return super().wheelEvent(event)\n        if self.isSelected() and self.original_pixmap:\n            from .display_scaling import DisplayScaling\n            delta = event.angleDelta().y() / 120\n            scale_factor = 1.1 if delta > 0 else 0.9\n            current_width = self.pixmap().width()\n            current_height = self.pixmap().height()\n            min_size = DisplayScaling.scale(64)\n            new_width = max(min_size, int(current_width * scale_factor))\n            new_height = max(min_size, int(current_height * scale_factor))\n            scaled_pixmap = self.original_pixmap.scaled(\n                new_width, new_height,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n            self.setPixmap(scaled_pixmap)\n            event.accept()\n        else:\n            super().wheelEvent(event)\n\n    def paint(self, painter, option, widget):\n        option_copy = QtWidgets.QStyleOptionGraphicsItem(option)\n        option_copy.state &= ~QtWidgets.QStyle.State_Selected\n        super().paint(painter, option_copy, widget)\n        \n        if self.isSelected():\n            pixmap = self.pixmap()\n            if pixmap:\n                painter.save()\n                painter.setPen(QtGui.QPen(QtGui.QColor(30, 144, 255), 2))\n                rect = QtCore.QRectF(0, 0, pixmap.width(), pixmap.height())\n                painter.drawRect(rect)\n                painter.restore()\n\n    def mousePressEvent(self, event):\n        pos = event.pos()\n        self.last_mouse_pos = pos\n        self.resize_mode = False\n        super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        current_pos = event.pos()\n        if self.resize_mode and self.original_pixmap and self.last_mouse_pos:\n            delta_x = current_pos.x() - self.last_mouse_pos.x()\n            delta_y = current_pos.y() - self.last_mouse_pos.y()\n            current_width = self.pixmap().width()\n            current_height = self.pixmap().height()\n            new_width = max(30, current_width + delta_x)\n            new_height = max(30, current_height + delta_y)\n            scaled_pixmap = self.original_pixmap.scaled(\n                int(new_width), int(new_height),\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n            self.setPixmap(scaled_pixmap)\n            self.last_mouse_pos = current_pos\n            event.accept()\n        else:\n            super().mouseMoveEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        self.resize_mode = False\n        self.last_mouse_pos = None\n        super().mouseReleaseEvent(event)\n\n    def get_decoration_info(self):\n        try:\n            file_path = os.path.join(os.path.dirname(__file__), 'decoration_stats.json')\n            with open(file_path, 'r') as f:\n                stats = json.load(f)\n            info = stats.get(self.filename, {})\n            stat_multipliers = {k: v for k, v in info.items() if k != 'category'}\n            category = info.get('category', 'plant')\n            return stat_multipliers, category\n        except FileNotFoundError:\n            print(f\"decoration_stats.json not found at {file_path}. Using empty stats.\")\n            return {}, 'plant'\n        except json.JSONDecodeError:\n            print(f\"Error decoding decoration_stats.json at {file_path}. Using empty stats.\")\n            return {}, 'plant'\n\n\nclass DecorationWindow(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent, QtCore.Qt.Window)\n        loc = Localization.instance()\n        self.setWindowTitle(f\"{loc.get('decorations')} (D)\")\n        \n        from .display_scaling import DisplayScaling\n        self.setFixedWidth(DisplayScaling.scale(800))\n        self.decoration_items = []\n        layout = QtWidgets.QVBoxLayout(self)\n        scroll_area = QtWidgets.QScrollArea()\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        layout.addWidget(scroll_area)\n        content_widget = QtWidgets.QWidget()\n        self.grid_layout = QtWidgets.QGridLayout(content_widget)\n        scroll_area.setWidget(content_widget)\n        self.load_decorations()\n\n    def add_decoration_item(self, item):\n        self.decoration_items.append(item)\n\n    def load_decorations(self):\n        decoration_path = \"images/decoration\"\n        items_per_row = 4\n        row, col = 0, 0\n        from .display_scaling import DisplayScaling\n        item_size = DisplayScaling.scale(128)\n        \n        for filename in os.listdir(decoration_path):\n            if filename.endswith(('.png', '.jpg', '.jpeg')):\n                full_path = os.path.join(decoration_path, filename)\n                pixmap = QtGui.QPixmap(full_path)\n                scaled_pixmap = pixmap.scaled(item_size, item_size, \n                                        QtCore.Qt.KeepAspectRatio, \n                                        QtCore.Qt.SmoothTransformation)\n                item = DecorationItem(scaled_pixmap, full_path)\n                self.grid_layout.addWidget(item, row, col)\n                col += 1\n                if col >= items_per_row:\n                    col = 0\n                    row += 1\n        self.setFixedHeight(min((row + 1) * (item_size + DisplayScaling.scale(20)) + DisplayScaling.scale(40), DisplayScaling.scale(650)))\n\n\nclass ComputeBackendOverlay:\n    \"\"\"\n    Small badge drawn in the bottom-right corner of the play scene showing\n    the active compute backend (NumPy or ONNX + provider name).\n\n    Constructed once during Ui.__init__ and stays visible for the whole\n    session.\n\n    Colour coding:\n        NumPy          ->  dark grey  (#1e1e1e) with light grey text\n        ONNX           ->  dark blue  (#0d2137) with cyan text\n        ONNX missing   ->  amber      (#2a1a00) with orange text  (warning)\n    \"\"\"\n\n    _PADDING_X = 10   # horizontal inner padding (pixels, before scaling)\n    _PADDING_Y = 5    # vertical   inner padding\n    _MARGIN    = 8    # gap from window edge\n    _FONT_SIZE = 8    # point size (before DisplayScaling)\n    _Z_VALUE   = 998  # just below the debug watermark at 999\n\n    def __init__(self, scene, window_width: int, window_height: int):\n        from .display_scaling import DisplayScaling\n\n        backend = get_backend()\n        label, bg_color, text_color = self._style_for(backend.name)\n\n        font = QtGui.QFont(\"Courier New\", DisplayScaling.scale(self._FONT_SIZE))\n        font.setBold(True)\n\n        # Measure text so the pill sizes itself to its content\n        fm     = QtGui.QFontMetrics(font)\n        text_w = fm.horizontalAdvance(label)\n        text_h = fm.height()\n        pad_x  = DisplayScaling.scale(self._PADDING_X)\n        pad_y  = DisplayScaling.scale(self._PADDING_Y)\n        margin = DisplayScaling.scale(self._MARGIN)\n\n        pill_w = text_w + pad_x * 2\n        pill_h = text_h + pad_y * 2\n\n        x = window_width  - pill_w - margin\n        y = window_height - pill_h - margin\n\n        # Background pill\n        self._bg = QtWidgets.QGraphicsRectItem(x, y, pill_w, pill_h)\n        self._bg.setBrush(QtGui.QBrush(QtGui.QColor(bg_color)))\n        self._bg.setPen(QtGui.QPen(QtGui.QColor(text_color), 1))\n        self._bg.setOpacity(0.82)\n        self._bg.setZValue(self._Z_VALUE)\n        scene.addItem(self._bg)\n\n        # Text label\n        self._text = QtWidgets.QGraphicsTextItem(label)\n        self._text.setDefaultTextColor(QtGui.QColor(text_color))\n        self._text.setFont(font)\n        self._text.setPos(x + pad_x, y + pad_y - 1)\n        self._text.setZValue(self._Z_VALUE + 1)\n        scene.addItem(self._text)\n\n    @staticmethod\n    def _style_for(backend_name: str):\n        \"\"\"\n        Return (label_str, bg_hex, text_hex) based on the backend name\n        reported by compute_backend.get_backend().name\n        \"\"\"\n        name = backend_name.lower()\n\n        if name.startswith(\"onnx\") and \"unavailable\" not in name:\n            # Extract a short provider name from e.g. \"onnx [DmlExecutionProvider]\"\n            provider = \"\"\n            if \"[\" in backend_name and \"]\" in backend_name:\n                raw = backend_name.split(\"[\")[1].rstrip(\"]\")\n                short = {\n                    \"DmlExecutionProvider\":      \"DirectML\",\n                    \"QNNExecutionProvider\":      \"QNN·HTP\",\n                    \"OpenVINOExecutionProvider\": \"OpenVINO\",\n                    \"CPUExecutionProvider\":      \"CPU\",\n                }\n                provider = short.get(raw, raw.replace(\"ExecutionProvider\", \"\"))\n            label = f\"ONNX · {provider}\" if provider else \"ONNX\"\n            return label, \"#0d2137\", \"#00e5ff\"      # dark blue / cyan\n\n        elif \"unavailable\" in name:\n            # ONNX was requested but onnxruntime is not installed\n            return \"NUMPY (onnx n/a)\", \"#2a1a00\", \"#ffaa00\"   # amber warning\n\n        else:\n            return \"NUMPY\", \"#1e1e1e\", \"#cccccc\"    # dark grey / light grey\n\n    def show_colored_message(self, text, color=\"#FFFFFF\", duration=5000):\n        \"\"\"Show a temporary colored message in the status bar.\"\"\"\n        if hasattr(self, 'status_bar') and self.status_bar:\n            original_style = self.status_bar.styleSheet()\n            self.status_bar.setStyleSheet(f\"color: {color}; background-color: black; font-size: 11px;\")\n            self.status_bar.showMessage(text, duration)\n            QtCore.QTimer.singleShot(duration, lambda: self.status_bar.setStyleSheet(original_style))\n\n\nclass Ui:\n    def __init__(self, window, debug_mode=False):\n        self.window = window\n        self.awarded_decorations = set()\n        self.tamagotchi_logic = None\n        self.debug_mode = debug_mode\n        self.setup_neurogenesis_debug_shortcut()\n        self.setup_decorations_shortcut()\n        \n        screen = QtWidgets.QApplication.primaryScreen()\n        screen_size = screen.size()\n        from .display_scaling import DisplayScaling\n        DisplayScaling.initialize(screen_size.width(), screen_size.height())\n        \n        if screen_size.width() <= 1920:\n            base_width = 1440\n            base_height = 960\n        else:\n            base_width = 1344\n            base_height = 936\n\n        self.window.setMinimumSize(DisplayScaling.scale(base_width), DisplayScaling.scale(base_height))\n        self.window_width = DisplayScaling.scale(base_width)\n        self.window_height = DisplayScaling.scale(base_height)\n        self.window.setWindowTitle(\"Dosidicus\")\n        self.window.resize(self.window_width, self.window_height)\n\n        self.scene = QtWidgets.QGraphicsScene()\n        self.view = QtWidgets.QGraphicsView(self.scene)\n        self.tutorial_manager = TutorialManager(self, window)\n        self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.view.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate)\n        self.view.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)\n        self.window.setCentralWidget(self.view)\n\n        self.setup_ui_elements()\n        self.setup_menu_bar()\n        self.enhanced_neuron_inspector_instance = None\n        self.squid_brain_window = None\n\n        loc = Localization.instance()\n        self.debug_text = QtWidgets.QGraphicsTextItem(loc.get('debug'))\n        self.debug_text.setDefaultTextColor(QtGui.QColor(\"#a9a9a9\"))\n        font = QtGui.QFont()\n        font.setPointSize(DisplayScaling.scale(20))\n        self.debug_text.setFont(font)\n        self.debug_text.setRotation(-90)\n        self.debug_text.setPos(DisplayScaling.scale(75), DisplayScaling.scale(75))\n        self.debug_text.setZValue(999)\n        self.debug_text.setVisible(self.debug_mode)\n        self.scene.addItem(self.debug_text)\n\n        # ── Compute backend badge ─────────────────────────────────────────\n        self._backend_overlay = ComputeBackendOverlay(\n            scene=self.scene,\n            window_width=self.window_width,\n            window_height=self.window_height,\n        )\n        # ─────────────────────────────────────────────────────────────────\n\n        self.decoration_window = DecorationWindow(self.window)\n        self.decoration_window.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool)\n        self.decoration_window.setAttribute(QtCore.Qt.WA_QuitOnClose, False)\n        self.statistics_window = None\n\n        self.view.setAcceptDrops(True)\n        self.view.dragEnterEvent = self.dragEnterEvent\n        self.view.dragMoveEvent = self.dragMoveEvent\n        self.view.dropEvent = self.dropEvent\n        self.view.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.view.keyPressEvent = self.keyPressEvent\n        \n        try:\n            from status_bar_component import StatusBarComponent\n            self.status_bar = StatusBarComponent(self.window)\n        except ImportError:\n            print(\"Status bar component not available, skipping\")\n        self.optimize_animations()\n\n    def setup_neurogenesis_debug_shortcut(self):\n        self.neurogenesis_debug_shortcut = QtWidgets.QShortcut(\n            QtGui.QKeySequence(QtCore.Qt.ALT + QtCore.Qt.Key_N), \n            self.window\n        )\n        self.neurogenesis_debug_shortcut.activated.connect(self.show_neurogenesis_debug)\n\n    def show_neurogenesis_debug(self):\n        if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n            print(\"Brain window not initialized\")\n            return\n        if not hasattr(self, '_neurogenesis_debug_dialog'):\n            self._neurogenesis_debug_dialog = NeurogenesisDebugDialog(\n                self.squid_brain_window.brain_widget, \n                self.window\n            )\n        self._neurogenesis_debug_dialog.update_debug_info()\n        self._neurogenesis_debug_dialog.show()\n        self._neurogenesis_debug_dialog.raise_()\n\n    def show_neuron_laboratory(self):\n        if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n            self.show_message(\"Brain Tool not initialized\")\n            return\n        brain_widget = getattr(self.squid_brain_window, 'brain_widget', None)\n        if brain_widget is None:\n            self.show_message(\"Brain widget not available\")\n            return\n        if not hasattr(self, '_neuron_laboratory') or self._neuron_laboratory is None:\n            self._neuron_laboratory = NeuronLaboratory(brain_widget, self.window)\n        self._neuron_laboratory.show()\n        self._neuron_laboratory.raise_()\n        self._neuron_laboratory.activateWindow()\n\n    # ── Export helpers ────────────────────────────────────────────────────\n\n    def _get_exports_dir(self):\n        exports_dir = os.path.normpath(\n            os.path.join(os.path.dirname(os.path.abspath(__file__)), \"..\", \"exports\")\n        )\n        os.makedirs(exports_dir, exist_ok=True)\n        return exports_dir\n\n    def _exports_timestamp(self):\n        return time.strftime(\"%Y%m%d_%H%M%S\")\n\n    def export_memory(self, mode):\n        \"\"\"Export STM, LTM, or all memories to /exports as JSON. mode: 'stm'|'ltm'|'all'\"\"\"\n        try:\n            squid = None\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n                squid = getattr(self.tamagotchi_logic, 'squid', None)\n            if squid is None:\n                self.show_message(\"No squid loaded – cannot export memories.\")\n                return\n\n            def _get(obj, *names):\n                for n in names:\n                    v = getattr(obj, n, None)\n                    if v is not None:\n                        return v\n                return None\n\n            stm = _get(squid, 'short_term_memory', 'stm', 'short_term', 'recent_memories')\n            ltm = _get(squid, 'long_term_memory',  'ltm', 'long_term',  'memories')\n\n            exports_dir = self._get_exports_dir()\n            ts = self._exports_timestamp()\n\n            def _serialise(obj):\n                try:\n                    return json.loads(json.dumps(obj, default=str))\n                except Exception:\n                    return str(obj)\n\n            if mode in (\"stm\", \"all\") and stm is not None:\n                path = os.path.join(exports_dir, f\"memory_stm_{ts}.json\")\n                with open(path, \"w\", encoding=\"utf-8\") as f:\n                    json.dump(_serialise(stm), f, indent=2)\n                print(f\"[Export] STM saved → {path}\")\n\n            if mode in (\"ltm\", \"all\") and ltm is not None:\n                path = os.path.join(exports_dir, f\"memory_ltm_{ts}.json\")\n                with open(path, \"w\", encoding=\"utf-8\") as f:\n                    json.dump(_serialise(ltm), f, indent=2)\n                print(f\"[Export] LTM saved → {path}\")\n\n            if mode == \"all\":\n                path = os.path.join(exports_dir, f\"memory_all_{ts}.json\")\n                with open(path, \"w\", encoding=\"utf-8\") as f:\n                    json.dump({\"stm\": _serialise(stm), \"ltm\": _serialise(ltm)}, f, indent=2)\n                print(f\"[Export] All memory saved → {path}\")\n\n            label = {\"stm\": \"STM\", \"ltm\": \"LTM\", \"all\": \"All Memory\"}[mode]\n            self.show_message(f\"{label} exported to /exports\")\n\n        except Exception as e:\n            print(f\"[Export] Memory export error: {e}\")\n            traceback.print_exc()\n            self.show_message(f\"Export failed: {e}\")\n\n    def export_weights(self, fmt):\n        \"\"\"Export brain weights to /exports as CSV or TXT. fmt: 'csv'|'txt'\"\"\"\n        try:\n            brain = None\n            if hasattr(self, 'squid_brain_window') and self.squid_brain_window:\n                brain = getattr(self.squid_brain_window, 'brain_widget', None)\n            if brain is None or not hasattr(brain, 'weights'):\n                self.show_message(\"Brain not initialised – cannot export weights.\")\n                return\n\n            weights = brain.weights\n            exports_dir = self._get_exports_dir()\n            ts = self._exports_timestamp()\n            sorted_weights = sorted(weights.items(), key=lambda x: (str(x[0][0]), str(x[0][1])))\n\n            if fmt == \"csv\":\n                path = os.path.join(exports_dir, f\"weights_{ts}.csv\")\n                with open(path, \"w\", encoding=\"utf-8\") as f:\n                    f.write(\"from,to,weight\\n\")\n                    for (src, dst), w in sorted_weights:\n                        f.write(f\"{src},{dst},{w}\\n\")\n            else:\n                path = os.path.join(exports_dir, f\"weights_{ts}.txt\")\n                with open(path, \"w\", encoding=\"utf-8\") as f:\n                    f.write(f\"Weights export – {ts}\\n\")\n                    f.write(f\"{'From':<30} {'To':<30} {'Weight':>12}\\n\")\n                    f.write(\"-\" * 74 + \"\\n\")\n                    for (src, dst), w in sorted_weights:\n                        f.write(f\"{str(src):<30} {str(dst):<30} {w:>12.6f}\\n\")\n\n            print(f\"[Export] Weights ({fmt.upper()}) saved → {path}\")\n            self.show_message(f\"Weights exported as {fmt.upper()} to /exports\")\n\n        except Exception as e:\n            print(f\"[Export] Weights export error: {e}\")\n            traceback.print_exc()\n            self.show_message(f\"Export failed: {e}\")\n\n    def export_neurons(self):\n        \"\"\"Export a list of all neurons to /exports as JSON.\"\"\"\n        try:\n            brain = None\n            if hasattr(self, 'squid_brain_window') and self.squid_brain_window:\n                brain = getattr(self.squid_brain_window, 'brain_widget', None)\n            if brain is None:\n                self.show_message(\"Brain not initialised – cannot export neurons.\")\n                return\n\n            if hasattr(brain, 'neuron_positions'):\n                neuron_names = sorted(brain.neuron_positions.keys())\n            elif hasattr(brain, 'neurons'):\n                neuron_names = sorted(brain.neurons.keys() if isinstance(brain.neurons, dict) else brain.neurons)\n            else:\n                neuron_names = []\n\n            if not neuron_names:\n                self.show_message(\"No neurons found to export.\")\n                return\n\n            exports_dir = self._get_exports_dir()\n            ts = self._exports_timestamp()\n            path = os.path.join(exports_dir, f\"neurons_{ts}.json\")\n            with open(path, \"w\", encoding=\"utf-8\") as f:\n                json.dump({\"exported_at\": ts, \"count\": len(neuron_names), \"neurons\": neuron_names}, f, indent=2)\n\n            print(f\"[Export] Neurons ({len(neuron_names)}) saved → {path}\")\n            self.show_message(f\"{len(neuron_names)} neurons exported to /exports\")\n\n        except Exception as e:\n            print(f\"[Export] Neuron export error: {e}\")\n            traceback.print_exc()\n            self.show_message(f\"Export failed: {e}\")\n\n    # ─────────────────────────────────────────────────────────────────────\n\n    def setup_decorations_shortcut(self):\n        self.decorations_shortcut = QtWidgets.QShortcut(\n            QtGui.QKeySequence(QtCore.Qt.Key_D), \n            self.window\n        )\n        self.decorations_shortcut.activated.connect(self.show_decorations_window)\n\n    def show_decorations_window(self):\n        self.decoration_window.show()\n        self.decoration_window.activateWindow()\n        self.decoration_window.raise_()\n\n    def optimize_animations(self):\n        self.scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)\n        self.view.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)\n\n    def set_tamagotchi_logic(self, logic):\n        self.tamagotchi_logic = logic\n        if hasattr(logic, 'debug_mode'):\n            self.debug_mode = logic.debug_mode\n            self.debug_action.setChecked(self.debug_mode)\n            self.debug_text.setVisible(self.debug_mode)\n        if hasattr(self, 'neurogenesis_action'):\n            self.neurogenesis_action.triggered.connect(self.trigger_neurogenesis)\n        self.create_multiplayer_menu()\n\n    def show_tutorial_overlay(self):\n        self.tutorial_manager.start_tutorial()\n\n    def remove_tutorial_overlay(self):\n        self.tutorial_manager.advance_to_next_step()\n\n    def show_second_tutorial_banner(self):\n        win_width = self.window_width\n        win_height = self.window_height\n        banner_height = 100\n        banner = QtWidgets.QGraphicsRectItem(0, win_height - banner_height, win_width, banner_height)\n        banner.setBrush(QtGui.QColor(25, 25, 112, 230))\n        banner.setPen(QtGui.QPen(QtGui.QColor(135, 206, 250, 150), 1))\n        banner.setZValue(2000)\n        setattr(banner, '_is_tutorial_element', True)\n        self.scene.addItem(banner)\n        title_text = QtWidgets.QGraphicsTextItem(\"🧠 NEURAL NETWORK\")\n        title_text.setDefaultTextColor(QtGui.QColor(135, 206, 250))\n        title_text.setFont(QtGui.QFont(\"Arial\", 12, QtGui.QFont.Bold))\n        title_text.setPos(20, win_height - banner_height + 10)\n        title_text.setZValue(2001)\n        setattr(title_text, '_is_tutorial_element', True)\n        self.scene.addItem(title_text)\n        info_text = QtWidgets.QGraphicsTextItem(\n            \"This is the squid's neural network. His behaviour is driven by his needs (round neurons).\\n\"\n            \"The network adapts and learns as the squid interacts with his environment.\"\n        )\n        info_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        info_text.setFont(QtGui.QFont(\"Arial\", 11))\n        info_text.setPos(20, win_height - banner_height + 35)\n        info_text.setTextWidth(win_width - 150)\n        info_text.setZValue(2001)\n        setattr(info_text, '_is_tutorial_element', True)\n        self.scene.addItem(info_text)\n        dismiss_button = QtWidgets.QPushButton(\"Got it!\")\n        dismiss_button.setStyleSheet(\"\"\"\n            QPushButton {\n                background-color: #1E90FF;\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }\n            QPushButton:hover {\n                background-color: #4169E1;\n            }\n        \"\"\")\n        dismiss_button.clicked.connect(self.close_tutorial_completely)\n        dismiss_proxy = self.scene.addWidget(dismiss_button)\n        dismiss_proxy.setPos(win_width - 120, win_height - banner_height + 35)\n        dismiss_proxy.setZValue(2002)\n        setattr(dismiss_proxy, '_is_tutorial_element', True)\n        self.tutorial_timer = QtCore.QTimer()\n        self.tutorial_timer.timeout.connect(self.close_tutorial_completely)\n        self.tutorial_timer.setSingleShot(True)\n        self.tutorial_timer.start(12000)\n\n    def close_tutorial_completely(self):\n        if hasattr(self, 'tutorial_timer') and self.tutorial_timer.isActive():\n            self.tutorial_timer.stop()\n        for item in self.scene.items():\n            if hasattr(item, '_is_tutorial_element'):\n                self.scene.removeItem(item)\n        self.scene.update()\n\n    def show_experience_buffer(self):\n        \"\"\"Show the Experience Buffer window from the debug menu\"\"\"\n        loc = Localization.instance()\n        \n        # Get brain widget from the brain window\n        if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n            self.show_message(\"Brain window not initialized\")\n            return\n        \n        brain_widget = self.squid_brain_window.brain_widget\n        \n        if not brain_widget:\n            QtWidgets.QMessageBox.warning(self.window, \"Missing Brain\", \"Brain widget not available\")\n            return\n\n        if not hasattr(brain_widget, 'enhanced_neurogenesis') or brain_widget.enhanced_neurogenesis is None:\n            QtWidgets.QMessageBox.warning(self.window, \"No Neurogenesis\", \"Neurogenesis system not initialized\")\n            return\n\n        # Create or show the dialog\n        if not hasattr(self, '_experience_buffer_dialog') or not self._experience_buffer_dialog:\n            from .brain_network_tab import ExperienceBufferDialog\n            self._experience_buffer_dialog = ExperienceBufferDialog(brain_widget, self.window)\n        \n        self._experience_buffer_dialog.refresh_data()\n        self._experience_buffer_dialog.show()\n        self._experience_buffer_dialog.raise_()\n        self._experience_buffer_dialog.activateWindow()\n\n    def open_brain_designer(self):\n        \"\"\"\n        Open the Brain Tool (if closed) and switch it to Designer Mode.\n        Ensures only one instance runs by using the existing window.\n        \"\"\"\n        # 1. Ensure Brain Window is initialized (Lazy Loading)\n        if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n                current_debug_mode = getattr(self.tamagotchi_logic, 'debug_mode', False)\n                \n                # Check if logic already holds a reference\n                if hasattr(self.tamagotchi_logic, 'brain_window') and self.tamagotchi_logic.brain_window:\n                    self.squid_brain_window = self.tamagotchi_logic.brain_window\n                else: \n                    # Create new window\n                    self.squid_brain_window = SquidBrainWindow(\n                        self.tamagotchi_logic, \n                        current_debug_mode, \n                        show_decorations_callback=self.show_decorations_window\n                    )\n                    # Register back to logic\n                    if hasattr(self.tamagotchi_logic, 'set_brain_window'): \n                         self.tamagotchi_logic.set_brain_window(self.squid_brain_window)\n            else:\n                self.show_message(\"Cannot open Designer: Game logic not initialized.\")\n                return\n\n        # 2. Show and Focus the Brain Window\n        if not self.squid_brain_window.isVisible():\n            self.squid_brain_window.show()\n            self.squid_brain_window.raise_()\n            self.squid_brain_window.activateWindow()\n            \n            # Sync the \"Brain Tool\" menu checkmark\n            if hasattr(self, 'brain_action'):\n                self.brain_action.setChecked(True)\n\n        # 3. Switch to Designer Mode\n        # Check if already in designer mode to avoid redundant reloading\n        if hasattr(self.squid_brain_window, 'designer_view') and self.squid_brain_window.designer_view:\n            print(\"Designer mode already active.\")\n            return\n\n        if hasattr(self.squid_brain_window, 'switch_to_designer_mode'):\n            self.squid_brain_window.switch_to_designer_mode()\n            self.show_message(\"Switching to Designer Mode...\")\n        else:\n            self.show_message(\"Error: Brain Tool does not support Designer Mode.\")\n\n    def setup_plugin_menu(self, plugin_manager_instance):\n        loc = Localization.instance()\n        if not hasattr(self, 'plugins_menu'):\n            self.plugins_menu = self.menu_bar.addMenu(loc.get('plugins'))\n        if not hasattr(self, 'plugin_manager_action') or not self.plugin_manager_action:\n            self.plugin_manager_action = QtWidgets.QAction('Plugin Manager', self.window)\n            self.plugins_menu.addAction(self.plugin_manager_action)\n            self.plugins_menu.addSeparator()\n        if plugin_manager_instance:\n             self.apply_plugin_menu_registrations(plugin_manager_instance)\n\n    def create_multiplayer_menu(self):\n        if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n            self.setup_plugin_menu(self.tamagotchi_logic.plugin_manager)\n        else:\n            print(\"WARNING: create_multiplayer_menu called but plugin_manager is not available\")\n\n    def apply_plugin_menu_registrations(self, plugin_manager):\n        loc = Localization.instance()\n        if not hasattr(self, 'plugins_menu') or not self.plugins_menu:\n            self.plugins_menu = self.menu_bar.addMenu(loc.get('plugins'))\n            self.plugin_manager_action = QtWidgets.QAction('Plugin Manager', self.window)\n            self.plugins_menu.addAction(self.plugin_manager_action)\n            self.plugins_menu.addSeparator()\n        elif not hasattr(self, 'plugin_manager_action') or \\\n             not self.plugin_manager_action or \\\n             self.plugin_manager_action not in self.plugins_menu.actions():\n            self.plugin_manager_action = QtWidgets.QAction('Plugin Manager', self.window)\n            try:\n                self.plugin_manager_action.triggered.disconnect()\n            except TypeError:\n                pass\n            all_actions = self.plugins_menu.actions()\n            if all_actions and all_actions[0].isSeparator():\n                self.plugins_menu.insertAction(all_actions[0], self.plugin_manager_action)\n            elif all_actions:\n                self.plugins_menu.insertAction(all_actions[0], self.plugin_manager_action)\n            else:\n                self.plugins_menu.addAction(self.plugin_manager_action)\n            current_actions = self.plugins_menu.actions()\n            pm_action_index = current_actions.index(self.plugin_manager_action) if self.plugin_manager_action in current_actions else -1\n            if pm_action_index != -1:\n                if pm_action_index + 1 >= len(current_actions) or not current_actions[pm_action_index + 1].isSeparator():\n                    sep = QtWidgets.QAction(self.window)\n                    sep.setSeparator(True)\n                    self.plugins_menu.insertAction(current_actions[pm_action_index + 1] if pm_action_index + 1 < len(current_actions) else None, sep)\n        try:\n            self.plugin_manager_action.triggered.disconnect()\n        except TypeError:\n            pass\n        if plugin_manager:\n            self.plugin_manager_action.triggered.connect(\n                lambda: self.show_plugin_manager(plugin_manager)\n            )\n            self.plugin_manager_action.setEnabled(True)\n            self.plugin_manager_action.setToolTip(\"Open the plugin manager.\")\n        else:\n            self.plugin_manager_action.setEnabled(False)\n            self.plugin_manager_action.setToolTip(\"Plugin manager is not available.\")\n        \n        actions_to_remove = []\n        separator_after_pm_action_found = False\n        pm_action_ref = self.plugin_manager_action\n        if pm_action_ref and pm_action_ref in self.plugins_menu.actions():\n            pm_action_index = self.plugins_menu.actions().index(pm_action_ref)\n            if pm_action_index + 1 < len(self.plugins_menu.actions()):\n                next_action = self.plugins_menu.actions()[pm_action_index + 1]\n                if next_action.isSeparator():\n                    separator_after_pm_action_found = True\n                    for i in range(pm_action_index + 2, len(self.plugins_menu.actions())):\n                        actions_to_remove.append(self.plugins_menu.actions()[i])\n        if not separator_after_pm_action_found:\n            temp_actions_to_keep = {pm_action_ref}\n            if pm_action_ref and pm_action_ref in self.plugins_menu.actions():\n                 pm_idx = self.plugins_menu.actions().index(pm_action_ref)\n                 if pm_idx + 1 < len(self.plugins_menu.actions()) and self.plugins_menu.actions()[pm_idx+1].isSeparator():\n                     temp_actions_to_keep.add(self.plugins_menu.actions()[pm_idx+1])\n            for action in self.plugins_menu.actions():\n                if action not in temp_actions_to_keep:\n                    actions_to_remove.append(action)\n        for action_to_remove in actions_to_remove:\n            self.plugins_menu.removeAction(action_to_remove)\n        if plugin_manager and hasattr(plugin_manager, 'plugins'):\n            enabled_plugin_keys = plugin_manager.get_enabled_plugins()\n            for plugin_name_key, plugin_data_dict in plugin_manager.plugins.items():\n                if plugin_name_key not in enabled_plugin_keys:\n                    continue\n                plugin_instance = plugin_data_dict.get('instance')\n                original_name = plugin_data_dict.get('original_name', plugin_name_key.capitalize())\n                if plugin_instance and hasattr(plugin_instance, 'register_menu_actions'):\n                    plugin_submenu = self.plugins_menu.addMenu(original_name)\n                    try:\n                        plugin_instance.register_menu_actions(self.window, plugin_submenu)\n                    except Exception as e:\n                        print(f\"Error calling register_menu_actions for {original_name}: {e}\")\n        print(\"Applied all plugin menu registrations\")\n\n    def toggle_plugin(self, plugin_name, enable_flag):\n        if hasattr(self, 'tamagotchi_logic') and hasattr(self.tamagotchi_logic, 'plugin_manager'):\n            plugin_mgr = self.tamagotchi_logic.plugin_manager\n            if plugin_name not in plugin_mgr.plugins:\n                print(f\"WARNING:UI: Attempted to toggle plugin '{plugin_name}', but it's not loaded/found in plugin_mgr.plugins.\")\n                return\n            success = False\n            if enable_flag:\n                print(f\"INFO:UI: Requesting PluginManager to enable plugin '{plugin_name}'.\")\n                success = plugin_mgr.enable_plugin(plugin_name)\n                if success:\n                    print(f\"INFO:UI: PluginManager reported success enabling '{plugin_name}'.\")\n                else:\n                    print(f\"WARNING:UI: PluginManager reported failure enabling '{plugin_name}'.\")\n            else:\n                print(f\"INFO:UI: Requesting PluginManager to disable plugin '{plugin_name}'.\")\n                success = plugin_mgr.disable_plugin(plugin_name)\n                if success:\n                    print(f\"INFO:UI: PluginManager reported success disabling '{plugin_name}'.\")\n                else:\n                    print(f\"WARNING:UI: PluginManager reported failure disabling '{plugin_name}'.\")\n            if hasattr(self, 'setup_plugin_menu') and callable(self.setup_plugin_menu):\n                self.setup_plugin_menu(plugin_mgr)\n            else:\n                print(\"WARNING:UI: setup_plugin_menu method not found, cannot refresh UI after toggle.\")\n        else:\n            print(\"WARNING:UI: Cannot toggle plugin - TamagotchiLogic or PluginManager not available.\")\n\n    def show_plugin_manager(self, plugin_manager):\n        dialog = PluginManagerDialog(plugin_manager, self.window)\n        dialog.exec_()\n        self.setup_plugin_menu(plugin_manager)\n\n    def setup_ui_elements(self):\n        loc = Localization.instance()\n        self.rect_item = self.scene.addRect(50, 50, self.window_width - 100, self.window_height - 100,\n                                        QtGui.QPen(QtGui.QColor(0, 0, 0)), QtGui.QBrush(QtGui.QColor(255, 255, 255)))\n        self.cleanliness_overlay = self.scene.addRect(50, 50, self.window_width - 100, self.window_height - 100,\n                                                    QtGui.QPen(QtCore.Qt.NoPen), QtGui.QBrush(QtGui.QColor(139, 69, 19, 0)))\n        \n        self.dirty_text_items = []\n        self.dirty_text_target_count = 0\n\n        self.feeding_message = QtWidgets.QGraphicsTextItem(loc.get(\"feed_msg\"))\n        self.feeding_message.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        self.feeding_message.setFont(QtGui.QFont(\"Arial\", 10, QtGui.QFont.Bold))\n        self.feeding_message.setPos(0, self.window_height - 75)\n        self.feeding_message.setTextWidth(self.window_width)\n        self.feeding_message.setHtml(f'<div style=\"text-align: center;\">{loc.get(\"feed_msg\")}</div>')\n        self.feeding_message.setOpacity(0)\n        self.scene.addItem(self.feeding_message)\n\n        self.points_label = QtWidgets.QGraphicsTextItem(loc.get(\"points\") + \":\")\n        self.points_label.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        self.points_label.setFont(QtGui.QFont(\"Arial\", 12))\n        self.points_label.setPos(self.window_width - 255, 10)\n        self.points_label.setZValue(2)\n        self.scene.addItem(self.points_label)\n\n        self.points_value_label = QtWidgets.QGraphicsTextItem(\"0\")\n        self.points_value_label.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        self.points_value_label.setFont(QtGui.QFont(\"Arial\", 12, QtGui.QFont.Bold))\n        self.points_value_label.setPos(self.window_width - 95, 10)\n        self.points_value_label.setZValue(2)\n        self.scene.addItem(self.points_value_label)\n\n        self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.view.setSceneRect(0, 0, self.window_width, self.window_height)\n        self.original_view_wheel_event = self.view.wheelEvent\n        self.view.wheelEvent = self.custom_wheel_event\n\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic is not None:\n            self.debug_text.setVisible(getattr(self.tamagotchi_logic, 'debug_mode', False))\n        \n        self.create_action_buttons()\n\n    def custom_wheel_event(self, event):\n        selected_items = self.scene.selectedItems()\n        resizable_selected = [item for item in selected_items if \n                            isinstance(item, ResizablePixmapItem)]\n        if resizable_selected:\n            for item in resizable_selected:\n                try:\n                    item.wheelEvent(event)\n                except Exception as e:\n                    print(f\"Error in wheel event handling: {e}\")\n        event.accept()\n\n    def create_action_buttons(self):\n        from .display_scaling import DisplayScaling\n        try:\n            from .config_manager import ConfigManager\n            config = ConfigManager()\n            display_config = config.get_display_config()\n            button_width = display_config['button_width']\n            button_height = display_config['button_height']\n            button_spacing = display_config['button_spacing']\n            button_font_size = display_config['button_font_size']\n        except Exception as e:\n            print(f\"Warning: Could not load display config, using defaults: {e}\")\n            button_width = 140\n            button_height = 50\n            button_spacing = 20\n            button_font_size = 16\n        \n        button_width = DisplayScaling.scale(button_width)\n        button_height = DisplayScaling.scale(button_height)\n        button_spacing = DisplayScaling.scale(button_spacing)\n        button_font_size = DisplayScaling.font_size(button_font_size)\n        \n        total_width = (button_width * 3) + (button_spacing * 2)\n        start_x = (self.window_width - total_width) / 2\n        y_pos = DisplayScaling.scale(15)\n        \n        loc = Localization.instance()\n        button_configs = [\n            (loc.get(\"feed_btn\"), \"#C8E6C9\", \"#4CAF50\", \"feed_action\"),\n            (loc.get(\"clean_btn\"), \"#B3E5FC\", \"#2196F3\", \"clean_action\"),\n            (loc.get(\"medicine_btn\"), \"#FFCDD2\", \"#F44336\", \"medicine_action\")\n        ]\n        \n        self.action_buttons = []\n        for i, (text, hover_color, pressed_color, action_name) in enumerate(button_configs):\n            x_pos = start_x + (i * (button_width + button_spacing))\n            button = ActionButton(text, hover_color, pressed_color, button_font_size)\n            button.setFixedSize(button_width, button_height)\n            button.action_name = action_name\n            proxy = self.scene.addWidget(button)\n            proxy.setPos(x_pos, y_pos)\n            proxy.setZValue(10000)\n            self.action_buttons.append((button, proxy))\n        \n    def connect_action_buttons(self):\n        if not hasattr(self, 'action_buttons'):\n            return\n        for button, proxy in self.action_buttons:\n            if hasattr(button, 'action_name'):\n                action = getattr(self, button.action_name, None)\n                if action:\n                    button.clicked.connect(action.trigger)\n\n    def update_dirty_text(self, cleanliness):\n        if cleanliness >= 25:\n            if self.dirty_text_items:\n                self.clear_dirty_text()\n            return\n        tank_left = 50\n        tank_right = self.window_width - 50\n        tank_top = 50\n        tank_bottom = self.window_height - 50\n        tank_width = tank_right - tank_left\n        tank_height = tank_bottom - tank_top\n        font_size = 8\n        word_width = font_size * 9\n        word_height = font_size + 14\n        cols = max(1, int(tank_width / word_width))\n        rows = max(1, int(tank_height / word_height))\n        max_words = cols * rows\n        progress = (25.0 - float(cleanliness)) / 25.0\n        target_count = int(progress * max_words)\n        self.dirty_text_target_count = target_count\n        current_count = len(self.dirty_text_items)\n        if current_count < target_count:\n            words_to_add = target_count - current_count\n            for _ in range(words_to_add):\n                word_index = len(self.dirty_text_items)\n                row_from_bottom = word_index // cols\n                col = word_index % cols\n                x = tank_left + (col * word_width) + random.randint(-2, 2)\n                y = tank_bottom - ((row_from_bottom + 1) * word_height) + random.randint(-1, 1)\n                if y < tank_top:\n                    break\n                loc = Localization.instance()\n                dirty_text = QtWidgets.QGraphicsTextItem(loc.get(\"dirty\"))\n                dirty_text.setDefaultTextColor(QtGui.QColor(101, 67, 33))\n                font = QtGui.QFont(\"Arial\", font_size, QtGui.QFont.Bold)\n                dirty_text.setFont(font)\n                dirty_text.setPos(x, y)\n                dirty_text.setZValue(1000)\n                dirty_text.setData(0, \"dirty_text\")\n                self.scene.addItem(dirty_text)\n                self.dirty_text_items.append(dirty_text)\n            self.scene.update()\n        elif current_count > target_count:\n            words_to_remove = current_count - target_count\n            for _ in range(words_to_remove):\n                if self.dirty_text_items:\n                    item = self.dirty_text_items.pop()\n                    self.scene.removeItem(item)\n            self.scene.update()\n    \n    def clear_dirty_text(self):\n        for item in self.dirty_text_items:\n            self.scene.removeItem(item)\n        self.dirty_text_items.clear()\n        self.dirty_text_target_count = 0\n        self.scene.update()\n\n    def check_neurogenesis(self, state):\n        current_time = time.time()\n        \n        if state.get('_debug_forced_neurogenesis', False):\n            new_name = f\"debug_neuron_{int(current_time)}\"\n            if self.neuron_positions:\n                center_x = sum(pos[0] for pos in self.neuron_positions.values()) / len(self.neuron_positions)\n                center_y = sum(pos[1] for pos in self.neuron_positions.values()) / len(self.neuron_positions)\n            else:\n                center_x, center_y = 600, 300\n            self.neuron_positions[new_name] = (\n                center_x + random.randint(-100, 100),\n                center_y + random.randint(-100, 100)\n            )\n            self.state[new_name] = 80\n            self.state_colors[new_name] = (150, 200, 255)\n            for existing in self.neuron_positions:\n                if existing != new_name:\n                    self.weights[(new_name, existing)] = random.uniform(-0.8, 0.8)\n                    self.weights[(existing, new_name)] = random.uniform(-0.8, 0.8)\n            if 'new_neurons' not in self.neurogenesis_data:\n                self.neurogenesis_data['new_neurons'] = []\n            self.neurogenesis_data['new_neurons'].append(new_name)\n            self.neurogenesis_data['last_neuron_time'] = current_time\n            print(f\"DEBUG: Created neuron '{new_name}' at {self.neuron_positions[new_name]}\")\n            print(f\"New connections: {[(k,v) for k,v in self.weights.items() if new_name in k]}\")\n            self.update()\n            return True\n\n        if current_time - self.neurogenesis_data.get('last_neuron_time', 0) > self.neurogenesis_config['cooldown']:\n            created = False\n            if state.get('novelty_exposure', 0) > self.neurogenesis_config['novelty_threshold']:\n                self._create_neuron_internal('novelty', state)\n                created = True\n            if state.get('sustained_stress', 0) > self.neurogenesis_config['stress_threshold']:\n                self._create_neuron_internal('stress', state)\n                created = True\n            if state.get('recent_rewards', 0) > self.neurogenesis_config['reward_threshold']:\n                self._create_neuron_internal('reward', state)\n                created = True\n            return created\n        return False\n    \n    def _create_neuron(self, neuron_type, trigger_data):\n        base_name = {\n            'novelty': 'novel',\n            'stress': 'defense', \n            'reward': 'reward'\n        }[neuron_type]\n        new_name = f\"{base_name}_{len(self.neurogenesis_data['new_neurons'])}\"\n        active_neurons = sorted(\n            [(k, v) for k, v in self.state.items() if isinstance(v, (int, float))],\n            key=lambda x: x[1],\n            reverse=True\n        )\n        if active_neurons:\n            base_x, base_y = self.neuron_positions[active_neurons[0][0]]\n        else:\n            base_x, base_y = 600, 300\n        self.neuron_positions[new_name] = (\n            base_x + random.randint(-50, 50),\n            base_y + random.randint(-50, 50)\n        )\n        self.state[new_name] = 50\n        self.state_colors[new_name] = {\n            'novelty': (255, 255, 150),\n            'stress': (255, 150, 150),\n            'reward': (150, 255, 150)\n        }[neuron_type]\n        default_weights = {\n            'novelty': {'curiosity': 0.6, 'anxiety': -0.4},\n            'stress': {'anxiety': -0.7, 'happiness': 0.3},\n            'reward': {'satisfaction': 0.8, 'happiness': 0.5}\n        }\n        for target, weight in default_weights[neuron_type].items():\n            self.weights[(new_name, target)] = weight\n            self.weights[(target, new_name)] = weight * 0.5\n        self.neurogenesis_data['new_neurons'].append(new_name)\n        self.neurogenesis_data['last_neuron_time'] = time.time()\n        return new_name\n    \n    def trigger_neurogenesis(self):\n        try:\n            if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n                print(\"Brain window not found\")\n                self.show_message(\"Brain window not initialized\")\n                return\n            brain = self.squid_brain_window.brain_widget\n            import time\n            import random\n            prev_neurons = set(brain.neuron_positions.keys())\n            new_name = f\"forced_{int(time.time())}\"\n            if brain.neuron_positions:\n                x_values = [pos[0] for pos in brain.neuron_positions.values()]\n                y_values = [pos[1] for pos in brain.neuron_positions.values()]\n                center_x = sum(x_values) / len(x_values)\n                center_y = sum(y_values) / len(y_values)\n            else:\n                center_x, center_y = 600, 300\n            pos_x = center_x + random.randint(-100, 100)\n            pos_y = center_y + random.randint(-100, 100)\n            print(f\"Creating neuron {new_name} at ({pos_x}, {pos_y})\")\n            brain.neuron_positions[new_name] = (pos_x, pos_y)\n            brain.state[new_name] = 75\n            if hasattr(brain, 'state_colors'):\n                brain.state_colors[new_name] = (150, 200, 255)\n            for existing in list(prev_neurons):\n                if existing in getattr(brain, 'excluded_neurons', []):\n                    continue\n                weight = random.uniform(-0.3, 0.3)\n                brain.weights[(new_name, existing)] = weight\n                brain.weights[(existing, new_name)] = weight * 0.8\n            if hasattr(brain, 'neurogenesis_data'):\n                if 'new_neurons' not in brain.neurogenesis_data:\n                    brain.neurogenesis_data['new_neurons'] = []\n                brain.neurogenesis_data['new_neurons'].append(new_name)\n                brain.neurogenesis_data['last_neuron_time'] = time.time()\n            if hasattr(brain, 'neurogenesis_highlight'):\n                brain.neurogenesis_highlight = {\n                    'neuron': new_name,\n                    'start_time': time.time(),\n                    'duration': 5.0\n                }\n            brain.update()\n            new_neurons = set(brain.neuron_positions.keys()) - prev_neurons\n            if new_neurons:\n                try:\n                    self.show_message(f\"Created neuron: {new_name}\")\n                except:\n                    pass\n                print(f\"Successfully created neuron: {new_name}\")\n            else:\n                self.show_message(\"Neuron creation failed!\")\n                print(\"ERROR: Failed to create neuron\")\n        except Exception as e:\n            import traceback\n            print(f\"NEUROGENESIS FAILURE:\\n{traceback.format_exc()}\")\n            try:\n                self.show_message(f\"Neurogenesis Error: {str(e)}\")\n            except:\n                pass\n\n    def toggle_decoration_window(self, checked):\n        if checked:\n            self.decoration_window.show()\n            self.decoration_window.activateWindow()\n        else:\n            self.decoration_window.hide()\n\n    def show_pause_message(self, is_paused):\n        for item in self.scene.items():\n            if hasattr(item, '_is_pause_message'):\n                self.scene.removeItem(item)\n        win_width = self.window_width\n        win_height = self.window_height\n        from .display_scaling import DisplayScaling\n        loc = Localization.instance()\n\n        if is_paused:\n            background = QtWidgets.QGraphicsRectItem(\n                -DisplayScaling.scale(200),\n                (win_height - DisplayScaling.scale(250)) / 2,\n                win_width + DisplayScaling.scale(400),\n                DisplayScaling.scale(250)\n            )\n            background.setBrush(QtGui.QColor(0, 0, 0, 180))\n            background.setPen(QtGui.QPen(QtCore.Qt.NoPen))\n            background.setZValue(1000)\n            background.original_rect = background.rect()\n            setattr(background, '_is_pause_message', True)\n            self.scene.addItem(background)\n\n            pause_font = QtGui.QFont(\"Arial\", DisplayScaling.font_size(24), QtGui.QFont.Bold)\n            pause_text = self.scene.addText(loc.get(\"paused_msg\"), pause_font)\n            pause_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n            pause_text.setZValue(1002)\n            setattr(pause_text, '_is_pause_message', True)\n            \n            text_rect = pause_text.boundingRect()\n            pause_text_x = (win_width - text_rect.width()) / 2\n            pause_text_y = (win_height - text_rect.height()) / 2 - DisplayScaling.scale(30)\n            pause_text.setPos(pause_text_x, pause_text_y)\n\n            sub_font = QtGui.QFont(\"Arial\", DisplayScaling.font_size(14))\n            sub_text = self.scene.addText(loc.get(\"paused_sub\"), sub_font)\n            sub_text.setDefaultTextColor(QtGui.QColor(200, 200, 200))\n            sub_text.setZValue(1002)\n            setattr(sub_text, '_is_pause_message', True)\n            \n            sub_rect = sub_text.boundingRect()\n            sub_text_x = (win_width - sub_rect.width()) / 2\n            sub_text_y = pause_text_y + text_rect.height() + 10\n            sub_text.setPos(sub_text_x, sub_text_y)\n        \n            self.pause_redraw_timer = QtCore.QTimer()\n            self.pause_redraw_timer.timeout.connect(self._redraw_pause_message)\n            self.pause_redraw_timer.start(500)\n        else:\n            if hasattr(self, 'pause_redraw_timer'):\n                self.pause_redraw_timer.stop()\n\n    def _remove_all_pause_elements(self):\n        if hasattr(self, 'pause_redraw_timer') and self.pause_redraw_timer:\n            self.pause_redraw_timer.stop()\n        for item in self.scene.items():\n            if hasattr(item, '_is_pause_message'):\n                self.scene.removeItem(item)\n\n    def _redraw_pause_message(self):\n        if not hasattr(self, 'tamagotchi_logic') or self.tamagotchi_logic.simulation_speed != 0:\n            if hasattr(self, 'pause_redraw_timer'):\n                self.pause_redraw_timer.stop()\n            return\n        for item in self.scene.items():\n            if hasattr(item, '_is_pause_message'):\n                if isinstance(item, QtWidgets.QGraphicsRectItem) and hasattr(item, 'original_rect'):\n                    item.setRect(item.original_rect)\n                else:\n                    self.scene.removeItem(item)\n        win_width = self.window_width\n        win_height = self.window_height\n        loc = Localization.instance()\n\n        background = QtWidgets.QGraphicsRectItem(\n            -200,\n            (win_height - 250) / 2,\n            win_width + 400,\n            250\n        )\n        background.setBrush(QtGui.QColor(0, 0, 0, 180))\n        background.setPen(QtGui.QPen(QtCore.Qt.NoPen))\n        background.setZValue(1000)\n        background.original_rect = background.rect()\n        setattr(background, '_is_pause_message', True)\n        self.scene.addItem(background)\n\n        pause_text = self.scene.addText(loc.get(\"paused_msg\"), QtGui.QFont(\"Arial\", 24, QtGui.QFont.Bold))\n        pause_text.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        pause_text.setZValue(1002)\n        setattr(pause_text, '_is_pause_message', True)\n        text_rect = pause_text.boundingRect()\n        pause_text_x = (win_width - text_rect.width()) / 2\n        pause_text_y = (win_height - text_rect.height()) / 2 - 30\n        pause_text.setPos(pause_text_x, pause_text_y)\n\n        sub_text = self.scene.addText(loc.get(\"paused_sub\"), QtGui.QFont(\"Arial\", 14))\n        sub_text.setDefaultTextColor(QtGui.QColor(200, 200, 200))\n        sub_text.setZValue(1002)\n        setattr(sub_text, '_is_pause_message', True)\n        sub_rect = sub_text.boundingRect()\n        sub_text_x = (win_width - sub_rect.width()) / 2\n        sub_text_y = pause_text_y + text_rect.height() + 10\n        sub_text.setPos(sub_text_x, sub_text_y)\n        self.scene.update()\n        self.view.viewport().update()\n\n    def handle_window_resize(self, event):\n        self.window_width = event.size().width()\n        self.window_height = event.size().height()\n        self.scene.setSceneRect(0, 0, self.window_width, self.window_height)\n        self.rect_item.setRect(50, 50, self.window_width - 100, self.window_height - 100)\n        self.cleanliness_overlay.setRect(50, 50, self.window_width - 100, self.window_height - 100)\n        self.feeding_message.setPos(0, self.window_height - 75)\n        self.feeding_message.setTextWidth(self.window_width)\n        self.points_label.setPos(self.window_width - 265, 10)\n        self.points_value_label.setPos(self.window_width - 95, 10)\n        self.debug_text.setPos(self.window_width - 60, self.window_height - 60)\n        if hasattr(self, 'current_message_item') and self.current_message_item in self.scene.items():\n            text_height = self.current_message_item.boundingRect().height()\n            message_y = self.window_height - text_height - 20\n            self.current_message_item.setPos(0, message_y)\n            self.current_message_item.setTextWidth(self.window_width)\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'squid') and self.tamagotchi_logic.squid:\n            squid = self.tamagotchi_logic.squid\n            squid.ui.window_width = self.window_width\n            squid.ui.window_height = self.window_height\n            squid.center_x = self.window_width // 2\n            squid.center_y = self.window_height // 2\n            if hasattr(squid, 'update_preferred_vertical_range'):\n                squid.update_preferred_vertical_range()\n            squid.squid_x = max(50, min(squid.squid_x, self.window_width - 50 - squid.squid_width))\n            squid.squid_y = max(50, min(squid.squid_y, self.window_height - 120 - squid.squid_height))\n            squid.squid_item.setPos(squid.squid_x, squid.squid_y)\n            if hasattr(squid, 'update_view_cone'):\n                squid.update_view_cone()\n            if hasattr(squid, 'startled_icon') and squid.startled_icon is not None:\n                squid.update_startled_icon_position()\n            if hasattr(squid, 'sick_icon_item') and squid.sick_icon_item is not None:\n                squid.update_sick_icon_position()\n\n    def show_message(self, message):\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'plugin_manager'):\n                results = self.tamagotchi_logic.plugin_manager.trigger_hook(\n                    \"on_message_display\", \n                    ui=self,\n                    original_message=message\n                )\n                for result in results:\n                    if isinstance(result, str) and result:\n                        message = result\n                        break\n        try:\n            for item in self.scene.items():\n                try:\n                    if hasattr(item, '_is_message_item'):\n                        self.scene.removeItem(item)\n                except Exception:\n                    continue\n        except Exception:\n            pass\n\n        message_item = QtWidgets.QGraphicsTextItem(message)\n        message_item.setDefaultTextColor(QtGui.QColor(255, 255, 255))\n        from .display_scaling import DisplayScaling\n        font = QtGui.QFont(\"Verdana\", DisplayScaling.font_size(12), QtGui.QFont.Bold)\n        message_item.setFont(font)\n        message_item.setTextWidth(self.window_width)\n        text_height = message_item.boundingRect().height()\n        message_y = self.window_height - text_height - 40\n        \n        message_item.setPos(0, message_y)\n        message_item.setHtml(f'<div style=\"text-align: center; background-color: #000000; padding: 0px;\">{message}</div>')\n        message_item.setZValue(999)\n        message_item.setOpacity(1)\n        try:\n            setattr(message_item, '_is_message_item', True)\n        except TypeError:\n            pass\n        self.scene.addItem(message_item)\n        self.current_message_item = message_item\n        self.fade_out_animation = QtCore.QPropertyAnimation(message_item, b\"opacity\")\n        self.fade_out_animation.setDuration(10000)\n        self.fade_out_animation.setStartValue(1.0)\n        self.fade_out_animation.setEndValue(0.0)\n        self.fade_out_animation.finished.connect(lambda: self.scene.removeItem(message_item))\n        self.fade_out_animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)\n\n    def update_points(self, points):\n        self.points_value_label.setPlainText(str(points))\n\n    def get_nearby_decorations(self, x, y, radius=100):\n        nearby_decorations = []\n        for item in self.scene.items():\n            if isinstance(item, ResizablePixmapItem):\n                item_center = item.sceneBoundingRect().center()\n                distance = ((item_center.x() - x) ** 2 + (item_center.y() - y) ** 2) ** 0.5\n                if distance <= radius:\n                    nearby_decorations.append(item)\n        return nearby_decorations\n\n    def move_decoration(self, decoration, dx):\n        current_pos = decoration.pos()\n        new_x = current_pos.x() + dx\n        scene_rect = self.scene.sceneRect()\n        new_x = max(scene_rect.left(), min(new_x, scene_rect.right() - decoration.boundingRect().width()))\n        animation = QtCore.QVariantAnimation()\n        animation.setStartValue(current_pos)\n        animation.setEndValue(QtCore.QPointF(new_x, current_pos.y()))\n        animation.setDuration(300)\n        animation.setEasingCurve(QtCore.QEasingCurve.OutCubic)\n        animation.valueChanged.connect(decoration.setPos)\n        decoration._animation = animation\n        animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)\n\n    def dragEnterEvent(self, event):\n        if event.mimeData().hasUrls():\n            event.accept()\n        else:\n            event.ignore()\n\n    def dragMoveEvent(self, event):\n        if event.mimeData().hasUrls():\n            event.accept()\n        else:\n            event.ignore()\n\n    def dropEvent(self, event):\n        if event.mimeData().hasUrls():\n            url = event.mimeData().urls()[0]\n            file_path = url.toLocalFile()\n            pixmap = QtGui.QPixmap(file_path)\n            if not pixmap.isNull():\n                filename = os.path.basename(file_path)\n                item = ResizablePixmapItem(pixmap, file_path)\n                item.original_pixmap = pixmap\n                if not ('rock01' in file_path.lower() or 'rock02' in file_path.lower()):\n                    from .display_scaling import DisplayScaling\n                    target_max_size = DisplayScaling.scale(192)\n                    orig_width = pixmap.width()\n                    orig_height = pixmap.height()\n                    max_dimension = max(orig_width, orig_height)\n                    if max_dimension > target_max_size:\n                        scale_factor = target_max_size / max_dimension\n                        scaled_pixmap = pixmap.scaled(\n                            int(orig_width * scale_factor),\n                            int(orig_height * scale_factor),\n                            QtCore.Qt.KeepAspectRatio,\n                            QtCore.Qt.SmoothTransformation\n                        )\n                        item.setPixmap(scaled_pixmap)\n                pos = self.view.mapToScene(event.pos())\n                item.setPos(pos)\n                self.scene.addItem(item)\n                self.scene.clearSelection()\n                item.setSelected(True)\n                unique_id = str(uuid.uuid4())\n                item._decoration_id = unique_id\n                if unique_id not in self.awarded_decorations:\n                    self.awarded_decorations.add(unique_id)\n                    if (hasattr(self, 'tamagotchi_logic') and\n                        self.tamagotchi_logic is not None and\n                        hasattr(self.tamagotchi_logic, 'statistics_window') and\n                        self.tamagotchi_logic.statistics_window):\n                            self.tamagotchi_logic.statistics_window.award(10)\n                event.accept()\n\n    def keyPressEvent(self, event):\n        if event.key() == QtCore.Qt.Key_Delete:\n            self.delete_selected_items()\n        elif event.key() == QtCore.Qt.Key_N and event.modifiers() & QtCore.Qt.ShiftModifier:\n            self.direct_create_neuron()\n\n    def show_preferences(self):\n        \"\"\"Show the preferences window\"\"\"\n        if not hasattr(self, '_preferences_window') or not self._preferences_window:\n            self._preferences_window = PreferencesWindow(self.window)\n        self._preferences_window.show()\n        self._preferences_window.raise_()\n        self._preferences_window.activateWindow()\n\n    def direct_create_neuron(self):\n        try:\n            if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n                print(\"ERROR: Brain window not initialized\")\n                return\n            brain = self.squid_brain_window.brain_widget\n            import time\n            import random\n            new_name = f\"forced_{int(time.time())}\"\n            if brain.neuron_positions:\n                x_values = [pos[0] for pos in brain.neuron_positions.values()]\n                y_values = [pos[1] for pos in brain.neuron_positions.values()]\n                center_x = sum(x_values) / len(x_values)\n                center_y = sum(y_values) / len(y_values)\n            else:\n                center_x, center_y = 600, 300\n            pos_x = center_x + random.randint(-100, 100)\n            pos_y = center_y + random.randint(-100, 100)\n            print(f\"Creating neuron {new_name} at ({pos_x}, {pos_y})\")\n            brain.neuron_positions[new_name] = (pos_x, pos_y)\n            brain.state[new_name] = 75\n            if hasattr(brain, 'state_colors'):\n                brain.state_colors[new_name] = (150, 200, 255)\n            excluded = getattr(brain, 'excluded_neurons', [])\n            for existing in list(brain.neuron_positions.keys()):\n                if existing != new_name and existing not in excluded:\n                    weight = random.uniform(-0.3, 0.3)\n                    brain.weights[(new_name, existing)] = weight\n                    brain.weights[(existing, new_name)] = weight * 0.8\n            if hasattr(brain, 'neurogenesis_data'):\n                if 'new_neurons' not in brain.neurogenesis_data:\n                    brain.neurogenesis_data['new_neurons'] = []\n                brain.neurogenesis_data['new_neurons'].append(new_name)\n                brain.neurogenesis_data['last_neuron_time'] = time.time()\n            if hasattr(brain, 'neurogenesis_highlight'):\n                brain.neurogenesis_highlight = {\n                    'neuron': new_name,\n                    'start_time': time.time(),\n                    'duration': 5.0\n                }\n            brain.update()\n            print(f\"Successfully created neuron: {new_name}\")\n        except Exception as e:\n            import traceback\n            print(f\"NEUROGENESIS FAILURE:\\n{traceback.format_exc()}\")\n\n    def delete_selected_items(self):\n        for item in self.scene.selectedItems():\n            if isinstance(item, ResizablePixmapItem):\n                is_poop = (hasattr(item, 'category') and item.category == 'poop') or \\\n                          (hasattr(item, 'filename') and item.filename and 'poop' in item.filename.lower())\n                if is_poop:\n                    if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic and \\\n                       hasattr(self.tamagotchi_logic, 'statistics_window') and self.tamagotchi_logic.statistics_window:\n                        self.tamagotchi_logic.statistics_window.award(5)\n                    self.show_floating_score(item, \"+5\")\n                if hasattr(item, '_decoration_id'):\n                    self.awarded_decorations.discard(item._decoration_id)\n                self.scene.removeItem(item)\n        self.scene.update()\n\n    def show_floating_score(self, item, text, color=QtGui.QColor(50, 205, 50)):\n        item_rect = item.sceneBoundingRect()\n        center_x = item_rect.center().x()\n        top_y = item_rect.top() - 30\n        score_text = QtWidgets.QGraphicsTextItem(text)\n        score_text.setDefaultTextColor(color)\n        from .display_scaling import DisplayScaling\n        font = QtGui.QFont(\"Arial\", DisplayScaling.font_size(16), QtGui.QFont.Bold)\n        score_text.setFont(font)\n        text_width = score_text.boundingRect().width()\n        score_text.setPos(center_x - text_width / 2, top_y)\n        score_text.setZValue(1000)\n        self.scene.addItem(score_text)\n        start_pos = score_text.pos()\n        end_pos = QtCore.QPointF(start_pos.x(), start_pos.y() - 40)\n        pos_animation = QtCore.QVariantAnimation()\n        pos_animation.setStartValue(start_pos)\n        pos_animation.setEndValue(end_pos)\n        pos_animation.setDuration(1000)\n        pos_animation.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n        pos_animation.valueChanged.connect(score_text.setPos)\n        opacity_animation = QtCore.QPropertyAnimation(score_text, b\"opacity\")\n        opacity_animation.setStartValue(1.0)\n        opacity_animation.setEndValue(0.0)\n        opacity_animation.setDuration(1000)\n        opacity_animation.setEasingCurve(QtCore.QEasingCurve.InQuad)\n        def cleanup():\n            try:\n                if score_text.scene():\n                    self.scene.removeItem(score_text)\n            except:\n                pass\n        opacity_animation.finished.connect(cleanup)\n        score_text._pos_animation = pos_animation\n        score_text._opacity_animation = opacity_animation\n        pos_animation.start()\n        opacity_animation.start()\n\n    def setup_menu_bar(self):\n        loc = Localization.instance()\n        self.menu_bar = self.window.menuBar()\n\n        file_menu = self.menu_bar.addMenu(loc.get(\"file\"))\n        self.new_game_action = QtWidgets.QAction(loc.get(\"new_game\"), self.window)\n        self.load_action = QtWidgets.QAction(loc.get(\"load_game\"), self.window)\n        self.save_action = QtWidgets.QAction(loc.get(\"save_game\"), self.window)\n        file_menu.addAction(self.new_game_action)\n        file_menu.addAction(self.load_action)\n        file_menu.addAction(self.save_action)\n\n        view_menu = self.menu_bar.addMenu(loc.get(\"view\"))\n        self.brain_designer_action = QtWidgets.QAction(loc.get(\"brain_designer\"), self.window)\n        self.brain_designer_action.setIcon(self.window.style().standardIcon(QtWidgets.QStyle.SP_FileDialogDetailedView))\n        self.brain_designer_action.triggered.connect(self.open_brain_designer)\n        self.brain_designer_action.setShortcut(\"Ctrl+Shift+B\")\n        self.brain_designer_action.setToolTip(\"Open the visual neural network designer\")\n        view_menu.addAction(self.brain_designer_action)\n        view_menu.addSeparator()\n\n        self.decorations_action = QtWidgets.QAction(loc.get(\"decorations\"), self.window)\n        self.decorations_action.setCheckable(True)\n        self.decorations_action.triggered.connect(self.toggle_decoration_window)\n        view_menu.addAction(self.decorations_action)\n\n        self.stats_window_action = QtWidgets.QAction(loc.get(\"statistics\"), self.window)\n        self.stats_window_action.triggered.connect(self.toggle_statistics_window)\n        view_menu.addAction(self.stats_window_action)\n\n        self.brain_action = QtWidgets.QAction(loc.get(\"brain_tool\"), self.window)\n        self.brain_action.setCheckable(True)\n        self.brain_action.triggered.connect(self.toggle_brain_window)\n        view_menu.addAction(self.brain_action)\n\n        self.neurogenesis_debug_action = QtWidgets.QAction(loc.get(\"neuron_lab\"), self.window)\n        self.neurogenesis_debug_action.triggered.connect(self.show_neuron_laboratory)\n        view_menu.addAction(self.neurogenesis_debug_action)\n\n        self.preferences_action = QtWidgets.QAction(loc.get(\"preferences\"), self.window)\n        self.preferences_action.triggered.connect(self.show_preferences)\n        self.preferences_action.setShortcut(\"Ctrl+P\")\n        view_menu.addAction(self.preferences_action)\n\n        speed_menu = self.menu_bar.addMenu(loc.get(\"speed\"))\n        self.pause_action = QtWidgets.QAction(loc.get(\"pause\"), self.window)\n        self.pause_action.setCheckable(True)\n        self.pause_action.triggered.connect(lambda: self.set_simulation_speed(0))\n        speed_menu.addAction(self.pause_action)\n        \n        self.normal_speed_action = QtWidgets.QAction(loc.get(\"normal_speed\"), self.window)\n        self.normal_speed_action.setCheckable(True)\n        self.normal_speed_action.triggered.connect(lambda: self.set_simulation_speed(1))\n        speed_menu.addAction(self.normal_speed_action)\n        \n        self.fast_speed_action = QtWidgets.QAction(loc.get(\"fast_speed\"), self.window)\n        self.fast_speed_action.setCheckable(True)\n        self.fast_speed_action.triggered.connect(lambda: self.set_simulation_speed(2))\n        speed_menu.addAction(self.fast_speed_action)\n        \n        self.very_fast_speed_action = QtWidgets.QAction(loc.get(\"very_fast\"), self.window)\n        self.very_fast_speed_action.setCheckable(True)\n        self.very_fast_speed_action.triggered.connect(lambda: self.set_simulation_speed(3))\n        speed_menu.addAction(self.very_fast_speed_action)\n\n        self.speed_action_group = QtWidgets.QActionGroup(self.window)\n        self.speed_action_group.addAction(self.pause_action)\n        self.speed_action_group.addAction(self.normal_speed_action)\n        self.speed_action_group.addAction(self.fast_speed_action)\n        self.speed_action_group.addAction(self.very_fast_speed_action)\n\n        actions_menu = self.menu_bar.addMenu(loc.get(\"actions\"))\n        self.feed_action = QtWidgets.QAction(loc.get(\"feed\"), self.window)\n        actions_menu.addAction(self.feed_action)\n        self.clean_action = QtWidgets.QAction(loc.get(\"clean\"), self.window)\n        actions_menu.addAction(self.clean_action)\n        self.medicine_action = QtWidgets.QAction(loc.get(\"medicine\"), self.window)\n        actions_menu.addAction(self.medicine_action)\n\n        debug_menu = self.menu_bar.addMenu(loc.get(\"debug\"))\n        self.debug_action = QtWidgets.QAction(loc.get(\"toggle_debug\"), self.window)\n        self.debug_action.setCheckable(True)\n        self.debug_action.triggered.connect(self.toggle_debug_mode)\n        debug_menu.addAction(self.debug_action)\n\n        self.view_cone_action = QtWidgets.QAction(loc.get(\"toggle_cone\"), self.window)\n        self.view_cone_action.setCheckable(True)\n        self.view_cone_action.setShortcut('V')\n        if hasattr(self.tamagotchi_logic, 'connect_view_cone_action'):\n            self.view_cone_action.triggered.connect(self.tamagotchi_logic.connect_view_cone_action)\n        elif hasattr(self, 'tamagotchi_logic') and hasattr(self.tamagotchi_logic, 'squid') and hasattr(self.tamagotchi_logic.squid, 'toggle_view_cone'):\n             self.view_cone_action.triggered.connect(self.tamagotchi_logic.squid.toggle_view_cone)\n        debug_menu.addAction(self.view_cone_action)\n\n        self.vision_action = QtWidgets.QAction(loc.get(\"squid_vision\"), self.window)\n        self.vision_action.triggered.connect(self.show_vision_window)\n        debug_menu.addAction(self.vision_action)\n        \n        self.neuron_monitor_action = QtWidgets.QAction(\"Neuron Output Monitor\", self.window)\n        self.neuron_monitor_action.triggered.connect(self.show_neuron_output_monitor)\n        debug_menu.addAction(self.neuron_monitor_action)\n\n        self.rock_test_action = QtWidgets.QAction('Rock test (forced)', self.window)\n        self.rock_test_action.triggered.connect(self.trigger_rock_test)\n\n        self.experience_buffer_action = QtWidgets.QAction(\"Neurogenesis Experience Buffer\", self.window)\n        self.experience_buffer_action.triggered.connect(self.show_experience_buffer)\n        self.experience_buffer_action.setToolTip(\"View neurogenesis experience buffer\")\n        debug_menu.addAction(self.experience_buffer_action)\n\n        # ── Export submenu ────────────────────────────────────────────────\n        debug_menu.addSeparator()\n        export_menu = debug_menu.addMenu(\"Export…\")\n\n        memories_submenu = export_menu.addMenu(\"Memories\")\n        self.export_stm_action = QtWidgets.QAction(\"Export STM\", self.window)\n        self.export_stm_action.setToolTip(\"Export short-term memory to /exports folder\")\n        self.export_stm_action.triggered.connect(lambda: self.export_memory(\"stm\"))\n        memories_submenu.addAction(self.export_stm_action)\n\n        self.export_ltm_action = QtWidgets.QAction(\"Export LTM\", self.window)\n        self.export_ltm_action.setToolTip(\"Export long-term memory to /exports folder\")\n        self.export_ltm_action.triggered.connect(lambda: self.export_memory(\"ltm\"))\n        memories_submenu.addAction(self.export_ltm_action)\n\n        self.export_all_memory_action = QtWidgets.QAction(\"Export All\", self.window)\n        self.export_all_memory_action.setToolTip(\"Export all memory to /exports folder\")\n        self.export_all_memory_action.triggered.connect(lambda: self.export_memory(\"all\"))\n        memories_submenu.addAction(self.export_all_memory_action)\n\n        weights_submenu = export_menu.addMenu(\"Weights\")\n        self.export_weights_csv_action = QtWidgets.QAction(\"CSV\", self.window)\n        self.export_weights_csv_action.setToolTip(\"Export weights as CSV to /exports folder\")\n        self.export_weights_csv_action.triggered.connect(lambda: self.export_weights(\"csv\"))\n        weights_submenu.addAction(self.export_weights_csv_action)\n\n        self.export_weights_txt_action = QtWidgets.QAction(\"TXT\", self.window)\n        self.export_weights_txt_action.setToolTip(\"Export weights as TXT to /exports folder\")\n        self.export_weights_txt_action.triggered.connect(lambda: self.export_weights(\"txt\"))\n        weights_submenu.addAction(self.export_weights_txt_action)\n\n        self.export_neurons_action = QtWidgets.QAction(\"Neurons\", self.window)\n        self.export_neurons_action.setToolTip(\"Export neuron list to /exports folder\")\n        self.export_neurons_action.triggered.connect(self.export_neurons)\n        export_menu.addAction(self.export_neurons_action)\n        # ─────────────────────────────────────────────────────────────────\n        \n        self.plugins_menu = self.menu_bar.addMenu(loc.get(\"plugins\"))\n        self.connect_action_buttons()\n\n    def show_neuron_output_monitor(self):\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if hasattr(self.tamagotchi_logic, 'neuron_output_monitor'):\n                # Force the window to be created and shown\n                self.tamagotchi_logic.neuron_output_monitor._ensure_log_window()\n                if self.tamagotchi_logic.neuron_output_monitor.log_window:\n                    self.tamagotchi_logic.neuron_output_monitor.log_window.show()\n                    self.tamagotchi_logic.neuron_output_monitor.log_window.raise_()\n            else:\n                 print(\"Neuron output monitor not initialized\")\n\n    def show_task_manager(self):\n        if not hasattr(self, '_task_manager') or not self._task_manager:\n            worker = getattr(self.tamagotchi_logic, 'brain_worker', None)\n            self._task_manager = TaskManagerWindow(worker, self.window)\n        self._task_manager.show()\n        self._task_manager.raise_()\n\n    def show_vision_window(self):\n        if not hasattr(self, 'vision_window') or self.vision_window is None or not self.vision_window.isVisible():\n            if self.tamagotchi_logic:\n                self.vision_window = VisionWindow(self.tamagotchi_logic, self.window)\n                self.vision_window.show()\n            else:\n                QtWidgets.QMessageBox.warning(self.window, \"Error\", \"Game logic is not yet initialized.\")\n        else:\n            self.vision_window.raise_()\n            self.vision_window.activateWindow()\n\n    def set_simulation_speed(self, speed):\n        if hasattr(self, 'tamagotchi_logic'):\n            is_paused = (speed == 0)\n            self.show_pause_message(is_paused)\n            self.tamagotchi_logic.set_simulation_speed(speed)\n            self.pause_action.setChecked(speed == 0)\n            self.normal_speed_action.setChecked(speed == 1)\n            self.fast_speed_action.setChecked(speed == 2)\n            self.very_fast_speed_action.setChecked(speed == 3)\n            speed_names = [\"Paused\", \"Normal\", \"Fast\", \"Very Fast\"]\n            self.show_message(f\"Simulation speed set to {speed_names[speed]}\")\n        else:\n            self.show_message(\"Game logic not initialized!\")\n\n    def toggle_debug_mode(self):\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic is not None:\n            current_debug = self.tamagotchi_logic.debug_mode\n        else:\n            current_debug = self.debug_mode\n        new_debug_mode = not current_debug\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic is not None:\n            self.tamagotchi_logic._propagating_debug_mode = True\n        self.debug_mode = new_debug_mode\n        if hasattr(self, 'debug_action'):\n            self.debug_action.setChecked(new_debug_mode)\n        if hasattr(self, 'debug_text'):\n            self.debug_text.setVisible(new_debug_mode)\n            self.scene.update()\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic is not None:\n            self.tamagotchi_logic.debug_mode = new_debug_mode\n            if hasattr(self.tamagotchi_logic, 'statistics_window'):\n                self.tamagotchi_logic.statistics_window.set_debug_mode(new_debug_mode)\n        if hasattr(self, 'squid_brain_window'):\n            self.squid_brain_window.debug_mode = new_debug_mode\n            if hasattr(self.squid_brain_window, 'brain_widget'):\n                self.squid_brain_window.brain_widget.debug_mode = new_debug_mode\n                self.squid_brain_window.brain_widget.update()\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic is not None:\n            self.tamagotchi_logic._propagating_debug_mode = False\n        print(f\"Debug mode {'enabled' if new_debug_mode else 'disabled'}\")\n        try:\n            self.show_message(f\"Debug mode {'enabled' if new_debug_mode else 'disabled'}\")\n        except TypeError:\n            pass\n\n    def trigger_rock_test(self):\n        if not hasattr(self.tamagotchi_logic, 'rock_interaction'):\n            self.show_message(\"Rock interaction system not initialized!\")\n            return\n        rocks = [item for item in self.scene.items() \n                if isinstance(item, ResizablePixmapItem) \n                and self.tamagotchi_logic.rock_interaction.is_valid_rock(item)]\n        if not rocks:\n            self.show_message(\"No rocks found in the tank!\")\n            return\n        if not hasattr(self.tamagotchi_logic, 'squid'):\n            self.show_message(\"Squid not initialized!\")\n            return\n        nearest_rock = min(rocks, key=lambda r: \n            math.hypot(\n                r.sceneBoundingRect().center().x() - self.tamagotchi_logic.squid.squid_x,\n                r.sceneBoundingRect().center().y() - self.tamagotchi_logic.squid.squid_y\n            )\n        )\n        self.tamagotchi_logic.rock_interaction.start_rock_test(nearest_rock)\n        self.show_message(\"Rock test initiated\")\n\n    def start_rps_game(self):\n        if hasattr(self, 'tamagotchi_logic'):\n            self.tamagotchi_logic.start_rps_game()\n        else:\n            print(\"TamagotchiLogic not initialized\")\n\n    def test_rock_interaction(self):\n        if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n            if not self.tamagotchi_logic.debug_mode:\n                self.show_message(\"Enable debug mode first!\")\n                return\n            print(\"[DEBUG] Starting rock interaction test from menu...\")\n            self.tamagotchi_logic.test_rock_interaction()\n        else:\n            print(\"TamagotchiLogic not available for rock testing\")\n            self.show_message(\"Game logic not initialized!\")\n\n    def show_neuron_inspector(self):\n        if not self.squid_brain_window:\n            if hasattr(self, 'tamagotchi_logic') and self.tamagotchi_logic:\n                current_debug_mode = getattr(self.tamagotchi_logic, 'debug_mode', False)\n                if hasattr(self.tamagotchi_logic, 'brain_window') and self.tamagotchi_logic.brain_window:\n                    self.squid_brain_window = self.tamagotchi_logic.brain_window\n                else: \n                    self.squid_brain_window = SquidBrainWindow(self.tamagotchi_logic, current_debug_mode, show_decorations_callback=self.show_decorations_window)\n                    if hasattr(self.tamagotchi_logic, 'set_brain_window'): \n                         self.tamagotchi_logic.set_brain_window(self.squid_brain_window)\n            else:\n                self.show_message(\"Brain Tool is not initialized yet.\")\n                return\n        if not self.squid_brain_window.isVisible():\n            self.squid_brain_window.show()\n\n        if not hasattr(self.squid_brain_window, 'brain_widget') or not self.squid_brain_window.brain_widget:\n            self.show_message(\"Brain widget component is not ready.\")\n            return\n\n        if not hasattr(self, 'enhanced_neuron_inspector_instance') or \\\n           not self.enhanced_neuron_inspector_instance or \\\n           not self.enhanced_neuron_inspector_instance.isVisible():\n            self.enhanced_neuron_inspector_instance = EnhancedNeuronInspector(\n                brain_tool_window=self.squid_brain_window,\n                brain_widget_ref=self.squid_brain_window.brain_widget\n            )\n        self.enhanced_neuron_inspector_instance.show()\n        self.enhanced_neuron_inspector_instance.raise_()\n        self.enhanced_neuron_inspector_instance.activateWindow()\n        self.enhanced_neuron_inspector_instance.update_neuron_list()\n        if self.enhanced_neuron_inspector_instance.neuron_combo.count() > 0:\n            self.enhanced_neuron_inspector_instance.update_info()\n\n    def toggle_statistics_window(self):\n        if self.statistics_window is None:\n            self.create_statistics_window()\n        if self.statistics_window is not None:\n            if self.statistics_window.isVisible():\n                self.statistics_window.hide()\n            else:\n                self.statistics_window.show()\n        else:\n            print(\"Failed to create statistics window\")\n\n    def create_statistics_window(self):\n        if hasattr(self, 'tamagotchi_logic'):\n            if not hasattr(self.tamagotchi_logic, 'statistics_window'):\n                self.tamagotchi_logic.statistics_window = StatisticsWindow(self.tamagotchi_logic.squid, show_decorations_callback=self.show_decorations_window)\n            self.statistics_window = self.tamagotchi_logic.statistics_window\n        else:\n            print(\"TamagotchiLogic not initialized\")\n\n    def toggle_brain_window(self, checked):\n        if checked:\n            self.squid_brain_window.show()\n        else:\n            self.squid_brain_window.hide()\n\n    def toggle_designer_mode(self, checked=None):\n        if not hasattr(self, 'squid_brain_window') or not self.squid_brain_window:\n            self.show_message(\"Brain Tool not initialized\")\n            if hasattr(self, 'designer_mode_action'):\n                self.designer_mode_action.setChecked(False)\n            return\n        if not hasattr(self.squid_brain_window, 'designer_controller') or \\\n           not self.squid_brain_window.designer_controller:\n            self.show_message(\"Designer mode not available - integration required\")\n            if hasattr(self, 'designer_mode_action'):\n                self.designer_mode_action.setChecked(False)\n            return\n        if not self.squid_brain_window.isVisible():\n            self.squid_brain_window.show()\n            if hasattr(self, 'brain_action'):\n                self.brain_action.setChecked(True)\n        controller = self.squid_brain_window.designer_controller\n        controller.toggle_designer_mode()\n        is_active = controller.designer_mode_active\n        if hasattr(self, 'designer_mode_action'):\n            self.designer_mode_action.setChecked(is_active)\n        status = \"enabled\" if is_active else \"disabled\"\n        self.show_message(f\"Designer mode {status}\")\n\n    def connect_view_cone_action(self, toggle_function):\n        self.view_cone_action.triggered.connect(toggle_function)\n\n    def get_decorations_data(self):\n        decorations_data = []\n        for item in self.scene.items():\n            if isinstance(item, ResizablePixmapItem):\n                try:\n                    pixmap = item.pixmap()\n                    buffer = QtCore.QBuffer()\n                    buffer.open(QtCore.QIODevice.WriteOnly)\n                    pixmap.save(buffer, \"PNG\")\n                    pixmap_data = base64.b64encode(buffer.data()).decode('utf-8')\n                    pos = item.pos()\n                    pos_tuple = (pos.x(), pos.y())\n                    scale = item.scale()\n                    filename = getattr(item, 'filename', 'unknown')\n                    decoration_dict = {\n                        'pixmap_data': pixmap_data,\n                        'pos': pos_tuple,\n                        'scale': scale,\n                        'filename': filename\n                    }\n                    decorations_data.append(decoration_dict)\n                except Exception as e:\n                    print(f\"Error serializing decoration: {e}\")\n                    continue\n        print(f\"Saved {len(decorations_data)} decoration(s)\")\n        return decorations_data\n\n    def load_decorations_data(self, decorations_data):\n        if not decorations_data:\n            print(\"No decorations to load\")\n            return\n        for item in list(self.scene.items()):\n            if isinstance(item, ResizablePixmapItem):\n                self.scene.removeItem(item)\n        if hasattr(self, 'decoration_window'):\n            self.decoration_window.decoration_items.clear()\n        loaded_count = 0\n        for decoration_dict in decorations_data:\n            try:\n                pixmap_data = decoration_dict['pixmap_data']\n                pixmap = QtGui.QPixmap()\n                pixmap.loadFromData(\n                    QtCore.QByteArray(base64.b64decode(pixmap_data.encode('utf-8')))\n                )\n                filename = decoration_dict.get('filename', 'unknown')\n                item = ResizablePixmapItem(pixmap, filename)\n                pos_tuple = decoration_dict['pos']\n                item.setPos(QtCore.QPointF(pos_tuple[0], pos_tuple[1]))\n                scale = decoration_dict.get('scale', 1.0)\n                item.setScale(scale)\n                self.scene.addItem(item)\n                if hasattr(self, 'decoration_window'):\n                    self.decoration_window.add_decoration_item(item)\n                loaded_count += 1\n            except Exception as e:\n                print(f\"Error loading decoration: {e}\")\n                import traceback\n                traceback.print_exc()\n                continue\n        print(f\"Loaded {loaded_count} decoration(s)\")\n\n    def get_pixmap_data(self, item):\n        pixmap = item.pixmap()\n        buffer = QtCore.QBuffer()\n        buffer.open(QtCore.QIODevice.WriteOnly)\n        pixmap.save(buffer, \"PNG\")\n        pixmap_data = buffer.data().toBase64().data().decode()\n        return pixmap_data\n\n    def closeEvent(self, event):\n        event.ignore()\n        self.hide()\n        if hasattr(self.parent(), 'decorations_action'):\n            self.parent().decorations_action.setChecked(False)\n\n    def get_rock_items(self):\n        return [item for item in self.scene.items() \n                if isinstance(item, ResizablePixmapItem) \n                and getattr(item, 'can_be_picked_up', False)]\n    \n    def highlight_rock(self, rock, highlight=True):\n        effect = QtWidgets.QGraphicsColorizeEffect()\n        effect.setColor(QtGui.QColor(255, 255, 0))\n        effect.setStrength(0.7 if highlight else 0.0)\n        rock.setGraphicsEffect(effect if highlight else None)\n\n    def reset_all_rock_states(self):\n        for rock in self.get_rock_items():\n            rock.is_being_carried = False\n            self.highlight_rock(rock, False)\n\n\n"
  },
  {
    "path": "src/vision.py",
    "content": "from PyQt5 import QtCore, QtGui, QtWidgets\r\nfrom .brain_base_tab import BrainBaseTab\r\nfrom .localisation import loc\r\n\r\nclass VisionWindow(QtWidgets.QDialog):\r\n    def __init__(self, tamagotchi_logic, parent=None):\r\n        super().__init__(parent)\r\n        self.tamagotchi_logic = tamagotchi_logic\r\n        self.setWindowTitle(loc(\"vision_window_title\"))\r\n        self.setMinimumSize(400, 300)\r\n\r\n        # Store original view cone state and enable it for the dialog\r\n        self.original_view_cone_state = self.tamagotchi_logic.squid.view_cone_visible\r\n        if not self.original_view_cone_state:\r\n            self.tamagotchi_logic.squid.toggle_view_cone()\r\n\r\n        self.initialize_ui()\r\n\r\n        # Timer to refresh the view\r\n        self.update_timer = QtCore.QTimer(self)\r\n        self.update_timer.timeout.connect(self.update_view)\r\n        self.update_timer.start(1000) # Update every second\r\n\r\n    def initialize_ui(self):\r\n        \"\"\"Initializes the UI for the Vision window.\"\"\"\r\n        layout = QtWidgets.QVBoxLayout(self) # Set layout on the dialog itself\r\n        layout.setContentsMargins(15, 15, 15, 15)\r\n        layout.setSpacing(10)\r\n\r\n        # Title\r\n        title_layout = QtWidgets.QHBoxLayout()\r\n        title_icon = QtWidgets.QLabel(\"👁️\")\r\n        title_icon.setStyleSheet(\"font-size: 28px;\")\r\n        \r\n        # Reuse existing key \"visible_objects\"\r\n        title_label = QtWidgets.QLabel(loc(\"visible_objects\"))\r\n        title_label.setStyleSheet(\"font-size: 24px; font-weight: bold; color: #343a40;\")\r\n        title_layout.addWidget(title_icon)\r\n        title_layout.addWidget(title_label)\r\n        title_layout.addStretch()\r\n        layout.addLayout(title_layout)\r\n\r\n        # List widget to display visible objects\r\n        self.visible_objects_list = QtWidgets.QListWidget()\r\n        self.visible_objects_list.setStyleSheet(\"\"\"\r\n            QListWidget {\r\n                background-color: #303030;\r\n                border: 1px solid #dee2e6;\r\n                border-radius: 8px;\r\n                padding: 10px;\r\n                font-size: 24px;\r\n                color: white;\r\n            }\r\n            QListWidget::item {\r\n                padding: 5px;\r\n            }\r\n            QListWidget::item:hover {\r\n                background-color: #f1f3f5;\r\n            }\r\n        \"\"\")\r\n        layout.addWidget(self.visible_objects_list)\r\n\r\n        # Create a horizontal layout for the buttons\r\n        button_layout = QtWidgets.QHBoxLayout()\r\n\r\n        # Add the new \"Toggle View Cone\" button\r\n        # Reuse existing key \"toggle_cone\" from Debug menu\r\n        self.toggle_cone_button = QtWidgets.QPushButton(loc(\"toggle_cone\"))\r\n        self.toggle_cone_button.clicked.connect(self.toggle_view_cone)\r\n        button_layout.addWidget(self.toggle_cone_button)\r\n\r\n        # Add a stretch to push the close button to the right\r\n        button_layout.addStretch()\r\n\r\n        # Add the close button\r\n        # Reuse existing key \"close\"\r\n        self.close_button = QtWidgets.QPushButton(loc(\"close\"))\r\n        self.close_button.clicked.connect(self.close)\r\n        button_layout.addWidget(self.close_button)\r\n\r\n        # Add the button layout to the main layout\r\n        layout.addLayout(button_layout)\r\n\r\n    def toggle_view_cone(self):\r\n        \"\"\"Toggles the visibility of the squid's view cone.\"\"\"\r\n        if self.tamagotchi_logic and hasattr(self.tamagotchi_logic, 'squid'):\r\n            self.tamagotchi_logic.squid.toggle_view_cone()\r\n\r\n    def closeEvent(self, event):\r\n        \"\"\"Override the close event to restore the view cone's original state.\"\"\"\r\n        if self.tamagotchi_logic.squid.view_cone_visible != self.original_view_cone_state:\r\n            self.tamagotchi_logic.squid.toggle_view_cone()\r\n        super().closeEvent(event)\r\n\r\n    def update_view(self):\r\n        \"\"\"The method to update the content, formerly update_from_brain_state.\"\"\"\r\n        if not self.tamagotchi_logic or not hasattr(self.tamagotchi_logic, 'squid'):\r\n            self.visible_objects_list.clear()\r\n            self.visible_objects_list.addItem(loc(\"vis_logic_unavailable\"))\r\n            return\r\n\r\n        squid = self.tamagotchi_logic.squid\r\n        \r\n        # Gather all world objects to check for visibility\r\n        all_objects = []\r\n        if hasattr(self.tamagotchi_logic, 'food_items'):\r\n            all_objects.extend(self.tamagotchi_logic.food_items)\r\n        if hasattr(self.tamagotchi_logic, 'poop_items'):\r\n            all_objects.extend(self.tamagotchi_logic.poop_items)\r\n        if hasattr(self.tamagotchi_logic, 'user_interface') and hasattr(self.tamagotchi_logic.user_interface, 'scene'):\r\n             all_decorations = [item for item in self.tamagotchi_logic.user_interface.scene.items() if hasattr(item, 'category')]\r\n             all_objects.extend(all_decorations)\r\n\r\n        # Use the squid's own vision method to get what it can see\r\n        visible_objects = squid.get_visible_objects(all_objects)\r\n\r\n        self.visible_objects_list.clear()\r\n\r\n        if not visible_objects:\r\n            self.visible_objects_list.addItem(loc(\"vis_nothing_in_view\"))\r\n        else:\r\n            for obj in visible_objects:\r\n                # Reuse existing \"unknown\" key\r\n                obj_name = loc(\"unknown\")\r\n                \r\n                # Determine object name based on its attributes\r\n                if hasattr(obj, 'category') and obj.category:\r\n                    # Attempt to translate category (e.g. 'rock', 'plant') using existing keys\r\n                    obj_name = loc(obj.category, default=obj.category.capitalize())\r\n                elif hasattr(obj, 'is_sushi'):\r\n                    # Use existing \"sushi\" and \"food\" keys (Cheese is default food)\r\n                    obj_name = loc(\"sushi\") if obj.is_sushi else loc(\"food\")\r\n                \r\n                distance = squid.distance_to(obj.pos().x(), obj.pos().y())\r\n                \r\n                dist_label = loc(\"vis_distance\")\r\n                list_item_text = f\"{obj_name} ({dist_label}: {distance:.0f})\"\r\n                self.visible_objects_list.addItem(list_item_text)"
  },
  {
    "path": "src/vision_worker.py",
    "content": "\"\"\"\r\nvision_worker.py - Background thread for squid vision calculations\r\n\r\nHandles vision cone calculations, food detection, and object visibility\r\nin a separate thread to prevent blocking the main UI.\r\n\r\nThe worker maintains a cache of visible objects and emits signals when\r\nvisibility changes, allowing the squid to react without polling.\r\n\r\nUsage:\r\n    1. Create VisionWorker with scene reference\r\n    2. Connect to visibility signals (food_visibility_changed, etc.)\r\n    3. Call update_squid_state() periodically with squid position/direction\r\n    4. Worker emits signals when visibility changes\r\n\"\"\"\r\n\r\nimport time\r\nimport math\r\nfrom typing import Dict, List, Tuple, Optional, Set, Any\r\nfrom dataclasses import dataclass, field\r\nfrom PyQt5.QtCore import QThread, pyqtSignal, QMutex, QMutexLocker, QWaitCondition, QPointF\r\n\r\n\r\n@dataclass\r\nclass SquidVisionState:\r\n    \"\"\"Snapshot of squid's vision-relevant state\"\"\"\r\n    # Position\r\n    squid_x: float = 0.0\r\n    squid_y: float = 0.0\r\n    squid_width: float = 100.0\r\n    squid_height: float = 100.0\r\n    \r\n    # View direction\r\n    current_view_angle: float = 0.0\r\n    view_cone_angle: float = math.pi / 2.5  # ~72 degrees\r\n    \r\n    # Window bounds\r\n    window_width: float = 1280.0\r\n    window_height: float = 900.0\r\n    \r\n    # Timestamp\r\n    timestamp: float = field(default_factory=time.time)\r\n\r\n\r\n@dataclass\r\nclass SceneObject:\r\n    \"\"\"Lightweight representation of a scene object for vision calculations\"\"\"\r\n    x: float\r\n    y: float\r\n    width: float = 64.0\r\n    height: float = 64.0\r\n    category: str = 'unknown'\r\n    is_sushi: bool = False\r\n    obj_id: int = 0  # For tracking identity\r\n\r\n\r\n@dataclass\r\nclass VisionResult:\r\n    \"\"\"Results of a vision calculation\"\"\"\r\n    visible_food: List[Tuple[float, float]] = field(default_factory=list)\r\n    visible_plants: List[Tuple[float, float]] = field(default_factory=list)\r\n    visible_rocks: List[Tuple[float, float]] = field(default_factory=list)\r\n    visible_poop: List[Tuple[float, float]] = field(default_factory=list)\r\n    \r\n    # Cached values for quick access\r\n    can_see_food: bool = False\r\n    nearest_food_distance: float = float('inf')\r\n    nearest_food_position: Optional[Tuple[float, float]] = None\r\n    \r\n    # Proximity detection (doesn't require vision cone)\r\n    nearby_plants: List[Tuple[float, float]] = field(default_factory=list)\r\n    plant_proximity_value: float = 0.0\r\n    \r\n    timestamp: float = field(default_factory=time.time)\r\n\r\n\r\nclass VisionWorker(QThread):\r\n    \"\"\"\r\n    Background worker for squid vision calculations.\r\n    \r\n    Signals:\r\n        food_visibility_changed: Emitted when food visibility changes\r\n            - bool: can_see_food\r\n            - list: visible food positions [(x, y), ...]\r\n            \r\n        plant_proximity_changed: Emitted when plant proximity changes\r\n            - float: proximity value (0-100)\r\n            - list: nearby plant positions\r\n            \r\n        visibility_update: Emitted with full visibility results\r\n            - VisionResult: Complete vision state\r\n            \r\n        threat_detected: Emitted when potential threat enters vision\r\n            - str: threat type\r\n            - tuple: position (x, y)\r\n    \"\"\"\r\n    \r\n    food_visibility_changed = pyqtSignal(bool, list)\r\n    plant_proximity_changed = pyqtSignal(float, list)\r\n    visibility_update = pyqtSignal(object)  # VisionResult\r\n    threat_detected = pyqtSignal(str, tuple)\r\n    \r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        \r\n        # Thread control\r\n        self._running = True\r\n        self._paused = False\r\n        \r\n        # Mutex for thread-safe access\r\n        self._mutex = QMutex()\r\n        self._condition = QWaitCondition()\r\n        \r\n        # Current state\r\n        self._squid_state: Optional[SquidVisionState] = None\r\n        self._scene_objects: List[SceneObject] = []\r\n        self._state_dirty = False\r\n        \r\n        # Previous results for change detection\r\n        self._last_result: Optional[VisionResult] = None\r\n        self._last_food_visible = False\r\n        self._last_plant_proximity = 0.0\r\n        \r\n        # Update frequency control\r\n        self._update_interval = 1.0 / 20.0  # 20 Hz max\r\n        self._last_update_time = 0.0\r\n        \r\n        # Performance stats\r\n        self._calc_count = 0\r\n        self._total_calc_time = 0.0\r\n    \r\n    def stop(self):\r\n        \"\"\"Stop the worker thread\"\"\"\r\n        self._running = False\r\n        self._mutex.lock()\r\n        self._condition.wakeAll()\r\n        self._mutex.unlock()\r\n    \r\n    def pause(self):\r\n        \"\"\"Pause vision calculations\"\"\"\r\n        self._paused = True\r\n    \r\n    def resume(self):\r\n        \"\"\"Resume vision calculations\"\"\"\r\n        self._paused = False\r\n        self._mutex.lock()\r\n        self._condition.wakeAll()\r\n        self._mutex.unlock()\r\n    \r\n    def update_squid_state(self, state: SquidVisionState):\r\n        \"\"\"\r\n        Update the squid's position and view state.\r\n        Call this from the main thread when the squid moves.\r\n        \"\"\"\r\n        with QMutexLocker(self._mutex):\r\n            self._squid_state = state\r\n            self._state_dirty = True\r\n            self._condition.wakeOne()\r\n    \r\n    def update_scene_objects(self, objects: List[SceneObject]):\r\n        \"\"\"\r\n        Update the list of scene objects to check visibility for.\r\n        Call this when objects are added/removed from the scene.\r\n        \"\"\"\r\n        with QMutexLocker(self._mutex):\r\n            self._scene_objects = objects.copy()\r\n            self._state_dirty = True\r\n            self._condition.wakeOne()\r\n    \r\n    def get_last_result(self) -> Optional[VisionResult]:\r\n        \"\"\"Get the most recent vision result (thread-safe)\"\"\"\r\n        with QMutexLocker(self._mutex):\r\n            return self._last_result\r\n    \r\n    def get_stats(self) -> Dict[str, Any]:\r\n        \"\"\"Get performance statistics\"\"\"\r\n        avg_time = self._total_calc_time / max(1, self._calc_count)\r\n        return {\r\n            'calculation_count': self._calc_count,\r\n            'avg_calc_time_ms': avg_time,\r\n        }\r\n    \r\n    def run(self):\r\n        \"\"\"Main worker loop\"\"\"\r\n        print(\"👁 VisionWorker started\")\r\n        \r\n        while self._running:\r\n            should_calculate = False\r\n            squid_state = None\r\n            objects = None\r\n            \r\n            # Check if we need to calculate\r\n            self._mutex.lock()\r\n            try:\r\n                if not self._state_dirty and self._running and not self._paused:\r\n                    # Wait for state change or timeout (100ms)\r\n                    self._condition.wait(self._mutex, 100)\r\n                \r\n                if self._state_dirty and not self._paused:\r\n                    current_time = time.time()\r\n                    if current_time - self._last_update_time >= self._update_interval:\r\n                        should_calculate = True\r\n                        squid_state = self._squid_state\r\n                        objects = self._scene_objects.copy()\r\n                        self._state_dirty = False\r\n                        self._last_update_time = current_time\r\n            finally:\r\n                self._mutex.unlock()\r\n            \r\n            # Perform vision calculation\r\n            if should_calculate and squid_state:\r\n                start_time = time.perf_counter()\r\n                \r\n                try:\r\n                    result = self._calculate_visibility(squid_state, objects or [])\r\n                    \r\n                    # Update cached result\r\n                    with QMutexLocker(self._mutex):\r\n                        self._last_result = result\r\n                    \r\n                    # Check for changes and emit signals\r\n                    self._check_and_emit_changes(result)\r\n                    \r\n                    # Stats\r\n                    calc_time = (time.perf_counter() - start_time) * 1000\r\n                    self._calc_count += 1\r\n                    self._total_calc_time += calc_time\r\n                    \r\n                except Exception as e:\r\n                    print(f\"👁 Vision calculation error: {e}\")\r\n                    import traceback\r\n                    traceback.print_exc()\r\n        \r\n        print(\"👁 VisionWorker stopped\")\r\n    \r\n    def _calculate_visibility(self, squid: SquidVisionState, \r\n                             objects: List[SceneObject]) -> VisionResult:\r\n        \"\"\"Calculate what the squid can see\"\"\"\r\n        result = VisionResult(timestamp=time.time())\r\n        \r\n        # Get squid center and bounding box\r\n        squid_cx = squid.squid_x + squid.squid_width / 2\r\n        squid_cy = squid.squid_y + squid.squid_height / 2\r\n        \r\n        # Squid bounding box edges\r\n        squid_left = squid.squid_x\r\n        squid_right = squid.squid_x + squid.squid_width\r\n        squid_top = squid.squid_y\r\n        squid_bottom = squid.squid_y + squid.squid_height\r\n        \r\n        # Vision cone length (diagonal of window)\r\n        cone_length = math.sqrt(squid.window_width**2 + squid.window_height**2)\r\n        \r\n        # Half cone angle for comparison\r\n        half_cone = squid.view_cone_angle / 2\r\n        \r\n        # Process each object\r\n        food_items = []\r\n        min_food_dist = float('inf')\r\n        nearest_food = None\r\n        \r\n        # Track plant proximity (with bounding box awareness)\r\n        min_plant_edge_dist = float('inf')\r\n        plant_is_touching = False\r\n        \r\n        for obj in objects:\r\n            # Get object center\r\n            obj_cx = obj.x + obj.width / 2\r\n            obj_cy = obj.y + obj.height / 2\r\n            \r\n            # Object bounding box edges\r\n            obj_left = obj.x\r\n            obj_right = obj.x + obj.width\r\n            obj_top = obj.y\r\n            obj_bottom = obj.y + obj.height\r\n            \r\n            # Calculate center-to-center distance\r\n            dx = obj_cx - squid_cx\r\n            dy = obj_cy - squid_cy\r\n            distance = math.sqrt(dx*dx + dy*dy)\r\n            \r\n            # Check if in vision cone\r\n            in_cone = False\r\n            if distance <= cone_length:\r\n                # Calculate angle to object\r\n                angle_to_obj = math.atan2(dy, dx)\r\n                \r\n                # Calculate angle difference (handle wrap-around)\r\n                angle_diff = abs(angle_to_obj - squid.current_view_angle)\r\n                while angle_diff > math.pi:\r\n                    angle_diff = 2 * math.pi - angle_diff\r\n                \r\n                in_cone = angle_diff <= half_cone\r\n            \r\n            # Categorize visible objects\r\n            pos = (obj.x, obj.y)\r\n            \r\n            if obj.category == 'food':\r\n                if in_cone:\r\n                    # Prioritize sushi\r\n                    if obj.is_sushi:\r\n                        food_items.insert(0, pos)\r\n                    else:\r\n                        food_items.append(pos)\r\n                    \r\n                    if distance < min_food_dist:\r\n                        min_food_dist = distance\r\n                        nearest_food = pos\r\n            \r\n            elif obj.category == 'plant':\r\n                if in_cone:\r\n                    result.visible_plants.append(pos)\r\n                \r\n                # ============================================================\r\n                # PLANT PROXIMITY - Uses bounding box distance, not center\r\n                # ============================================================\r\n                \r\n                # Check if bounding boxes overlap (touching = 100)\r\n                boxes_overlap = not (\r\n                    squid_right < obj_left or   # squid is left of plant\r\n                    squid_left > obj_right or   # squid is right of plant\r\n                    squid_bottom < obj_top or   # squid is above plant\r\n                    squid_top > obj_bottom      # squid is below plant\r\n                )\r\n                \r\n                if boxes_overlap:\r\n                    plant_is_touching = True\r\n                    result.nearby_plants.append(pos)\r\n                else:\r\n                    # Calculate distance between bounding box edges\r\n                    # Find the closest point on the plant rect to the squid rect\r\n                    \r\n                    # Horizontal distance\r\n                    if squid_right < obj_left:\r\n                        hdist = obj_left - squid_right\r\n                    elif squid_left > obj_right:\r\n                        hdist = squid_left - obj_right\r\n                    else:\r\n                        hdist = 0  # Overlapping horizontally\r\n                    \r\n                    # Vertical distance\r\n                    if squid_bottom < obj_top:\r\n                        vdist = obj_top - squid_bottom\r\n                    elif squid_top > obj_bottom:\r\n                        vdist = squid_top - obj_bottom\r\n                    else:\r\n                        vdist = 0  # Overlapping vertically\r\n                    \r\n                    # Edge-to-edge distance\r\n                    edge_dist = math.sqrt(hdist*hdist + vdist*vdist)\r\n                    \r\n                    # Track if within proximity range (300px from edge)\r\n                    if edge_dist < 300:\r\n                        result.nearby_plants.append(pos)\r\n                        min_plant_edge_dist = min(min_plant_edge_dist, edge_dist)\r\n            \r\n            elif obj.category == 'rock':\r\n                if in_cone:\r\n                    result.visible_rocks.append(pos)\r\n            \r\n            elif obj.category == 'poop':\r\n                if in_cone:\r\n                    result.visible_poop.append(pos)\r\n        \r\n        # Set food results\r\n        result.visible_food = food_items\r\n        result.can_see_food = len(food_items) > 0\r\n        result.nearest_food_distance = min_food_dist\r\n        result.nearest_food_position = nearest_food\r\n        \r\n        # ============================================================\r\n        # Calculate final plant proximity value\r\n        # ============================================================\r\n        if plant_is_touching:\r\n            # Bounding boxes overlap = maximum proximity\r\n            result.plant_proximity_value = 100.0\r\n        elif result.nearby_plants:\r\n            # Scale 0-100 based on edge distance (closer = higher)\r\n            # At 0 distance (touching) = 100, at 300 distance = 0\r\n            max_range = 300.0\r\n            result.plant_proximity_value = max(0.0, 100.0 - (min_plant_edge_dist / max_range * 100.0))\r\n        else:\r\n            result.plant_proximity_value = 0.0\r\n        \r\n        return result\r\n    \r\n    def _check_and_emit_changes(self, result: VisionResult):\r\n        \"\"\"Check for changes and emit appropriate signals\"\"\"\r\n        # Food visibility change\r\n        if result.can_see_food != self._last_food_visible:\r\n            self._last_food_visible = result.can_see_food\r\n            self.food_visibility_changed.emit(result.can_see_food, result.visible_food)\r\n        \r\n        # Plant proximity change (with threshold to avoid noise)\r\n        proximity_diff = abs(result.plant_proximity_value - self._last_plant_proximity)\r\n        if proximity_diff > 5.0:  # 5% threshold\r\n            self._last_plant_proximity = result.plant_proximity_value\r\n            self.plant_proximity_changed.emit(result.plant_proximity_value, result.nearby_plants)\r\n        \r\n        # Always emit full update\r\n        self.visibility_update.emit(result)\r\n\r\n\r\ndef extract_scene_objects(scene, food_items: List, \r\n                          decorations: Optional[List] = None) -> List[SceneObject]:\r\n    \"\"\"\r\n    Helper function to extract SceneObject list from Qt scene.\r\n    Call this from the main thread before updating the vision worker.\r\n    \r\n    Args:\r\n        scene: QGraphicsScene instance\r\n        food_items: List of food item graphics items\r\n        decorations: Optional list of decoration items (if not provided, extracts from scene)\r\n    \r\n    Returns:\r\n        List of SceneObject instances\r\n    \"\"\"\r\n    objects = []\r\n    obj_id = 0\r\n    \r\n    # Extract food items\r\n    for item in food_items:\r\n        try:\r\n            pos = item.pos()\r\n            rect = item.boundingRect()\r\n            objects.append(SceneObject(\r\n                x=pos.x(),\r\n                y=pos.y(),\r\n                width=rect.width(),\r\n                height=rect.height(),\r\n                category='food',\r\n                is_sushi=getattr(item, 'is_sushi', False),\r\n                obj_id=obj_id\r\n            ))\r\n            obj_id += 1\r\n        except:\r\n            pass\r\n    \r\n    # Extract decorations from scene if not provided\r\n    if decorations is None:\r\n        # Try multiple import paths to handle package context\r\n        ResizablePixmapItem = None\r\n        \r\n        # Try relative import first (when used within package)\r\n        try:\r\n            from .ui import ResizablePixmapItem\r\n        except ImportError:\r\n            pass\r\n        \r\n        # Try absolute import (when used standalone)\r\n        if ResizablePixmapItem is None:\r\n            try:\r\n                from ui import ResizablePixmapItem\r\n            except ImportError:\r\n                pass\r\n        \r\n        # Fallback: scan scene items by checking for 'category' attribute\r\n        if ResizablePixmapItem is not None:\r\n            decorations = [item for item in scene.items() \r\n                          if isinstance(item, ResizablePixmapItem)]\r\n        else:\r\n            # Last resort: use duck typing - any item with 'category' attribute\r\n            decorations = [item for item in scene.items() \r\n                          if hasattr(item, 'category') and item.category in ('plant', 'rock', 'poop', 'decoration')]\r\n    \r\n    for item in decorations:\r\n        try:\r\n            pos = item.pos()\r\n            rect = item.boundingRect()\r\n            category = getattr(item, 'category', 'unknown')\r\n            \r\n            # Skip non-categorized items\r\n            if category not in ('plant', 'rock', 'poop'):\r\n                continue\r\n            \r\n            objects.append(SceneObject(\r\n                x=pos.x(),\r\n                y=pos.y(),\r\n                width=rect.width(),\r\n                height=rect.height(),\r\n                category=category,\r\n                obj_id=obj_id\r\n            ))\r\n            obj_id += 1\r\n        except:\r\n            pass\r\n    \r\n    return objects\r\n\r\n\r\ndef create_squid_vision_state(squid) -> SquidVisionState:\r\n    \"\"\"\r\n    Helper function to create SquidVisionState from a Squid instance.\r\n    Call this from the main thread.\r\n    \"\"\"\r\n    return SquidVisionState(\r\n        squid_x=squid.squid_x,\r\n        squid_y=squid.squid_y,\r\n        squid_width=squid.squid_width,\r\n        squid_height=squid.squid_height,\r\n        current_view_angle=getattr(squid, 'current_view_angle', 0.0),\r\n        view_cone_angle=getattr(squid, 'view_cone_angle', math.pi / 2.5),\r\n        window_width=squid.ui.window_width,\r\n        window_height=squid.ui.window_height,\r\n        timestamp=time.time()\r\n    )\r\n"
  },
  {
    "path": "translations/de.py",
    "content": "LANGUAGE_HEADER = \"de - Deutsch\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"Hunger\",\r\n    \"happiness\": \"Glück\",\r\n    \"cleanliness\": \"Sauberkeit\",\r\n    \"sleepiness\": \"Schläfrigkeit\",\r\n    \"satisfaction\": \"Zufriedenheit\",\r\n    \"anxiety\": \"Angst\",\r\n    \"curiosity\": \"Neugier\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"Sieht Futter\",\r\n    \"is_eating\": \"Frisst\",\r\n    \"is_sleeping\": \"Schläft\",\r\n    \"is_sick\": \"Krank\",\r\n    \"pursuing_food\": \"Verfolgt Futter\",\r\n    \"is_startled\": \"Erschrocken\",\r\n    \"is_fleeing\": \"Flüchtet\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"Neuheit\",\r\n    \"stress\": \"Stress\",\r\n    \"reward\": \"Belohnung\",\r\n\r\n    # ===== HAUPTMENÜ =====\r\n    \"file\": \"Datei\",\r\n    \"new_game\": \"Neues Spiel\",\r\n    \"load_game\": \"Spiel laden\",\r\n    \"save_game\": \"Spiel speichern\",\r\n    \"view\": \"Ansicht\",\r\n    \"speed\": \"Geschwindigkeit\",\r\n    \"pause\": \"Pause\",\r\n    \"actions\": \"Aktionen\",\r\n    \"debug\": \"Debug\",\r\n    \"plugins\": \"Plugins\",\r\n\r\n    # ===== ANSICHT-MENÜ =====\r\n    \"brain_designer\": \"Gehirn-Designer\",\r\n    \"decorations\": \"Dekorationen\",\r\n    \"statistics\": \"Statistiken\",\r\n    \"brain_tool\": \"Gehirn-Werkzeug\",\r\n    \"neuron_lab\": \"Neuronen-Labor\",\r\n    \"task_manager\": \"Task-Manager\",\r\n\r\n    # ===== GESCHWINDIGKEIT-MENÜ =====\r\n    \"normal_speed\": \"Normal (1x)\",\r\n    \"fast_speed\": \"Schnell (2x)\",\r\n    \"very_fast\": \"Sehr schnell (3x)\",\r\n\r\n    # ===== DEBUG-MENÜ =====\r\n    \"toggle_debug\": \"Debug-Modus umschalten\",\r\n    \"toggle_cone\": \"Sichtkegel umschalten\",\r\n    \"squid_vision\": \"Tintenfisch-Sicht\",\r\n\r\n    # ===== AKTIONS-SCHALTFLÄCHEN =====\r\n    \"feed\": \"Füttern\",\r\n    \"clean\": \"Reinigen\",\r\n    \"medicine\": \"Medizin\",\r\n    \"feed_btn\": \"FÜTTERN\",\r\n    \"clean_btn\": \"REINIGEN\",\r\n    \"medicine_btn\": \"MEDIZIN\",\r\n\r\n    # ===== NACHRICHTEN =====\r\n    \"feed_msg\": \"Tintenfisch muss gefüttert werden\",\r\n    \"points\": \"Punkte\",\r\n    \"dirty\": \"SCHMUTZIG\",\r\n    \"paused_msg\": \"SIMULATION PAUSIERT\",\r\n    \"paused_sub\": \"Nutzen Sie das Menü Geschwindigkeit zum Fortfahren\",\r\n\r\n    # ===== DIALOGE =====\r\n    \"yes\": \"Ja\",\r\n    \"no\": \"Nein\",\r\n    \"ok\": \"OK\",\r\n    \"cancel\": \"Abbrechen\",\r\n    \"close\": \"Schließen\",\r\n    \"save\": \"Speichern\",\r\n    \"load\": \"Laden\",\r\n    \"reset\": \"Zurücksetzen\",\r\n    \"apply_changes\": \"Änderungen übernehmen\",\r\n    \"got_it\": \"Verstanden!\",\r\n    \"finish\": \"Abschließen\",\r\n    \"confirm_new_game\": \"Neues Spiel starten? Aktueller Fortschritt geht verloren.\",\r\n    \"confirm_exit\": \"Sind Sie sicher, dass Sie beenden möchten?\",\r\n    \"save_successful\": \"Spiel erfolgreich gespeichert!\",\r\n    \"load_successful\": \"Spiel erfolgreich geladen!\",\r\n    \"error_saving\": \"Fehler beim Speichern des Spiels.\",\r\n    \"error_loading\": \"Fehler beim Laden des Spiels.\",\r\n    \"no_save_found\": \"Kein Speicherstand gefunden.\",\r\n    \"startup\": \"Start\",\r\n    \"show_tutorial_q\": \"Tutorial anzeigen?\",\r\n    \"auto_decline\": \"(Automatische Ablehnung in {seconds}s)\",\r\n    \"tutorial_title\": \"Tutorial\",\r\n    \"tutorial_query\": \"Möchten Sie das Tutorial sehen?\",\r\n\r\n    # ===== ÜBER-REITER =====\r\n    \"hello\": \"HALLO\",\r\n    \"my_name_is\": \"mein Name ist\",\r\n    \"change_name\": \"Name ändern\",\r\n    \"enter_new_name\": \"Geben Sie einen neuen Namen für Ihren Tintenfisch ein:\",\r\n    \"change_colour\": \"Farbe ändern\",\r\n    \"view_certificate\": \"Zertifikat ansehen\",\r\n    \"care_tips\": \"Pflegetipps\",\r\n    \"care_tips_for\": \"Pflegetipps für {personality} Tintenfische\",\r\n    \"dosidicus_title\": \"Dosidicus electronicus\",\r\n    \"dosidicus_desc\": \"Ein digitales Haustier im Tamagotchi-Stil mit einem einfachen neuronalen Netzwerk\",\r\n    \"string_acronym\": \"Simulated Tamagotchi Reactions via Inferencing and Neurogenesis (STRINg)\",\r\n    \"research_project\": \"Dies ist ein Forschungsprojekt. Bitte schlagen Sie Funktionen vor.\",\r\n    \"version_dosidicus\": \"Dosidicus-Version:\",\r\n    \"version_brain_tool\": \"Gehirn-Werkzeug-Version:\",\r\n    \"version_decision\": \"Entscheidungs-Engine-Version:\",\r\n    \"version_neuro\": \"Neurogenese-Version:\",\r\n    \"created_by\": \"von\",\r\n\r\n    # ===== PERSÖNLICHKEIT =====\r\n    \"squid_personality\": \"Tintenfisch-Persönlichkeit\",\r\n    \"personality_modifier\": \"Persönlichkeits-Modifikator\",\r\n    \"description\": \"Beschreibung:\",\r\n    \"personality_modifiers\": \"Persönlichkeits-Modifikatoren:\",\r\n    \"care_tips_label\": \"Pflegetipps:\",\r\n    \"personality_note\": \"Hinweis: Die Persönlichkeit wird zu Beginn eines neuen Spiels zufällig generiert\",\r\n\r\n    # Persönlichkeitstypen\r\n    \"personality_timid\": \"Ängstlich\",\r\n    \"personality_adventurous\": \"Abenteuerlustig\",\r\n    \"personality_lazy\": \"Faul\",\r\n    \"personality_energetic\": \"Energetisch\",\r\n    \"personality_introvert\": \"Introvertiert\",\r\n    \"personality_greedy\": \"Gierig\",\r\n    \"personality_stubborn\": \"Stur\",\r\n\r\n    # Persönlichkeitsbeschreibungen\r\n    \"desc_timid\": \"Ihr Tintenfisch ist ängstlich. Er neigt dazu, leichter erschrocken und besorgt zu sein, besonders in neuen Situationen. Er bevorzugt ruhige Umgebungen und erkundet weniger auf eigene Faust. Er kann jedoch starke Bindungen aufbauen, wenn er sich sicher fühlt.\",\r\n    \"desc_adventurous\": \"Ihr Tintenfisch ist abenteuerlustig. Er liebt es, zu erkunden und neue Dinge auszuprobieren. Er ist oft der Erste, der neue Objekte oder Bereiche untersucht. Dieser Tintenfisch blüht bei Neuheiten auf und langweilt sich in gleichbleibender Umgebung schnell.\",\r\n    \"desc_lazy\": \"Ihr Tintenfisch ist faul. Er bevorzugt einen entspannten Lebensstil und ist weniger aktiv. Er benötigt eventuell extra Ermutigung, kann aber auch sehr zufrieden damit sein, einfach nur herumzuliegen. Er ist ein Experte im Energiesparen!\",\r\n    \"desc_energetic\": \"Ihr Tintenfisch ist energetisch. Er ist immer in Bewegung und voller Tatendrang. Dieser Tintenfisch braucht viel Stimulation und Beschäftigung, um glücklich zu bleiben. Ohne genug Auslastung kann er unruhig werden.\",\r\n    \"desc_introvert\": \"Ihr Tintenfisch ist introvertiert. Er genießt die Einsamkeit und bevorzugt ruhige Orte. Er interagiert zwar mit anderen, braucht aber Zeit für sich, um 'aufzutanken'. Er ist oft beobachtend und bedacht in seinem Handeln.\",\r\n    \"desc_greedy\": \"Ihr Tintenfisch ist gierig. Er ist stark auf Futter und Ressourcen fokussiert. Belohnungen motivieren ihn besonders. Er kann fordernd sein, ist aber auch sehr einfallsreich darin, verstecktes Futter zu finden!\",\r\n    \"desc_stubborn\": \"Ihr Tintenfisch ist stur. Er hat einen starken Willen und feste Vorlieben. Er ist resistenter gegen Veränderungen und braucht länger, um sich an neue Routinen zu gewöhnen. Seine Entschlossenheit hilft ihm jedoch bei der Problemlösung.\",\r\n\r\n    # Persönlichkeit Kurz-Modifikatoren\r\n    \"mod_timid\": \"Höhere Wahrscheinlichkeit für Angstzustände\",\r\n    \"mod_adventurous\": \"Erhöhte Neugier und Erkundungsdrang\",\r\n    \"mod_lazy\": \"Langsamere Bewegung und geringerer Energieverbrauch\",\r\n    \"mod_energetic\": \"Schnellere Bewegung und höheres Aktivitätsniveau\",\r\n    \"mod_introvert\": \"Bevorzugt Einsamkeit und ruhige Umgebungen\",\r\n    \"mod_greedy\": \"Fokussiert auf Futter und Ressourcen\",\r\n    \"mod_stubborn\": \"Frisst nur Lieblingsfutter (Sushi), verweigert evtl. den Schlaf\",\r\n\r\n    # Details zu Modifikatoren\r\n    \"modifiers_timid\": \"- Angst steigt 50% schneller\\n- Neugier steigt 50% langsamer\\n- Angst sinkt um 50% in der Nähe von Pflanzen\",\r\n    \"modifiers_adventurous\": \"- Neugier steigt 50% schneller\",\r\n    \"modifiers_lazy\": \"- Bewegt sich langsamer\\n- Energieverbrauch ist niedriger\",\r\n    \"modifiers_energetic\": \"- Bewegt sich schneller\\n- Energieverbrauch ist höher\",\r\n    \"modifiers_introvert\": \"- Bevorzugt ruhige, weniger überfüllte Orte\\n- Braucht mehr Zeit allein zum 'Auftanken'\",\r\n    \"modifiers_greedy\": \"- Wird 50% ängstlicher bei Hunger\\n- Zufriedenheit steigt beim Fressen stärker an\",\r\n    \"modifiers_stubborn\": \"- Bevorzugt Lieblingsfutter (Sushi)\\n- Kann Schlaf verweigern, selbst wenn er müde ist\",\r\n\r\n    # Pflegetipps\r\n    \"tips_timid\": \"- Pflanzen platzieren, um Angst zu mindern\\n- Umgebung sauber und ruhig halten\\n- Langsam nähern und plötzliche Bewegungen vermeiden\\n- Konsistente Routine beibehalten\\n- Häufiges Ändern der Fenstergröße vermeiden\",\r\n    \"tips_adventurous\": \"- Regelmäßig neue Objekte oder Deko einführen\\n- Vielfältige Futteroptionen anbieten\\n- Erkundung durch strategische Futterplatzierung fördern\\n- Viel Platz zum Erkunden bieten\\n- Natürliche Neugier mit interessanten Gegenständen fördern\",\r\n    \"tips_lazy\": \"- Futter näher an den Ruheplätzen platzieren\\n- Umgebung häufiger reinigen\\n- Lockmittel verwenden, um Bewegung zu fördern\\n- Nicht zu viel Aktivität erwarten - sie entspannen lieber\\n- Ruheplätze sauber und komfortabel halten\",\r\n    \"tips_energetic\": \"- Großen, offenen Raum für Bewegung bieten\\n- Häufige Fütterungsmöglichkeiten anbieten\\n- Interaktive Elemente oder Spiele einführen\\n- Umgebung mit variabler Deko stimulierend halten\\n- Benötigen mehr Futter aufgrund hohen Energieverbrauchs\",\r\n    \"tips_introvert\": \"- Ruhige, abgeschiedene Bereiche mit Deko schaffen\\n- Überfüllung der Umgebung vermeiden\\n- Bedürfnis nach Zeit allein respektieren\\n- Geschützte Räume mit Pflanzen schaffen\\n- Sanft nähern und Freiraum geben\",\r\n    \"tips_greedy\": \"- Verschiedene Futtersorten anbieten (inkl. Sushi)\\n- Futter als Belohnung für gewünschtes Verhalten nutzen\\n- Vorsicht vor Überfütterung\\n- Wird bei Hunger ängstlicher als andere Typen\\n- Möglichkeiten zum Sammeln von Gegenständen bieten\",\r\n    \"tips_stubborn\": \"- Immer Sushi bereitstellen (Lieblingsfutter)\\n- Geduldig bei Veränderungen sein\\n- Positive Verstärkung für gewünschtes Verhalten nutzen\\n- Kann anderes Futter bei Hunger verweigern\\n- Kann Schlaf widerstehen - ruhige Umgebung schaffen\",\r\n\r\n    # ===== ENTSCHEIDUNGEN-REITER =====\r\n    \"thought_process\": \"Gedankengang des Tintenfisches\",\r\n    \"step\": \"Schritt\",\r\n    \"step1_title\": \"Die Welt wahrnehmen\",\r\n    \"step2_title\": \"Basis-Triebe berechnen\",\r\n    \"step3_title\": \"Persönlichkeit & Erinnerungen anwenden\",\r\n    \"step4_title\": \"Die finale Entscheidung treffen\",\r\n    \"final_action\": \"Finale Aktion:\",\r\n    \"awaiting_thought\": \"Warte auf den nächsten Gedanken...\",\r\n    \"awaiting_decision\": \"Warte auf Entscheidung...\",\r\n    \"sensing_condition\": \"Der Tintenfisch prüft Zustand und sichtbare Objekte:\",\r\n    \"visible_objects\": \"Sichtbare Objekte\",\r\n    \"no_sensory_data\": \"Keine Sensordaten verfügbar.\",\r\n    \"none\": \"Keine\",\r\n    \"no_urges\": \"Keine Triebe berechnet.\",\r\n    \"strongest_urge\": \"Basierend auf Bedürfnissen ist der stärkste Trieb\",\r\n    \"initial_scores\": \"Anfangswerte:\",\r\n    \"personality_memory_adjust\": \"Persönlichkeit und Erinnerungen passen Triebe an:\",\r\n    \"no_adjustments\": \"Diesmal keine signifikanten Anpassungen.\",\r\n    \"final_scores_text\": \"Nach allen Berechnungen steht das Ergebnis fest. Der höchste Wert bestimmt die Aktion.\",\r\n    \"no_final_scores\": \"Keine Finalwerte verfügbar.\",\r\n    \"squid_decided\": \"Der Tintenfisch hat sich entschieden für\",\r\n    \"with_confidence\": \"mit einem Vertrauenswert von\",\r\n    \"score_increased\": \"erhöht\",\r\n    \"score_decreased\": \"verringert\",\r\n    \"score_for\": \"Der Wert für\",\r\n    \"by_amount\": \"um\",\r\n\r\n    # ===== LERNEN-REITER =====\r\n    \"active_learning_pairs\": \"Aktive Lernpaare\",\r\n    \"hebbian_cycle\": \"Hebbscher Zyklus\",\r\n    \"hebbian_paused\": \"PAUSIERT\",\r\n    \"learning_ready\": \"Lernsystem bereit\",\r\n    \"learning_ready_desc\": \"Hebbsches Lernen verknüpft Neuronen, die gemeinsam feuern.\",\r\n    \"log_cleared\": \"Protokoll geleert\",\r\n    \"log_cleared_desc\": \"Lernpaare erscheinen hier, wenn neue Verbindungen entstehen.\",\r\n    \"hebbian_overview\": \"Überblick: Hebbsches Lernen\",\r\n    \"neurons_fire_together\": \"Neurons that fire together, wire together\",\r\n    \"hebbian_principle\": \"Dieses Prinzip beschreibt, wie Netzwerke durch Erfahrung lernen.\",\r\n    \"hebbian_explanation\": \"Hebbsches Lernen ist eine einfache Regel: Wenn zwei Neuronen gleichzeitig aktiv sind, stärkt sich ihre Verbindung (Gewichtung). Werden sie getrennt aktiv, schwächt sie sich ab. So entstehen natürliche Assoziationen.\",\r\n    \"excitatory_connections\": \"Erregende Verbindungen\",\r\n    \"excitatory_desc\": \"Positive Gewichte (0.0-1.0) fördern gemeinsame Aktivierung\",\r\n    \"inhibitory_connections\": \"Hemmende Verbindungen\",\r\n    \"inhibitory_desc\": \"Negative Gewichte (-1.0-0.0) hemmen gemeinsame Aktivierung\",\r\n    \"very_strong\": \"Sehr stark\",\r\n    \"strong\": \"Stark\",\r\n    \"moderate\": \"Mittel\",\r\n    \"weak\": \"Schwach\",\r\n    \"very_weak\": \"Sehr schwach\",\r\n    \"inhibited\": \"Gehemmt\",\r\n\r\n    # ===== MEMORY TAB =====\r\n    \"memory\": \"Gedächtnis\",\r\n    \"memories\": \"Erinnerungen\",\r\n    \"short_term_memory\": \"Kurzzeitgedächtnis\",\r\n    \"long_term_memory\": \"Langzeitgedächtnis\",\r\n    \"no_memories\": \"Noch keine Erinnerungen gespeichert.\",\r\n    \"overview\": \"Übersicht\",\r\n    \"memory_stats\": \"Gedächtnis-Statistiken\",\r\n    \"categories\": \"Kategorien\",\r\n    \"time_label\": \"Zeit:\",\r\n    \"important_label\": \"Wichtig\",\r\n    \"unknown\": \"Unbekannt\",\r\n    \"category_label\": \"Kategorie:\",\r\n    \"key_label\": \"Schlüssel:\",\r\n    \"access_count\": \"Abrufe:\",\r\n    \"full_content\": \"Vollständiger Inhalt:\",\r\n    \"effects_label\": \"Effekte:\",\r\n    \"positive\": \"Positiv\",\r\n    \"negative\": \"Negativ\",\r\n    \"neutral\": \"Neutral\",\r\n\r\n    # ===== NETZWERK-REITER =====\r\n    \"brain_network\": \"Gehirn-Netzwerk\",\r\n    \"neurons\": \"Neuronen\",\r\n    \"connections\": \"Verbindungen\",\r\n    \"activity\": \"Aktivität\",\r\n\r\n    # ===== STATISTIK-FENSTER =====\r\n    \"status\": \"Status\",\r\n    \"health\": \"Gesundheit\",\r\n\r\n    # ===== NEURONEN-NAMEN =====\r\n    \"external_stimulus\": \"Reiz\",\r\n    \"plant_proximity\": \"Pflanzennähe\",\r\n    \"layer_name\": \"Ebene\",\r\n    \"layer_input\": \"Eingang\",\r\n    \"layer_output\": \"Ausgang\",\r\n    \"layer_hidden\": \"Verborgen\",\r\n\r\n    # ===== NEUROGENESE LOGS =====\r\n    \"log_created\": \"{time} - ein {type} Neuron ({name}) wurde erstellt, da der {type}-Zähler bei {value:.2f} lag\",\r\n    \"log_pruned\": \"{time} - ein Neuron ({name}) wurde entfernt wegen {reason}\",\r\n    \"log_stress_detail\": \"Eine hemmende Verbindung zu ANGST wurde erstellt\\nMaximaler Angstwert permanent um 10 reduziert\",\r\n\r\n    # Zustands-Anzeigen\r\n    \"playing\": \"Spielt\",\r\n    \"hiding\": \"Versteckt\",\r\n    \"anxious\": \"Ängstlich\",\r\n    \"curious\": \"Neugierig\",\r\n\r\n    # ===== AKTIONEN =====\r\n    \"eat\": \"Essen\",\r\n    \"sleep\": \"Schlafen\",\r\n    \"play\": \"Spielen\",\r\n    \"explore\": \"Erkunden\",\r\n    \"rest\": \"Ruhen\",\r\n    \"hide\": \"Verstecken\",\r\n    \"wander\": \"Wandern\",\r\n    \"idle\": \"Untätig\",\r\n    \"seek_food\": \"Futter suchen\",\r\n    \"seek_shelter\": \"Schutz suchen\",\r\n\r\n    # ===== OBJEKTE =====\r\n    \"food\": \"Futter\",\r\n    \"rock\": \"Fels\",\r\n    \"poop\": \"Haufen\",\r\n    \"plant\": \"Pflanze\",\r\n    \"sushi\": \"Sushi\",\r\n    \"decoration\": \"Dekoration\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"Ein Tintenfisch ist geschlüpft! Du musst dich um ihn kümmern.\",\r\n    \"tutorial_feed\": \"Füttere ihn bei Hunger (Aktionsmenü)\",\r\n    \"tutorial_clean\": \"Reinige das Becken, wenn es schmutzig wird\",\r\n    \"tutorial_watch\": \"Beobachte ihn, um seine Persönlichkeit kennenzulernen\",\r\n    \"tutorial_neural\": \"NEURONALES NETZWERK\",\r\n    \"tutorial_neural_desc\": \"Dies ist sein Gehirn. Verhalten wird durch Bedürfnisse (runde Neuronen) gesteuert.\\nDas Netzwerk lernt durch Interaktion mit der Umwelt.\",\r\n    \"tutorial_satisfaction\": \"Halte Zufriedenheit hoch und Angst niedrig.\",\r\n    \"tutorial_traits\": \"Dein Tintenfisch entwickelt einzigartige Eigenschaften durch deine Pflege.\",\r\n\r\n    # ===== BRAIN DESIGNER =====\r\n    \"required_only\": \"Nur Erforderliche\",\r\n    \"dosidicus_default\": \"Dosidicus Standard\",\r\n    \"full_sensors\": \"Volle Sensoren\",\r\n    \"the_insomniac\": \"Der Schlaflose\",\r\n    \"the_hyperactive\": \"Der Hyperaktive\",\r\n    \"the_hangry\": \"Der Hangry\",\r\n    \"the_depressive\": \"Der Depressive\",\r\n    \"the_obsessive\": \"Der Obsessive\",\r\n    \"balanced\": \"Ausgeglichen\",\r\n    \"minimal\": \"Minimal\",\r\n    \"dense\": \"Dicht\",\r\n    \"chaotic\": \"Chaotisch\",\r\n    \"calm\": \"Ruhig\",\r\n\r\n    # ===== SPLASH SCREEN =====\r\n    \"squid_hatched\": \"EIN TINTENFISCH IST GESCHLÜPFT!\",\r\n    \"look_after\": \"DU MUSST DICH UM IHN KÜMMERN..\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"Lernen\",\r\n    \"tab_decisions\": \"Entscheidungen\",\r\n    \"tab_personality\": \"Persönlichkeit\",\r\n    \"tab_about\": \"Über\",\r\n    \r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"Neuronen-Inspektor\",\r\n    \"lbl_name\": \"Name:\",\r\n    \"lbl_value\": \"Aktueller Wert:\",\r\n    \"lbl_position\": \"Position:\",\r\n    \"lbl_type\": \"Typ:\",\r\n    \"grp_neurogenesis\": \"Neurogenese-Details\",\r\n    \"lbl_created\": \"Erstellt am:\",\r\n    \"lbl_trigger\": \"Trigger-Typ:\",\r\n    \"lbl_trigger_val\": \"Trigger-Wert:\",\r\n    \"lbl_state\": \"Zugehöriger Zustand:\",\r\n    \"col_connected\": \"Verbunden mit\",\r\n    \"col_weight\": \"Gewicht\",\r\n    \"col_direction\": \"Richtung\",\r\n    \"btn_refresh_data\": \"Daten aktualisieren\",\r\n    \"type_core\": \"Kern\",\r\n    \"type_neuro\": \"Neurogenese\",\r\n    \"type_system\": \"Systemstatus\",\r\n    \"direction_incoming\": \"Eingehend\",\r\n    \"direction_outgoing\": \"Ausgehend\",\r\n\r\n    # ===== TUTORIAL STEPS =====\r\n    \"next\": \"Weiter\",\r\n    \"tutorial_step1_text\": \"Ein Tintenfisch ist geschlüpft! Kümmere dich um ihn:\\n• Füttern bei Hunger (Aktionsmenü)\\n• Becken reinigen bei Schmutz\\n• Verhalten beobachten für Persönlichkeit\",\r\n    \"tutorial_step2_text\": \"Das neuronale Netzwerk steuert sein Verhalten durch Bedürfnisse (Neuronen).\\nEs passt sich durch Umweltinteraktion an.\",\r\n    \"tutorial_step3_text\": \"Er kann neue Neuronen bei extremen Reizen bilden.\\nDas hilft ihm, sich an schwierige Situationen anzupassen.\",\r\n    \"tutorial_step4_text\": \"Gleichzeitiges Feuern stärkt Verbindungen. So lernt er Assoziationen zwischen Reizen und Reaktionen.\",\r\n    \"tutorial_step5_text\": \"Entscheidungen basieren auf Bedürfnissen und Erinnerungen.\\nJede Wahl formt sein künftiges Verhalten.\",\r\n    \"tutorial_step6_text\": \"Drücke jederzeit 'D' für Dekorationen.\\nPlatziere Gegenstände und beobachte die Reaktion. Jede Deko beeinflusst den Geisteszustand anders. (Mausrad=Größe / ENTF=Löschen)\",\r\n    \"tutorial_step7_text\": \"Zufriedenheit hoch, Angst niedrig halten.\\nDein Tintenfisch wird durch deine Erziehung einzigartig.\",\r\n\r\n    # ===== NETWORK & LEARNING TABS =====\r\n    \"stats_neurons\": \"Neuronen\",\r\n    \"stats_connections\": \"Verbindungen\",\r\n    \"stats_health\": \"Netzwerk-Gesundheit\",\r\n    \"emergency_alert\": \"🚨 Notfall: {name}\",\r\n    \"global_cooldown\": \"Abklingzeit\",\r\n    \"style_label\": \"Stil:\",\r\n    \"chk_links\": \"Links zeigen\",\r\n    \"chk_weights\": \"Gewichte zeigen\",\r\n    \"chk_pruning\": \"Pruning aktiv\",\r\n    \"tooltip_brain_designer\": \"Brain Designer öffnen\",\r\n    \"msg_already_open\": \"Bereits offen\",\r\n    \"msg_designer_running\": \"Brain Designer läuft bereits!\",\r\n    \"msg_launch_failed\": \"Start fehlgeschlagen\",\r\n    \"msg_designer_fail\": \"Konnte Brain Designer nicht starten:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"Gehirn fehlt\",\r\n    \"msg_cannot_open_lab\": \"Labor nicht öffenbar: Brain Widget fehlt.\",\r\n    \"msg_cannot_open_buffer\": \"Puffer nicht öffenbar: Brain Widget fehlt.\",\r\n    \"msg_no_neurogenesis\": \"Keine Neurogenese\",\r\n    \"msg_neurogenesis_not_init\": \"Puffer nicht öffenbar: System nicht initialisiert.\",\r\n    \"msg_decorations_unavailable\": \"Deko nicht verfügbar\",\r\n    \"msg_decorations_fail\": \"Deko-Fenster nicht verfügbar.\",\r\n    \"func_neurons_title\": \"Funktionale Neuronen\",\r\n    \"count_label\": \"Anzahl\",\r\n    \"avg_utility_label\": \"Durchschn. Nutzen\",\r\n    \"total_activations_label\": \"Gesamtaktivierungen\",\r\n    \"specialisations_label\": \"Spezialisierungen\",\r\n    \"buffer_title\": \"Neurogenese-Erfahrungspuffer\",\r\n    \"buffer_header\": \"Letzte Erfahrungen\",\r\n    \"col_type\": \"Typ\",\r\n    \"col_pattern\": \"Muster\",\r\n    \"col_outcome\": \"Ergebnis\",\r\n    \"col_time\": \"Zeit\",\r\n    \"btn_refresh\": \"Aktualisieren\",\r\n    \"buffer_size\": \"Puffergröße\",\r\n    \"top_patterns\": \"Top-Muster\",\r\n    \"no_patterns\": \"Noch keine Muster\",\r\n}"
  },
  {
    "path": "translations/en.py",
    "content": "LANGUAGE_HEADER = \"en - English\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"Hunger\",\r\n    \"happiness\": \"Happiness\",\r\n    \"cleanliness\": \"Cleanliness\",\r\n    \"sleepiness\": \"Sleepiness\",\r\n    \"satisfaction\": \"Satisfaction\",\r\n    \"anxiety\": \"Anxiety\",\r\n    \"curiosity\": \"Curiosity\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"Can See Food\",\r\n    \"is_eating\": \"Eating\",\r\n    \"is_sleeping\": \"Sleeping\",\r\n    \"is_sick\": \"Sick\",\r\n    \"pursuing_food\": \"Pursuing Food\",\r\n    \"is_startled\": \"Startled\",\r\n    \"is_fleeing\": \"Fleeing\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"Novelty\",\r\n    \"stress\": \"Stress\",\r\n    \"reward\": \"Reward\",\r\n\r\n    # ===== MAIN MENU =====\r\n    \"file\": \"File\",\r\n    \"new_game\": \"New Game\",\r\n    \"load_game\": \"Load Game\",\r\n    \"save_game\": \"Save Game\",\r\n    \"view\": \"View\",\r\n    \"speed\": \"Speed\",\r\n    \"pause\": \"Pause\",\r\n    \"actions\": \"Actions\",\r\n    \"debug\": \"Debug\",\r\n    \"plugins\": \"Plugins\",\r\n\r\n    # ===== VIEW MENU =====\r\n    \"brain_designer\": \"Brain Designer\",\r\n    \"decorations\": \"Decorations\",\r\n    \"statistics\": \"Statistics\",\r\n    \"brain_tool\": \"Brain Tool\",\r\n    \"neuron_lab\": \"Neuron Laboratory\",\r\n    \"task_manager\": \"Task Manager\",\r\n\r\n    # ===== SPEED MENU =====\r\n    \"normal_speed\": \"Normal (1x)\",\r\n    \"fast_speed\": \"Fast (2x)\",\r\n    \"very_fast\": \"Very Fast (3x)\",\r\n\r\n    # ===== DEBUG MENU =====\r\n    \"toggle_debug\": \"Toggle Debug Mode\",\r\n    \"toggle_cone\": \"Toggle View Cone\",\r\n    \"squid_vision\": \"Squid Vision\",\r\n\r\n    # ===== ACTION BUTTONS =====\r\n    \"feed\": \"Feed\",\r\n    \"clean\": \"Clean\",\r\n    \"medicine\": \"Medicine\",\r\n    \"feed_btn\": \"FEED\",\r\n    \"clean_btn\": \"CLEAN\",\r\n    \"medicine_btn\": \"MEDICINE\",\r\n\r\n    # ===== MESSAGES =====\r\n    \"feed_msg\": \"Squid requires feeding\",\r\n    \"points\": \"Points\",\r\n    \"dirty\": \"DIRTY\",\r\n    \"paused_msg\": \"SIMULATION PAUSED\",\r\n    \"paused_sub\": \"Use the Speed menu to resume\",\r\n\r\n    # ===== DIALOGS =====\r\n    \"yes\": \"Yes\",\r\n    \"no\": \"No\",\r\n    \"ok\": \"OK\",\r\n    \"cancel\": \"Cancel\",\r\n    \"close\": \"Close\",\r\n    \"save\": \"Save\",\r\n    \"load\": \"Load\",\r\n    \"reset\": \"Reset\",\r\n    \"apply_changes\": \"Apply Changes\",\r\n    \"got_it\": \"Got it!\",\r\n    \"finish\": \"Finish\",\r\n    \"confirm_new_game\": \"Start a new game? Current progress will be lost.\",\r\n    \"confirm_exit\": \"Are you sure you want to exit?\",\r\n    \"save_successful\": \"Game saved successfully!\",\r\n    \"load_successful\": \"Game loaded successfully!\",\r\n    \"error_saving\": \"Error saving game.\",\r\n    \"error_loading\": \"Error loading game.\",\r\n    \"no_save_found\": \"No save file found.\",\r\n    \"startup\": \"Startup\",\r\n    \"show_tutorial_q\": \"Show tutorial?\",\r\n    \"auto_decline\": \"(Auto-declining in {seconds}s)\",\r\n    \"tutorial_title\": \"Tutorial\",\r\n    \"tutorial_query\": \"Would you like to see the tutorial?\",\r\n\r\n    # ===== ABOUT TAB =====\r\n    \"hello\": \"HELLO\",\r\n    \"my_name_is\": \"my name is\",\r\n    \"change_name\": \"Change Name\",\r\n    \"enter_new_name\": \"Enter new name for your squid:\",\r\n    \"change_colour\": \"Change Colour\",\r\n    \"view_certificate\": \"View Certificate\",\r\n    \"care_tips\": \"Care Tips\",\r\n    \"care_tips_for\": \"Care Tips for {personality} Squids\",\r\n    \"dosidicus_title\": \"Dosidicus electronicus\",\r\n    \"dosidicus_desc\": \"A Tamagotchi-style digital pet with a simple neural network\",\r\n    \"string_acronym\": \"Simulated Tamagotchi Reactions via Inferencing and Neurogenesis (STRINg)\",\r\n    \"research_project\": \"This is a research project. Please suggest features.\",\r\n    \"version_dosidicus\": \"Dosidicus version:\",\r\n    \"version_brain_tool\": \"Brain Tool version:\",\r\n    \"version_decision\": \"Decision engine version:\",\r\n    \"version_neuro\": \"Neurogenesis version:\",\r\n    \"created_by\": \"by\",\r\n\r\n    # ===== PERSONALITY =====\r\n    \"squid_personality\": \"Squid Personality\",\r\n    \"personality_modifier\": \"Personality Modifier\",\r\n    \"description\": \"Description:\",\r\n    \"personality_modifiers\": \"Personality Modifiers:\",\r\n    \"care_tips_label\": \"Care Tips:\",\r\n    \"personality_note\": \"Note: Personality is randomly generated at the start of a new game\",\r\n\r\n    # Personality Types\r\n    \"personality_timid\": \"Timid\",\r\n    \"personality_adventurous\": \"Adventurous\",\r\n    \"personality_lazy\": \"Lazy\",\r\n    \"personality_energetic\": \"Energetic\",\r\n    \"personality_introvert\": \"Introvert\",\r\n    \"personality_greedy\": \"Greedy\",\r\n    \"personality_stubborn\": \"Stubborn\",\r\n\r\n    # Personality Descriptions\r\n    \"desc_timid\": \"Your squid is Timid. It tends to be more easily startled and anxious, especially in new situations. It may prefer quiet, calm environments and might be less likely to explore on its own. However, it can form strong bonds when it feels safe and secure.\",\r\n    \"desc_adventurous\": \"Your squid is Adventurous. It loves to explore and try new things. It's often the first to investigate new objects or areas in its environment. This squid thrives on novelty and might get bored more easily in unchanging surroundings.\",\r\n    \"desc_lazy\": \"Your squid is Lazy. It prefers a relaxed lifestyle and may be less active than other squids. It might need extra encouragement to engage in activities but can be quite content just lounging around. This squid is great at conserving energy!\",\r\n    \"desc_energetic\": \"Your squid is Energetic. It's always on the move, full of life and vigor. This squid needs plenty of stimulation and activities to keep it happy. It might get restless if not given enough opportunity to burn off its excess energy.\",\r\n    \"desc_introvert\": \"Your squid is an Introvert. It enjoys solitude and might prefer quieter, less crowded spaces. While it can interact with others, it may need time alone to 'recharge'. This squid might be more observant and thoughtful in its actions.\",\r\n    \"desc_greedy\": \"Your squid is Greedy. It has a strong focus on food and resources. This squid might be more motivated by treats and rewards than others. While it can be more demanding, it also tends to be resourceful and good at finding hidden treats!\",\r\n    \"desc_stubborn\": \"Your squid is Stubborn. It has a strong will and definite preferences. This squid might be more resistant to change and could take longer to adapt to new routines. However, its determination can also make it persistent in solving problems.\",\r\n\r\n    # Personality Short Modifiers\r\n    \"mod_timid\": \"Higher chance of becoming anxious\",\r\n    \"mod_adventurous\": \"Increased curiosity and exploration\",\r\n    \"mod_lazy\": \"Slower movement and energy consumption\",\r\n    \"mod_energetic\": \"Faster movement and higher activity levels\",\r\n    \"mod_introvert\": \"Prefers solitude and quiet environments\",\r\n    \"mod_greedy\": \"More focused on food and resources\",\r\n    \"mod_stubborn\": \"Only eats favorite food (sushi), may refuse to sleep\",\r\n\r\n    # Personality Modifier Details\r\n    \"modifiers_timid\": \"- Anxiety increases 50% faster\\n- Curiosity increases 50% slower\\n- Anxiety decreases by 50% when near plants\",\r\n    \"modifiers_adventurous\": \"- Curiosity increases 50% faster\",\r\n    \"modifiers_lazy\": \"- Moves slower\\n- Energy consumption is lower\",\r\n    \"modifiers_energetic\": \"- Moves faster\\n- Energy consumption is higher\",\r\n    \"modifiers_introvert\": \"- Prefers quieter, less crowded spaces\\n- May need more time alone to 'recharge'\",\r\n    \"modifiers_greedy\": \"- Gets 50% more anxious when hungry\\n- Satisfaction increases more when eating\",\r\n    \"modifiers_stubborn\": \"- Prefers favorite food (sushi)\\n- May refuse to sleep even when tired\",\r\n\r\n    # Care Tips\r\n    \"tips_timid\": \"- Place plants in the environment to reduce anxiety\\n- Keep the environment clean and calm\\n- Approach slowly and avoid sudden movements\\n- Maintain a consistent routine\\n- Avoid frequent window resizing which may startle them\",\r\n    \"tips_adventurous\": \"- Regularly introduce new objects or decorations\\n- Provide diverse food options\\n- Encourage exploration with strategic food placement\\n- Allow for lots of exploration space\\n- Enable their natural curiosity with interesting items\",\r\n    \"tips_lazy\": \"- Place food closer to the squid's resting spots\\n- Clean the environment more frequently\\n- Use enticing food to encourage movement\\n- Don't expect much activity - they prefer relaxation\\n- Ensure their favorite resting spots are clean and comfortable\",\r\n    \"tips_energetic\": \"- Provide a large, open space for movement\\n- Offer frequent feeding opportunities\\n- Introduce interactive elements or games\\n- Keep environment stimulating with varied decorations\\n- They need more food due to higher energy consumption\",\r\n    \"tips_introvert\": \"- Create quiet, secluded areas with decorations\\n- Avoid overcrowding the environment\\n- Respect the squid's need for alone time\\n- Create sheltered spaces using plants\\n- Approach gently and give space when needed\",\r\n    \"tips_greedy\": \"- Offer a variety of food types, including sushi\\n- Use food as a reward for desired behaviors\\n- Be cautious not to overfeed\\n- Will get more anxious when hungry compared to other types\\n- Provide opportunities to collect and arrange items\",\r\n    \"tips_stubborn\": \"- Always have sushi available as it's their favorite food\\n- Be patient when introducing changes\\n- Use positive reinforcement for desired behaviors\\n- This squid may refuse non-sushi foods when hungry\\n- May resist sleep even when tired - create calm environments\",\r\n\r\n    # ===== DECISIONS TAB =====\r\n    \"thought_process\": \"Squid's Thought Process\",\r\n    \"step\": \"Step\",\r\n    \"step1_title\": \"Sensing the World\",\r\n    \"step2_title\": \"Calculating Base Urges\",\r\n    \"step3_title\": \"Applying Personality & Memories\",\r\n    \"step4_title\": \"Making the Final Decision\",\r\n    \"final_action\": \"Final Action:\",\r\n    \"awaiting_thought\": \"Awaiting the squid's next thought...\",\r\n    \"awaiting_decision\": \"Awaiting Decision...\",\r\n    \"sensing_condition\": \"The squid assesses his current condition and visible objects:\",\r\n    \"visible_objects\": \"Visible Objects\",\r\n    \"no_sensory_data\": \"No sensory data available.\",\r\n    \"none\": \"None\",\r\n    \"no_urges\": \"No urges calculated.\",\r\n    \"strongest_urge\": \"Based on needs, the strongest urge is\",\r\n    \"initial_scores\": \"Initial scores:\",\r\n    \"personality_memory_adjust\": \"Personality traits and recent memories then adjust these urges:\",\r\n    \"no_adjustments\": \"No significant adjustments from personality or memory this time.\",\r\n    \"final_scores_text\": \"After all calculations, the final scores are tallied. The highest score determines the action.\",\r\n    \"no_final_scores\": \"No final scores available.\",\r\n    \"squid_decided\": \"The squid has decided\",\r\n    \"with_confidence\": \"with a confidence level of\",\r\n    \"score_increased\": \"increased\",\r\n    \"score_decreased\": \"decreased\",\r\n    \"score_for\": \"The score for\",\r\n    \"by_amount\": \"by\",\r\n\r\n    # ===== LEARNING TAB (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"Active Learning Pairs\",\r\n    \"hebbian_cycle\": \"Hebbian Cycle\",\r\n    \"hebbian_paused\": \"PAUSED\",\r\n    \"learning_ready\": \"Learning System Ready\",\r\n    \"learning_ready_desc\": \"Hebbian learning will create associations between neurons that activate together.\",\r\n    \"log_cleared\": \"Log Cleared\",\r\n    \"log_cleared_desc\": \"Learning pairs will appear here as your squid's neurons form new connections.\",\r\n    \"hebbian_overview\": \"Hebbian Learning Overview\",\r\n    \"neurons_fire_together\": \"Neurons that fire together, wire together\",\r\n    \"hebbian_principle\": \"This fundamental principle describes how neural networks learn through experience.\",\r\n    \"hebbian_explanation\": \"Hebbian learning is a simple yet powerful rule used in artificial neural networks. When two neurons activate simultaneously, the connection (weight) between them strengthens. If they activate separately, the connection weakens. This allows the network to form associations between related concepts naturally.\",\r\n    \"excitatory_connections\": \"Excitatory Connections\",\r\n    \"excitatory_desc\": \"Positive weights (0.0-1.0) make neurons more likely to activate together\",\r\n    \"inhibitory_connections\": \"Inhibitory Connections\",\r\n    \"inhibitory_desc\": \"Negative weights (-1.0-0.0) make neurons less likely to activate together\",\r\n    \"very_strong\": \"Very Strong\",\r\n    \"strong\": \"Strong\",\r\n    \"moderate\": \"Moderate\",\r\n    \"weak\": \"Weak\",\r\n    \"very_weak\": \"Very Weak\",\r\n    \"inhibited\": \"Inhibited\",\r\n\r\n    # ===== MEMORY TAB =====\r\n    \"memory\": \"Memory\",\r\n    \"memories\": \"Memories\",\r\n    \"short_term_memory\": \"Short-term Memory\",\r\n    \"long_term_memory\": \"Long-term Memory\",\r\n    \"no_memories\": \"No memories stored yet.\",\r\n    \"overview\": \"Overview\",\r\n    \"memory_stats\": \"Memory Statistics\",\r\n    \"categories\": \"Categories\",\r\n    \"time_label\": \"Time:\",\r\n    \"important_label\": \"Important\",\r\n    \"unknown\": \"Unknown\",\r\n    \"category_label\": \"Category:\",\r\n    \"key_label\": \"Key:\",\r\n    \"access_count\": \"Access count:\",\r\n    \"full_content\": \"Full Content:\",\r\n    \"effects_label\": \"Effects:\",\r\n    \"positive\": \"Positive\",\r\n    \"negative\": \"Negative\",\r\n    \"neutral\": \"Neutral\",\r\n\r\n    # ===== NETWORK TAB (ORIGINAL) =====\r\n    \"brain_network\": \"Brain Network\",\r\n    \"neurons\": \"Neurons\",\r\n    \"connections\": \"Connections\",\r\n    \"activity\": \"Activity\",\r\n\r\n    # ===== STATISTICS WINDOW =====\r\n    \"status\": \"Status\",\r\n    \"health\": \"Health\",\r\n\r\n    # ===== NEURON NAMES (NEW) =====\r\n    \"hunger\": \"Hunger\",\r\n    \"happiness\": \"Happiness\",\r\n    \"cleanliness\": \"Cleanliness\",\r\n    \"sleepiness\": \"Sleepiness\",\r\n    \"satisfaction\": \"Satisfaction\",\r\n    \"curiosity\": \"Curiosity\",\r\n    \"anxiety\": \"Anxiety\",\r\n    \"can_see_food\": \"Can See Food\",\r\n    \"is_eating\": \"Is Eating\",\r\n    \"is_sleeping\": \"Is Sleeping\",\r\n    \"is_sick\": \"Is Sick\",\r\n    \"is_fleeing\": \"Fleeing\",\r\n    \"is_startled\": \"Startled\",\r\n    \"pursuing_food\": \"Pursuing Food\",\r\n    \"external_stimulus\": \"Stimulus\",\r\n    \"plant_proximity\": \"Near Plant\",\r\n    \"stress\": \"Stress\",\r\n    \"novelty\": \"Novelty\",\r\n    \"reward\": \"Reward\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS (NEW) =====\r\n    \"layer_name\": \"Layer\",\r\n    \"layer_input\": \"Input\",\r\n    \"layer_output\": \"Output\",\r\n    \"layer_hidden\": \"Hidden\",\r\n\r\n    # ===== NEUROGENESIS LOGS (NEW) =====\r\n    \"log_created\": \"{time} - a {type} neuron ({name}) was created because {type} counter was {value:.2f}\",\r\n    \"log_pruned\": \"{time} - a neuron ({name}) was PRUNED due to {reason}\",\r\n    \"log_stress_detail\": \"An inhibitory connection was made to ANXIETY\\nMaximum anxiety value has been permanently reduced by 10\",\r\n\r\n    # State Pills\r\n    \"fleeing\": \"Fleeing!\",\r\n    \"startled\": \"Startled!\",\r\n    \"eating\": \"Eating\",\r\n    \"sleeping\": \"Sleeping\",\r\n    \"playing\": \"Playing\",\r\n    \"hiding\": \"Hiding\",\r\n    \"anxious\": \"Anxious\",\r\n    \"curious\": \"Curious\",\r\n\r\n    # ===== COMMON ACTIONS =====\r\n    \"eat\": \"Eat\",\r\n    \"sleep\": \"Sleep\",\r\n    \"play\": \"Play\",\r\n    \"explore\": \"Explore\",\r\n    \"rest\": \"Rest\",\r\n    \"hide\": \"Hide\",\r\n    \"wander\": \"Wander\",\r\n    \"idle\": \"Idle\",\r\n    \"seek_food\": \"Seek Food\",\r\n    \"seek_shelter\": \"Seek Shelter\",\r\n\r\n    # ===== OBJECTS =====\r\n    \"food\": \"Food\",\r\n    \"rock\": \"Rock\",\r\n    \"poop\": \"Poop\",\r\n    \"plant\": \"Plant\",\r\n    \"sushi\": \"Sushi\",\r\n    \"decoration\": \"Decoration\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"A squid has hatched and you must look after him!\",\r\n    \"tutorial_feed\": \"Feed him when he's hungry (Actions Menu)\",\r\n    \"tutorial_clean\": \"Clean his tank when it gets dirty\",\r\n    \"tutorial_watch\": \"Watch his behavior to learn about his personality\",\r\n    \"tutorial_neural\": \"NEURAL NETWORK\",\r\n    \"tutorial_neural_desc\": \"This is the squid's neural network. His behaviour is driven by his needs (round neurons).\\nThe network adapts and learns as the squid interacts with his environment.\",\r\n    \"tutorial_satisfaction\": \"Keep satisfaction high and anxiety low.\",\r\n    \"tutorial_traits\": \"Your squid will develop unique traits and behaviors based on how you raise him.\",\r\n\r\n    # ===== BRAIN DESIGNER (Templates) =====\r\n    \"designer_title\": \"Brain Designer\",\r\n    \"required_only\": \"Required Only\",\r\n    \"dosidicus_default\": \"Dosidicus Default\",\r\n    \"full_sensors\": \"Full Sensor Suite\",\r\n    \"the_insomniac\": \"The Insomniac\",\r\n    \"the_hyperactive\": \"The Hyperactive\",\r\n    \"the_hangry\": \"The Hangry\",\r\n    \"the_depressive\": \"The Depressive\",\r\n    \"the_obsessive\": \"The Obsessive\",\r\n    \"balanced\": \"Balanced\",\r\n    \"minimal\": \"Minimal\",\r\n    \"dense\": \"Dense\",\r\n    \"chaotic\": \"Chaotic\",\r\n    \"calm\": \"Calm\",\r\n\r\n    # ===== SPLASH SCREEN =====\r\n    \"squid_hatched\": \"A SQUID HAS HATCHED!\",\r\n    \"look_after\": \"YOU NEED TO LOOK AFTER HIM..\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"Learning\",\r\n    \"tab_decisions\": \"Decisions\",\r\n    \"tab_personality\": \"Personality\",\r\n    \"tab_about\": \"About\",\r\n\r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"Neuron Inspector\",\r\n    \"lbl_name\": \"Name:\",\r\n    \"lbl_value\": \"Current Value:\",\r\n    \"lbl_position\": \"Position:\",\r\n    \"lbl_type\": \"Type:\",\r\n    \"grp_neurogenesis\": \"Neurogenesis Details\",\r\n    \"lbl_created\": \"Created At:\",\r\n    \"lbl_trigger\": \"Trigger Type:\",\r\n    \"lbl_trigger_val\": \"Trigger Value:\",\r\n    \"lbl_state\": \"Associated State:\",\r\n    \"col_connected\": \"Connected To\",\r\n    \"col_weight\": \"Weight\",\r\n    \"col_direction\": \"Direction\",\r\n    \"btn_refresh_data\": \"Refresh Data\",\r\n    \"type_core\": \"Core\",\r\n    \"type_neuro\": \"Neurogenesis\",\r\n    \"type_system\": \"System Status\",\r\n    \"direction_incoming\": \"Incoming\",\r\n    \"direction_outgoing\": \"Outgoing\",\r\n\r\n    # ===== TUTORIAL STEPS =====\r\n    \"next\": \"Next\",\r\n    \"tutorial_step1_text\": \"A squid has hatched and you must look after him!\\n• Feed him when he's hungry (Actions Menu)\\n• Clean his tank when it gets dirty\\n• Watch his behavior to learn about his personality\",\r\n    \"tutorial_step2_text\": \"This is the squid's neural network. His behaviour is driven by his needs (neurones).\\nThe network adapts and learns as the squid interacts with his environment.\",\r\n    \"tutorial_step3_text\": \"The squid can generate new neurons in response to extreme environmental stimulus.\\nThese new neurons help the squid adapt to challenging situations.\",\r\n    \"tutorial_step4_text\": \"When a pair of neurons fire at the same time, their connection strengthens. This allows the squid to learn associations between different stimuli and responses.\",\r\n    \"tutorial_step5_text\": \"The neural network makes decisions based on current needs and past memories.\\nEach decision affects the squid's state and shapes future behavior.\",\r\n    \"tutorial_step6_text\": \"Press D at any time to open the Decorations window\\nDrag and drop decorations into the environment and see how squid reacts to different things. Each decoration type affects the squid's mental state in unique ways. Click and use the mouse wheel to resize/DEL to delete\",\r\n    \"tutorial_step7_text\": \"Keep satisfaction high and anxiety low.\\nYour squid will develop unique traits and behaviors based on how you raise him.\",\r\n\r\n    # ===== NETWORK & LEARNING TABS (NEW) =====\r\n    \"stats_neurons\": \"Neurons\",\r\n    \"stats_connections\": \"Connections\",\r\n    \"stats_health\": \"Network Health\",\r\n    \"emergency_alert\": \"🚨 Emergency: {name}\",\r\n    \"global_cooldown\": \"Cooldown\",\r\n    \"style_label\": \"Style:\",\r\n    \"chk_links\": \"Show links\",\r\n    \"chk_weights\": \"Show weights\",\r\n    \"chk_pruning\": \"Enable pruning\",\r\n    \"tooltip_brain_designer\": \"Show Brain Designer\",\r\n    \"msg_already_open\": \"Already Open\",\r\n    \"msg_designer_running\": \"Brain Designer is already running!\",\r\n    \"msg_launch_failed\": \"Launch Failed\",\r\n    \"msg_designer_fail\": \"Could not start Brain Designer:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"Missing Brain\",\r\n    \"msg_cannot_open_lab\": \"Cannot open Neuron Laboratory: Brain Widget is not available.\",\r\n    \"msg_cannot_open_buffer\": \"Cannot open Experience Buffer: Brain Widget is not available.\",\r\n    \"msg_no_neurogenesis\": \"No Neurogenesis\",\r\n    \"msg_neurogenesis_not_init\": \"Cannot open Experience Buffer: Neurogenesis system is not initialized.\",\r\n    \"msg_decorations_unavailable\": \"Decorations Unavailable\",\r\n    \"msg_decorations_fail\": \"Cannot open Decorations: Window is not available.\",\r\n    \"func_neurons_title\": \"Functional Neurons\",\r\n    \"count_label\": \"Count\",\r\n    \"avg_utility_label\": \"Avg Utility\",\r\n    \"total_activations_label\": \"Total Activations\",\r\n    \"specialisations_label\": \"Specialisations\",\r\n    \"buffer_title\": \"Neurogenesis Experience Buffer\",\r\n    \"buffer_header\": \"Recent Experiences\",\r\n    \"col_type\": \"Type\",\r\n    \"col_pattern\": \"Pattern\",\r\n    \"col_outcome\": \"Outcome\",\r\n    \"col_time\": \"Time\",\r\n    \"btn_refresh\": \"Refresh\",\r\n    \"buffer_size\": \"Buffer size\",\r\n    \"top_patterns\": \"Top patterns\",\r\n    \"no_patterns\": \"No patterns yet\",\r\n\r\n    # Learning Tab Educational Content\r\n    \"learning_pairs_tab\": \"Learning Pairs\",\r\n    \"mechanics_tab\": \"Mechanics\",\r\n    \"hebbian_quote\": \"\\\"Neurons that fire together, wire together\\\"\",\r\n    \"in_practice_title\": \"In Practice\",\r\n    \"in_practice_text\": \"In your squid's brain, Hebbian learning helps associate related states like 'hunger' with 'satisfaction' when feeding occurs, or 'curiosity' with 'anxiety' during exploration. These learned associations influence future behavior.\",\r\n    \"mechanics_title\": \"Learning Mechanics\",\r\n    \"mechanics_intro\": \"Hebbian learning updates connection strength (weight) between neurons based on their activity patterns. When neurons activate together, their connection strengthens; when they activate separately, it weakens.\",\r\n    \"learning_rule_title\": \"The Learning Rule\",\r\n    \"where_label\": \"Where:\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = Change in weight between two neurons\",\r\n    \"eta_desc\": \"<b>η</b> (eta) = Learning rate (controls speed of change)\",\r\n    \"activation_desc\": \"<b>x, y</b> = Activation values of the neurons (1 if active, 0 if inactive)\",\r\n    \"example_calc_title\": \"Example Calculation\",\r\n    \"scenario_label\": \"<b>Scenario:</b> 'hunger' and 'satisfaction' both activate\",\r\n    \"calc_result\": \"The weight increases by 0.1, strengthening the connection between these neurons.\",\r\n    \"over_time_title\": \"Over Time\",\r\n    \"over_time_text\": \"Through repeated activations, these small weight changes accumulate. Frequently co-occurring patterns develop strong connections, while rarely occurring patterns develop weak or negative connections. This is how your squid learns from experience!\",\r\n    \"str_excitatory\": \"Strong Excitatory\",\r\n    \"weak_excitatory\": \"Weak Excitatory\",\r\n    \"weak_inhibitory\": \"Weak Inhibitory\",\r\n    \"str_inhibitory\": \"Strong Inhibitory\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS =====\r\n    \"distance_rollover\": \"🌊 Distance counter rolled over! Now at {multiplier}x\",\r\n    \"time_min\": \"min\",\r\n    \"time_mins\": \"mins\",\r\n    \"time_hr\": \"hr\",\r\n    \"time_hrs\": \"hrs\",\r\n    \"time_fmt_hm\": \"{hours}h {minutes}m\",\r\n    \"stat_squid_age\": \"Squid Age\",\r\n    \"stat_distance\": \"Distance Swam (pixels)\",\r\n    \"stat_cheese\": \"Cheese Eaten\",\r\n    \"stat_sushi\": \"Sushi Eaten\",\r\n    \"stat_poops\": \"Poops Created\",\r\n    \"stat_max_poops\": \"Max Poops in Tank\",\r\n    \"stat_startles\": \"Times Startled\",\r\n    \"stat_ink\": \"Ink Clouds Created\",\r\n    \"stat_colour_change\": \"Times Colour Changed\",\r\n    \"stat_rocks\": \"Rocks Thrown\",\r\n    \"stat_plants\": \"Plant Interactions\",\r\n    \"stat_sleep\": \"Total Sleep Time (seconds)\",\r\n    \"stat_sickness\": \"Sickness Episodes\",\r\n    \"stat_novelty_neurons\": \"Novelty Neurons Created\",\r\n    \"stat_stress_neurons\": \"Stress Neurons Created\",\r\n    \"stat_reward_neurons\": \"Reward Neurons Created\",\r\n    \"stat_current_neurons\": \"Current Neurons\",\r\n\r\n    \"reset_stats_title\": \"Reset Statistics\",\r\n    \"reset_stats_msg\": \"Are you sure you want to reset all statistics?\",\r\n    \"export_stats_title\": \"Export Statistics\",\r\n    \"export_file_type\": \"Text Files (*.txt)\",\r\n    \"export_header\": \"Squid Statistics Export\",\r\n    \"export_time\": \"Export Time\",\r\n    \"export_activity_section\": \"Activity Statistics\",\r\n    \"export_end\": \"End of Statistics\",\r\n    \"export_success_title\": \"Export Successful\",\r\n    \"export_success_msg\": \"Statistics exported to {file_name}\",\r\n    \"export_error_title\": \"Export Error\",\r\n    \"export_error_msg\": \"Error exporting statistics: {error}\",\r\n\r\n    # ===== ACHIEVEMENTS (NEW) =====\r\n    # Categories\r\n    \"cat_feeding\": \"Feeding\",\r\n    \"cat_neurogenesis\": \"Neurogenesis\",\r\n    \"cat_sleep\": \"Sleep\",\r\n    \"cat_milestones\": \"Milestones\",\r\n    \"cat_exploration\": \"Exploration\",\r\n    \"cat_cleaning\": \"Cleaning\",\r\n    \"cat_health\": \"Health\",\r\n    \"cat_interaction\": \"Interaction\",\r\n    \"cat_ink\": \"Ink\",\r\n    \"cat_memory\": \"Memory\",\r\n    \"cat_emotional\": \"Emotional\",\r\n    \"cat_secret\": \"Secret\",\r\n    \"cat_meta\": \"Meta\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"Points\",\r\n    \"ui_unlocked\": \"Unlocked\",\r\n    \"ui_achievement_unlocked\": \"Achievement Unlocked!\",\r\n    \"ui_hidden\": \"Hidden achievement\",\r\n    \"ui_all\": \"All\",\r\n    \"ui_points_gained\": \"points\",\r\n\r\n    # --- Achievements ---\r\n\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"First Bite\",\r\n    \"ach_first_feeding_desc\": \"Feed the squid for the first time\",\r\n    \"ach_fed_10_times_name\": \"Regular Meals\",\r\n    \"ach_fed_10_times_desc\": \"Feed the squid 10 times\",\r\n    \"ach_fed_50_times_name\": \"Dedicated Caretaker\",\r\n    \"ach_fed_50_times_desc\": \"Feed the squid 50 times\",\r\n    \"ach_fed_100_times_name\": \"Master Chef\",\r\n    \"ach_fed_100_times_desc\": \"Feed the squid 100 times\",\r\n    \"ach_fed_500_times_name\": \"Culinary Legend\",\r\n    \"ach_fed_500_times_desc\": \"Feed the squid 500 times\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"Brain Spark\",\r\n    \"ach_first_neuron_desc\": \"Create the first neurogenesis neuron\",\r\n    \"ach_neurons_10_name\": \"Neural Network\",\r\n    \"ach_neurons_10_desc\": \"Create 10 neurons through neurogenesis\",\r\n    \"ach_neurons_50_name\": \"Expanding Mind\",\r\n    \"ach_neurons_50_desc\": \"Create 50 neurons through neurogenesis\",\r\n    \"ach_neurons_100_name\": \"Cerebral Powerhouse\",\r\n    \"ach_neurons_100_desc\": \"Create 100 neurons through neurogenesis\",\r\n    \"ach_first_neuron_levelup_name\": \"Strengthened Synapse\",\r\n    \"ach_first_neuron_levelup_desc\": \"Level up a neuron for the first time\",\r\n    \"ach_neuron_max_level_name\": \"Peak Performance\",\r\n    \"ach_neuron_max_level_desc\": \"Level a neuron to maximum strength\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"Sweet Dreams\",\r\n    \"ach_first_sleep_desc\": \"The squid wakes from its first sleep\",\r\n    \"ach_slept_10_times_name\": \"Well Rested\",\r\n    \"ach_slept_10_times_desc\": \"The squid has slept 10 times\",\r\n    \"ach_dream_state_name\": \"Deep Dreamer\",\r\n    \"ach_dream_state_desc\": \"Squid entered REM sleep\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"One Hour Old\",\r\n    \"ach_age_1_hour_desc\": \"Squid reached 1 hour old\",\r\n    \"ach_age_10_hours_name\": \"Growing Up\",\r\n    \"ach_age_10_hours_desc\": \"Squid reached 10 hours old\",\r\n    \"ach_age_24_hours_name\": \"One Day Wonder\",\r\n    \"ach_age_24_hours_desc\": \"Squid survived for 24 hours\",\r\n    \"ach_age_1_week_name\": \"Week Veteran\",\r\n    \"ach_age_1_week_desc\": \"Squid has lived for one week\",\r\n    \"ach_age_1_month_name\": \"Month Veteran\",\r\n    \"ach_age_1_month_desc\": \"Squid has lived for one month\",\r\n    \"ach_happiness_100_name\": \"Pure Bliss\",\r\n    \"ach_happiness_100_desc\": \"Reach 100% happiness\",\r\n    \"ach_all_stats_high_name\": \"Perfect Balance\",\r\n    \"ach_all_stats_high_desc\": \"All stats above 80% simultaneously\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"First Scrub\",\r\n    \"ach_first_clean_desc\": \"Clean the tank for the first time\",\r\n    \"ach_cleaned_25_times_name\": \"Spotless Environment\",\r\n    \"ach_cleaned_25_times_desc\": \"Clean the tank 25 times\",\r\n    \"ach_germaphobe_name\": \"Germaphobe\",\r\n    \"ach_germaphobe_desc\": \"Keep cleanliness above 90% for 1 hour straight\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"First Aid\",\r\n    \"ach_first_medicine_desc\": \"Give medicine for the first time\",\r\n    \"ach_medicine_10_times_name\": \"Doctor Squid\",\r\n    \"ach_medicine_10_times_desc\": \"Give medicine 10 times\",\r\n    \"ach_comeback_kid_name\": \"Comeback Kid\",\r\n    \"ach_comeback_kid_desc\": \"Recover from critically low health (<20%) to full\",\r\n\r\n    # Interaction (Rocks)\r\n    \"ach_first_rock_pickup_name\": \"Rock Collector\",\r\n    \"ach_first_rock_pickup_desc\": \"Pick up a rock for the first time\",\r\n    \"ach_rocks_picked_10_name\": \"Stone Gatherer\",\r\n    \"ach_rocks_picked_10_desc\": \"Pick up 10 rocks\",\r\n    \"ach_rocks_picked_50_name\": \"Boulder Hoarder\",\r\n    \"ach_rocks_picked_50_desc\": \"Pick up 50 rocks\",\r\n    \"ach_first_rock_throw_name\": \"Skipping Stones\",\r\n    \"ach_first_rock_throw_desc\": \"Throw a rock for the first time\",\r\n    \"ach_rocks_thrown_25_name\": \"Rock Launcher\",\r\n    \"ach_rocks_thrown_25_desc\": \"Throw 25 rocks\",\r\n    \"ach_rocks_thrown_100_name\": \"Catapult Master\",\r\n    \"ach_rocks_thrown_100_desc\": \"Throw 100 rocks\",\r\n\r\n    # Interaction (Decor)\r\n    \"ach_first_decoration_push_name\": \"Interior Decorator\",\r\n    \"ach_first_decoration_push_desc\": \"Push a decoration for the first time\",\r\n    \"ach_decorations_pushed_10_name\": \"Furniture Mover\",\r\n    \"ach_decorations_pushed_10_desc\": \"Push decorations 10 times\",\r\n    \"ach_decorations_pushed_50_name\": \"Feng Shui Master\",\r\n    \"ach_decorations_pushed_50_desc\": \"Push decorations 50 times\",\r\n    \"ach_first_plant_interact_name\": \"Green Thumb\",\r\n    \"ach_first_plant_interact_desc\": \"Interact with a plant for the first time\",\r\n    \"ach_plants_interacted_10_name\": \"Garden Explorer\",\r\n    \"ach_plants_interacted_10_desc\": \"Interact with plants 10 times\",\r\n    \"ach_plants_interacted_50_name\": \"Botanist\",\r\n    \"ach_plants_interacted_50_desc\": \"Interact with plants 50 times\",\r\n    \"ach_objects_investigated_25_name\": \"Curious Inspector\",\r\n    \"ach_objects_investigated_25_desc\": \"Investigate 25 different objects\",\r\n    \"ach_objects_investigated_100_name\": \"Master Detective\",\r\n    \"ach_objects_investigated_100_desc\": \"Investigate 100 different objects\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"Mischief Maker\",\r\n    \"ach_first_poop_throw_desc\": \"Squid threw a poop for the first time\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"Smoke Screen\",\r\n    \"ach_first_ink_cloud_desc\": \"Squid releases ink cloud for the first time\",\r\n    \"ach_ink_clouds_20_name\": \"Ink Master\",\r\n    \"ach_ink_clouds_20_desc\": \"Release 20 ink clouds\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"First Memory\",\r\n    \"ach_first_memory_desc\": \"Form the first memory\",\r\n    \"ach_memory_long_term_name\": \"Long Term Thinking\",\r\n    \"ach_memory_long_term_desc\": \"Promote a memory to long-term storage\",\r\n    \"ach_memories_50_name\": \"Photographic Memory\",\r\n    \"ach_memories_50_desc\": \"Have 50 memories stored\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"Curious George\",\r\n    \"ach_curiosity_100_desc\": \"Curiosity reaches 100%\",\r\n    \"ach_zen_master_name\": \"Zen Master\",\r\n    \"ach_zen_master_desc\": \"Keep anxiety below 10% for 30 minutes\",\r\n    \"ach_first_startle_name\": \"Startled!\",\r\n    \"ach_first_startle_desc\": \"Startle the squid for the first time\",\r\n    \"ach_nervous_wreck_name\": \"Nervous Wreck\",\r\n    \"ach_nervous_wreck_desc\": \"Anxiety reaches 100%\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"Night Owl\",\r\n    \"ach_night_owl_desc\": \"Play between midnight and 4 AM\",\r\n    \"ach_early_bird_name\": \"Early Bird\",\r\n    \"ach_early_bird_desc\": \"Play between 5 AM and 7 AM\",\r\n    \"ach_weekend_warrior_name\": \"Weekend Warrior\",\r\n    \"ach_weekend_warrior_desc\": \"Play on both Saturday and Sunday\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"Brain Surgeon\",\r\n    \"ach_brain_surgeon_desc\": \"Open the brain visualization tool\",\r\n    \"ach_speed_demon_name\": \"Speed Demon\",\r\n    \"ach_speed_demon_desc\": \"Run simulation at max speed for 10 minutes\",\r\n    \"ach_completionist_name\": \"Completionist\",\r\n    \"ach_completionist_desc\": \"Unlock 30 other achievements\",\r\n\r\n    # ===== NEURON LABORATORY =====\r\n    \"lab_title\": \"🧠 Neuron Laboratory\",\r\n    \"lab_live_refresh\": \"Live refresh\",\r\n    \"lab_unlock_editing\": \"🔓 Unlock editing\",\r\n    \"lab_tab_overview\": \"📊 Live Overview\",\r\n    \"lab_tab_inspector\": \"🔍 Deep Inspector\",\r\n    \"lab_tab_edit\": \"🔧 Edit Sandbox\",\r\n    \"lab_status_ready\": \"Ready\",\r\n    \"lab_status_locked\": \"🔒 {name} locked at {value}\",\r\n    \"lab_status_unlocked\": \"🔓 {name} unlocked\",\r\n    \r\n    # Overview Tab\r\n    \"lab_ov_counters\": \"Counter progress\",\r\n    \"lab_ov_newest\": \"Newest neurogenesis neurons\",\r\n    \"lab_ov_limits\": \"Limits & pruning\",\r\n    \"lab_ov_actions\": \"Quick actions\",\r\n    \"lab_force_hebbian\": \"Force Hebbian cycle\",\r\n    \"lab_pruning_enabled\": \"Pruning enabled:\",\r\n    \"lab_none_yet\": \"None yet\",\r\n    \"lab_ago\": \"{seconds}s ago\",\r\n    \r\n    # Inspector Tab\r\n    \"lab_pick_neuron\": \"Pick a neuron to inspect:\",\r\n    \"lab_connections_title\": \"Connections (excitatory vs inhibitory)\",\r\n    \"lab_header_partner\": \"Partner\",\r\n    \"lab_header_weight\": \"Weight\",\r\n    \"lab_header_type\": \"Type\",\r\n    \"lab_header_inf\": \"Influence\",\r\n    \"lab_impact_title\": \"Functional impact simulation\",\r\n    \"lab_header_neuron\": \"Neuron\",\r\n    \"lab_header_delta\": \"Δ Value\",\r\n    \"lab_no_connections\": \"No active connections at the moment\",\r\n    \"lab_did_you_know\": \"Did you know?\",\r\n    \"lab_type_excitatory\": \"Excitatory\",\r\n    \"lab_type_inhibitory\": \"Inhibitory\",\r\n    \r\n    # Edit Tab\r\n    \"lab_edit_locked_msg\": \"⚠️ Editing is locked – check 'Unlock editing' in the toolbar.\",\r\n    \"lab_edit_header\": \"Neuron values (drag to change) – click 🔒 to lock\",\r\n    \"lab_unlock_title\": \"Unlock editing?\",\r\n    \"lab_unlock_msg\": \"You can now change neuron values and force creation events. Use responsibly!\",\r\n    \r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"tiny\",\r\n    \"lab_inf_mild\": \"mild\",\r\n    \"lab_inf_mod\": \"moderate\",\r\n    \"lab_inf_strong\": \"STRONG\",\r\n    \r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"Hunger is a homeostatic drive. High hunger inhibits satisfaction and boosts anxiety.\",\r\n    \"lab_tip_happiness\": \"Happiness is reinforced by reward neurons. It inhibits anxiety and promotes curiosity.\",\r\n    \"lab_tip_anxiety\": \"Anxiety is reduced by stress neurons (inhibitory). High anxiety suppresses curiosity.\",\r\n    \"lab_tip_curiosity\": \"Curiosity spikes when novelty is high. It encourages exploration and reduces anxiety.\",\r\n    \"lab_tip_core\": \"Core neuron – fundamental to survival.\",\r\n    \"lab_tip_neuro_default\": \"Neurogenesis neuron – purpose inferred from birth context.\",\r\n    \"lab_tip_neuro_fmt\": \"Created by <b>{trigger}</b> – specialises in <b>{spec}</b>. Its job is to turn experiences into long-term behaviour.\",\r\n\r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"Squid's Vision\",\r\n    \"vis_logic_unavailable\": \"Squid logic not available.\",\r\n    \"vis_nothing_in_view\": \"Nothing currently in view.\",\r\n    \"vis_distance\": \"distance\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"Specialization\",\r\n    \"tooltip_type\": \"Type\",\r\n    \"tooltip_current\": \"Current\",\r\n    \"tooltip_utility\": \"Utility\",\r\n    \"tooltip_activations\": \"Activations\",\r\n    \"tooltip_last_active\": \"Last Active\",\r\n    \"tooltip_age\": \"Age\",\r\n    \"tooltip_core\": \"Core\",\r\n    \"tooltip_generated\": \"Generated\",\r\n    \"tooltip_functional\": \"Functional\",\r\n    \"tooltip_connections_header\": \"Connections\",\r\n    \"tooltip_connections_stats\": \"{incoming} in, {outgoing} out\",\r\n    \"tooltip_top_incoming\": \"Top Incoming\",\r\n    \"tooltip_top_outgoing\": \"Top Outgoing\",\r\n    \"tooltip_hint\": \"Double-click to inspect • Right-click for options\",\r\n\r\n    # State values\r\n    \"state_on\": \"ON\",\r\n    \"state_off\": \"OFF\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"{val}s ago\",\r\n    \"fmt_m_ago\": \"{val}m ago\",\r\n    \"fmt_h_ago\": \"{val}h ago\",\r\n    \"fmt_s_short\": \"{val}s\",\r\n    \"fmt_m_short\": \"{val}m\",\r\n    \"fmt_h_short\": \"{val}h\",\r\n    \"fmt_d_short\": \"{val}d\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW UI =====\r\n    \"designer_window_title\": \"Brain Designer - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"Brain Designer - Dosidicus-2 [Imported from Game]\",\r\n    \r\n    # Tabs\r\n    \"designer_tab_layers\": \"Layers\",\r\n    \"designer_tab_sensors\": \"Sensors\",\r\n    \"designer_tab_props\": \"Properties\",\r\n    \"designer_tab_connections\": \"Connections\",\r\n    \"designer_tab_outputs\": \"Outputs\",\r\n    \r\n    # Toolbar\r\n    \"designer_btn_generate\": \"🎲 Generate Sparse Network\",\r\n    \"designer_tooltip_generate\": \"Generate random connections between core neurons\",\r\n    \"designer_btn_neuron\": \"➕ Neuron\",\r\n    \"designer_tooltip_neuron\": \"Add a new neuron (Shift+N)\",\r\n    \"designer_btn_fix\": \"🔧 Auto-Fix\",\r\n    \"designer_tooltip_fix\": \"Automatically fix orphan neurons and connectivity issues\",\r\n    \"designer_btn_validate\": \"✓ Validate\",\r\n    \"designer_tooltip_validate\": \"Check design for issues\",\r\n    \"designer_btn_sync\": \"🔄 Sync from Game\",\r\n    \"designer_tooltip_sync\": \"Refresh brain state from running Dosidicus game\",\r\n    \"designer_btn_clear_conn\": \"🗑 Clear Connections\",\r\n    \"designer_tooltip_clear_conn\": \"Remove all connections (keeps neurons)\",\r\n    \"designer_tooltip_dice\": \"Instantly generate a random network (no dialog)\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 <b>Left-Drag</b> from neuron to create connection\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+Drag</b> neuron to move it\",\r\n    \"designer_help_pan\": \"<b>Right-Drag</b> to pan canvas\",\r\n    \"designer_help_zoom\": \"<b>Scroll Wheel</b> to zoom (or adjust weight on connection)\",\r\n    \"designer_help_edit_weight\": \"<b>Double-Click</b> connection to edit weight\",\r\n    \"designer_help_select\": \"<b>Click</b> neuron/connection to select\",\r\n    \"designer_help_delete\": \"<b>Del</b> to delete selected\",\r\n    \"designer_help_reverse\": \"<b>Space</b> to reverse connection direction\",\r\n    \"designer_help_keys_weight\": \"<b>+/-</b> keys to adjust weight (Shift for larger steps)\",\r\n    \"designer_help_page_weight\": \"<b>Page Up/Down</b> to adjust weight (large steps)\",\r\n    \"designer_help_add_neuron\": \"<b>Shift+N</b> to add neuron\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b> to save\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b> to open\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b> to export\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b> for new design\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b> to generate network\",\r\n    \"designer_help_dice\": \"🎲 <b>Dice button</b> for instant random generation\",\r\n    \"designer_help_outputs\": \"<b>Outputs tab</b> to bind neurons to squid behaviors\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"File\",\r\n    \"designer_menu_edit\": \"Edit\",\r\n    \"designer_menu_templates\": \"Templates\",\r\n    \"designer_menu_generate\": \"Generate\",\r\n    \r\n    # Actions\r\n    \"designer_action_new\": \"New Design\",\r\n    \"designer_action_save\": \"Save...\",\r\n    \"designer_action_export\": \"Export for Dosidicus...\",\r\n    \"designer_action_open\": \"Open...\",\r\n    \"designer_action_gen_sparse\": \"Generate Sparse Network...\",\r\n    \"designer_action_autofix\": \"Auto-Fix Connectivity\",\r\n    \"designer_action_validate\": \"Validate Design\",\r\n    \"designer_action_clear_conn\": \"Clear All Connections\",\r\n    \"designer_action_clear_outputs\": \"Clear All Output Bindings\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"Neurons: {count}\",\r\n    \"designer_status_connections\": \"Connections: {count}\",\r\n    \"designer_status_required\": \"Required: {ok}\",\r\n    \"designer_status_outputs\": \"Outputs: {count}\",\r\n    \"designer_status_selected\": \"Selected: {source} → {target} (weight: {weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"Weight updated: {source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"Deleted connection: {source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"Cleared {count} connections\",\r\n    \"designer_status_cleared_out\": \"Cleared {count} output bindings\",\r\n    \"designer_status_generated\": \"Generated {count} connections using '{style}' preset\",\r\n    \"designer_status_random_gen\": \"🎲 Generated {count} random connections (style: {style})\",\r\n    \"designer_status_synced\": \"✨ Synced: {neurons} neurons, {connections} connections\",\r\n    \"designer_status_imported\": \"✨ Active brain imported from running game\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"Game Not Running\",\r\n    \"designer_msg_game_not_running\": \"The Dosidicus game is no longer running.\\n\\nStart the game again to sync.\",\r\n    \"designer_msg_sync_confirm_title\": \"Sync from Game\",\r\n    \"designer_msg_sync_confirm\": \"Replace current design with the latest brain state from the game?\",\r\n    \"designer_msg_sync_failed_title\": \"Sync Failed\",\r\n    \"designer_msg_sync_failed\": \"Could not import brain state from game.\",\r\n    \r\n    \"designer_msg_live_import_title\": \"Live Brain Import\",\r\n    \"designer_msg_live_import_header\": \"🧠 Active brain imported from running game\",\r\n    \"designer_msg_live_import_body\": \"The designer is now showing the exact neural network from your running Dosidicus game.\\n\\n• {neurons} neurons\\n• {connections} connections\\n\\nChanges made here will NOT affect the running game.\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"Clear Connections\",\r\n    \"designer_msg_clear_conn_confirm\": \"Remove all {count} connections?\\n\\nNeurons will be kept.\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"Clear Output Bindings\",\r\n    \"designer_msg_clear_out_empty\": \"No output bindings to clear.\",\r\n    \"designer_msg_clear_out_confirm\": \"Remove all {count} output bindings?\",\r\n    \r\n    \"designer_msg_new_design_title\": \"New Design\",\r\n    \"designer_msg_new_design_confirm\": \"Start a new design? Unsaved changes will be lost.\",\r\n    \r\n    \"designer_msg_autofix_title\": \"Auto-Fix\",\r\n    \"designer_msg_autofix_result\": \"Created {count} connections:\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"No issues found.\",\r\n    \r\n    \"designer_msg_save_title\": \"Save Design\",\r\n    \"designer_msg_saved_title\": \"Saved\",\r\n    \"designer_msg_save_success\": \"Design saved successfully: {msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n({count} output bindings included)\",\r\n    \"designer_msg_error_title\": \"Error\",\r\n    \"designer_msg_save_fail\": \"Failed to save design:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"Export\",\r\n    \"designer_msg_exported_title\": \"Exported\",\r\n    \"designer_msg_export_success\": \"Design exported successfully\",\r\n    \"designer_msg_export_fail\": \"Failed to export design:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"Open Design\",\r\n    \"designer_msg_open_fail\": \"Could not load design:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"Load Template\",\r\n    \"designer_msg_select_template\": \"Select a template:\",\r\n    \"designer_msg_replace_design\": \"Replace current design?\",\r\n    \r\n    \"designer_msg_status_title\": \"Design Status\",\r\n    \"designer_msg_status_ok\": \"\\n✅ Status: OK\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ ISSUES:\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"Connection Weight\",\r\n    \"designer_input_weight_label\": \"Set weight for {source} → {target}:\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"No neuron selected\",\r\n    \"designer_prop_no_selection_disabled\": \"No Selection\",\r\n    \"designer_prop_lbl_name\": \"Name:\",\r\n    \"designer_prop_lbl_type\": \"Type:\",\r\n    \"designer_prop_lbl_x\": \"X:\",\r\n    \"designer_prop_lbl_y\": \"Y:\",\r\n    \"designer_prop_btn_delete\": \"Delete Neuron\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"Add Neuron\",\r\n    \"designer_add_grp_type\": \"Select Neuron Type\",\r\n    \"designer_add_btn_custom\": \"✨ Custom / Plugin Neuron\",\r\n    \"designer_add_btn_sensor\": \"📡 Input Sensor\",\r\n    \"designer_add_tooltip_custom\": \"Create a neuron with a specific name to link with game plugins\",\r\n    \"designer_add_grp_sensor\": \"Select Sensor\",\r\n    \"designer_add_grp_custom\": \"Define Custom Neuron\",\r\n    \"designer_add_info_custom\": \"<i>To affect the squid, the <b>Name</b> must match a plugin ID.<br>Example: Name it <b>'jet_boost'</b> to activate a jetpack plugin.</i>\",\r\n    \"designer_add_lbl_id\": \"Plugin ID / Name:\",\r\n    \"designer_add_ph_id\": \"e.g. turbo_mode\",\r\n    \"designer_add_btn_create\": \"Create Link\",\r\n    \"designer_add_all_added\": \"All sensors added\",\r\n    \"designer_add_err_title\": \"Error\",\r\n    \"designer_add_err_exists\": \"Exists\",\r\n    \"designer_add_msg_created\": \"Created {name}\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"Add Layer\",\r\n    \"designer_layer_dlg_title\": \"New Layer\",\r\n    \"designer_layer_dlg_label\": \"Name:\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"Input Sensors:\",\r\n    \"designer_sensor_tooltip_refresh\": \"Refresh sensor list (includes plugin-registered sensors)\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"Source\",\r\n    \"designer_conn_header_target\": \"Target\",\r\n    \"designer_conn_header_weight\": \"Weight\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>Output Bindings</b><br><small>Connect neurons to squid behaviors. When a neuron's activation exceeds the threshold, it triggers the bound action.</small>\",\r\n    \"designer_output_btn_add\": \"➕ Add Binding\",\r\n    \"designer_output_btn_edit\": \"✏️ Edit\",\r\n    \"designer_output_btn_remove\": \"🗑️ Remove\",\r\n    \"designer_output_col_neuron\": \"Neuron\",\r\n    \"designer_output_col_behavior\": \"→ Behavior\",\r\n    \"designer_output_col_threshold\": \"Threshold\",\r\n    \"designer_output_col_mode\": \"Mode\",\r\n    \"designer_output_col_enabled\": \"Enabled\",\r\n    \"designer_output_info\": \"{count} binding(s), {enabled} enabled\",\r\n    \"designer_output_err_missing\": \"⚠️ Neuron not found in design\",\r\n    \"designer_output_dlg_remove_title\": \"Remove Binding\",\r\n    \"designer_output_dlg_remove_msg\": \"Remove binding: {neuron} → {hook}?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"Add Output Binding\",\r\n    \"designer_binding_title_edit\": \"Configure Output Binding\",\r\n    \"designer_binding_grp_neuron\": \"Source Neuron\",\r\n    \"designer_binding_lbl_neuron\": \"Neuron:\",\r\n    \"designer_binding_lbl_current\": \"Current: --\",\r\n    \"designer_binding_grp_hook\": \"Output Behavior\",\r\n    \"designer_binding_lbl_trigger\": \"Trigger:\",\r\n    \"designer_binding_grp_settings\": \"Trigger Settings\",\r\n    \"designer_binding_lbl_thresh\": \"Threshold:\",\r\n    \"designer_binding_lbl_mode\": \"Mode:\",\r\n    \"designer_binding_lbl_cool\": \"Cooldown:\",\r\n    \"designer_binding_chk_enabled\": \"Enabled\",\r\n    \"designer_binding_err_neuron\": \"Please select a neuron\",\r\n    \"designer_binding_err_hook\": \"Please select an output behavior\",\r\n    \"designer_binding_err_duplicate\": \"A binding for {neuron} → {hook} already exists\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"Rising Edge (cross threshold going up)\",\r\n    \"designer_mode_falling\": \"Falling Edge (cross threshold going down)\",\r\n    \"designer_mode_above\": \"While Above (continuous while > threshold)\",\r\n    \"designer_mode_below\": \"While Below (continuous while < threshold)\",\r\n    \"designer_mode_change\": \"On Change (any significant change)\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"Built-in sensor: {name}\",\r\n    \"desc_vision_food\": \"Detects food in vision cone\",\r\n    \"desc_custom_sensor\": \"Custom sensor from {plugin}\",\r\n    \"desc_builtin\": \"builtin\",\r\n    \"desc_plugin\": \"plugin\",\r\n    \"desc_other\": \"other\",\r\n    \"desc_vision\": \"vision\",\r\n    \r\n    # ===== DESIGNER TEMPLATES (Extra Keys) =====\r\n    \"tmpl_core_name\": \"🟡 Required Only\",\r\n    \"tmpl_core_desc\": \"8 required neurons\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Dosidicus Default\",\r\n    \"tmpl_dosidicus_desc\": \"Standard layout\",\r\n    \"tmpl_full_sensors_name\": \"🟡 Full Sensor Suite\",\r\n    \"tmpl_full_sensors_desc\": \"All sensors\",\r\n    \"tmpl_insomniac_name\": \"🔴 The Insomniac\",\r\n    \"tmpl_insomniac_desc\": \"Anxiety & Curiosity block sleep\",\r\n    \"tmpl_hyperactive_name\": \"🔴 The Hyperactive\",\r\n    \"tmpl_hyperactive_desc\": \"Noise neurons overwhelm sleepiness\",\r\n    \"tmpl_hangry_name\": \"🔴 The Hangry\",\r\n    \"tmpl_hangry_desc\": \"Hunger causes extreme rage\",\r\n    \"tmpl_depressive_name\": \"🔴 The Depressive\",\r\n    \"tmpl_depressive_desc\": \"Resistant to happiness\",\r\n    \"tmpl_obsessive_name\": \"🔴 The Obsessive\",\r\n    \"tmpl_obsessive_desc\": \"Anxiety/Curiosity feedback loop\",\r\n    \"layer_sensors\": \"Sensors\",\r\n    \"layer_core\": \"Core\",\r\n    \"layer_input\": \"Input\",\r\n    \"layer_out\": \"Out\",\r\n    \"layer_racing_mind\": \"Racing Mind\",\r\n    \"layer_state\": \"State\",\r\n    \"layer_vision\": \"Vision\",\r\n    \"layer_noise\": \"Noise\",\r\n    \"layer_output\": \"Output\",\r\n    \"layer_gut_brain\": \"Gut-Brain\",\r\n    \"layer_gray\": \"Gray\",\r\n    \"layer_loop\": \"Loop\",\r\n    \"layer_stats\": \"Stats\",\r\n    \"layer_emotions\": \"Emotions\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"Delete Connection\",\r\n    \"designer_cnv_del_conn_msg\": \"Are you sure you want to delete the connection:\\n{source} → {target}?\",\r\n    \"designer_cnv_chk_dont_ask\": \"Don't ask again\",\r\n    \"designer_cnv_btn_del\": \"Yes, Delete\",\r\n    \"designer_cnv_btn_cancel\": \"Cancel\",\r\n    \"designer_cnv_dlg_edit_title\": \"Edit Connection\",\r\n    \"designer_cnv_lbl_conn\": \"Connection: {source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"Weight:\",\r\n    \"designer_cnv_info_weight\": \"Positive = Excitatory (green), Negative = Inhibitory (red)\",\r\n    \"designer_cnv_btn_del_conn\": \"Delete Connection\",\r\n    \"designer_cnv_btn_ok\": \"OK\",\r\n    \"designer_cnv_tooltip_invalid\": \"Invalid connection\",\r\n}"
  },
  {
    "path": "translations/es.py",
    "content": "LANGUAGE_HEADER = \"es - Español\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"Hambre\",\r\n    \"happiness\": \"Felicidad\",\r\n    \"cleanliness\": \"Limpieza\",\r\n    \"sleepiness\": \"Somnolencia\",\r\n    \"satisfaction\": \"Satisfacción\",\r\n    \"anxiety\": \"Ansiedad\",\r\n    \"curiosity\": \"Curiosidad\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"Puede Ver Comida\",\r\n    \"is_eating\": \"Comiendo\",\r\n    \"is_sleeping\": \"Durmiendo\",\r\n    \"is_sick\": \"Enfermo\",\r\n    \"pursuing_food\": \"Persiguiendo Comida\",\r\n    \"is_startled\": \"Asustado\",\r\n    \"is_fleeing\": \"Huyendo\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"Novedad\",\r\n    \"stress\": \"Estrés\",\r\n    \"reward\": \"Recompensa\",\r\n\r\n    # ===== MENU PRINCIPAL =====\r\n    \"file\": \"Archivo\",\r\n    \"new_game\": \"Nuevo Juego\",\r\n    \"load_game\": \"Cargar Juego\",\r\n    \"save_game\": \"Guardar Juego\",\r\n    \"view\": \"Ver\",\r\n    \"speed\": \"Velocidad\",\r\n    \"pause\": \"Pausa\",\r\n    \"actions\": \"Acciones\",\r\n    \"debug\": \"Depurar\",\r\n    \"plugins\": \"Plugins\",\r\n\r\n    # ===== MENU VER =====\r\n    \"brain_designer\": \"Disenador de Cerebro\",\r\n    \"decorations\": \"Decoraciones\",\r\n    \"statistics\": \"Estadisticas\",\r\n    \"brain_tool\": \"Herramienta Cerebral\",\r\n    \"neuron_lab\": \"Laboratorio de Neuronas\",\r\n    \"task_manager\": \"Administrador de Tareas\",\r\n\r\n    # ===== MENU VELOCIDAD =====\r\n    \"normal_speed\": \"Normal (1x)\",\r\n    \"fast_speed\": \"Rapido (2x)\",\r\n    \"very_fast\": \"Muy Rapido (3x)\",\r\n\r\n    # ===== MENU DEPURACION =====\r\n    \"toggle_debug\": \"Alternar Modo Depuracion\",\r\n    \"toggle_cone\": \"Alternar Cono de Vision\",\r\n    \"squid_vision\": \"Vision del Calamar\",\r\n\r\n    # ===== BOTONES DE ACCION =====\r\n    \"feed\": \"Alimentar\",\r\n    \"clean\": \"Limpiar\",\r\n    \"medicine\": \"Medicina\",\r\n    \"feed_btn\": \"ALIMENTAR\",\r\n    \"clean_btn\": \"LIMPIAR\",\r\n    \"medicine_btn\": \"MEDICINA\",\r\n\r\n    # ===== MENSAJES =====\r\n    \"feed_msg\": \"El calamar necesita comida\",\r\n    \"points\": \"Puntos\",\r\n    \"dirty\": \"SUCIO\",\r\n    \"paused_msg\": \"SIMULACION EN PAUSA\",\r\n    \"paused_sub\": \"Usa el menu Velocidad para reanudar\",\r\n\r\n    # ===== DIALOGOS =====\r\n    \"yes\": \"Si\",\r\n    \"no\": \"No\",\r\n    \"ok\": \"Aceptar\",\r\n    \"cancel\": \"Cancelar\",\r\n    \"close\": \"Cerrar\",\r\n    \"save\": \"Guardar\",\r\n    \"load\": \"Cargar\",\r\n    \"reset\": \"Reiniciar\",\r\n    \"apply_changes\": \"Aplicar Cambios\",\r\n    \"got_it\": \"Entendido!\",\r\n    \"finish\": \"Finalizar\",\r\n    \"confirm_new_game\": \"Iniciar un nuevo juego? El progreso actual se perdera.\",\r\n    \"confirm_exit\": \"Estas seguro de que quieres salir?\",\r\n    \"save_successful\": \"Juego guardado exitosamente!\",\r\n    \"load_successful\": \"Juego cargado exitosamente!\",\r\n    \"error_saving\": \"Error al guardar el juego.\",\r\n    \"error_loading\": \"Error al cargar el juego.\",\r\n    \"no_save_found\": \"No se encontro archivo de guardado.\",\r\n    \"startup\": \"Inicio\",\r\n    \"show_tutorial_q\": \"¿Mostrar tutorial?\",\r\n    \"auto_decline\": \"(Autocancelación en {seconds}s)\",\r\n    \"tutorial_title\": \"Tutorial\",\r\n    \"tutorial_query\": \"¿Te gustaría ver el tutorial?\",\r\n\r\n    # ===== PESTANA ACERCA DE =====\r\n    \"hello\": \"HOLA\",\r\n    \"my_name_is\": \"mi nombre es\",\r\n    \"change_name\": \"Cambiar Nombre\",\r\n    \"enter_new_name\": \"Introduce un nuevo nombre para tu calamar:\",\r\n    \"change_colour\": \"Cambiar Color\",\r\n    \"view_certificate\": \"Ver Certificado\",\r\n    \"care_tips\": \"Consejos de Cuidado\",\r\n    \"care_tips_for\": \"Consejos de Cuidado para Calamares {personality}\",\r\n    \"dosidicus_title\": \"Dosidicus electronicus\",\r\n    \"dosidicus_desc\": \"Una mascota digital estilo Tamagotchi con una red neuronal simple\",\r\n    \"string_acronym\": \"Reacciones de Tamagotchi Simuladas mediante Inferencia y Neurogenesis (STRINg)\",\r\n    \"research_project\": \"Este es un proyecto de investigacion. Por favor, sugiere funciones.\",\r\n    \"version_dosidicus\": \"Versión Dosidicus:\",\r\n    \"version_brain_tool\": \"Versión Herramienta Cerebral:\",\r\n    \"version_decision\": \"Versión Motor de Decisión:\",\r\n    \"version_neuro\": \"Versión Neurogénesis:\",\r\n    \"created_by\": \"por\",\r\n\r\n    # ===== PERSONALIDAD =====\r\n    \"squid_personality\": \"Personalidad del Calamar\",\r\n    \"personality_modifier\": \"Modificador de Personalidad\",\r\n    \"description\": \"Descripcion:\",\r\n    \"personality_modifiers\": \"Modificadores de Personalidad:\",\r\n    \"care_tips_label\": \"Consejos de Cuidado:\",\r\n    \"personality_note\": \"Nota: La personalidad se genera aleatoriamente al inicio de un nuevo juego\",\r\n\r\n    # ===== BRAIN TOOL TABS (NEW - SPANISH) =====\r\n    \"tab_learning\": \"Aprendizaje\",\r\n    \"tab_decisions\": \"Decisiones\",\r\n    \"tab_personality\": \"Personalidad\",\r\n    \"tab_about\": \"Acerca de\",\r\n\r\n    # ===== NEURON INSPECTOR (NEW - SPANISH) =====\r\n    \"inspector_title\": \"Inspector de Neuronas\",\r\n    \"lbl_name\": \"Nombre:\",\r\n    \"lbl_value\": \"Valor Actual:\",\r\n    \"lbl_position\": \"Posición:\",\r\n    \"lbl_type\": \"Tipo:\",\r\n    \"grp_neurogenesis\": \"Detalles de Neurogénesis\",\r\n    \"lbl_created\": \"Creado En:\",\r\n    \"lbl_trigger\": \"Tipo Disparador:\",\r\n    \"lbl_trigger_val\": \"Valor Disparador:\",\r\n    \"lbl_state\": \"Estado Asociado:\",\r\n    \"col_connected\": \"Conectado A\",\r\n    \"col_weight\": \"Peso\",\r\n    \"col_direction\": \"Dirección\",\r\n    \"btn_refresh_data\": \"Actualizar Datos\",\r\n    \"type_core\": \"Núcleo\",\r\n    \"type_neuro\": \"Neurogénesis\",\r\n    \"type_system\": \"Estado Sistema\",\r\n    \"direction_incoming\": \"Entrante\",\r\n    \"direction_outgoing\": \"Saliente\",\r\n\r\n    # Tipos de Personalidad\r\n    \"personality_timid\": \"Timido\",\r\n    \"personality_adventurous\": \"Aventurero\",\r\n    \"personality_lazy\": \"Perezoso\",\r\n    \"personality_energetic\": \"Energico\",\r\n    \"personality_introvert\": \"Introvertido\",\r\n    \"personality_greedy\": \"Gloton\",\r\n    \"personality_stubborn\": \"Terco\",\r\n\r\n    # Descripciones de Personalidad\r\n    \"desc_timid\": \"Tu calamar es Timido. Tiende a asustarse y ponerse ansioso con mas facilidad, especialmente en situaciones nuevas. Puede preferir ambientes tranquilos y calmados y podria ser menos propenso a explorar por su cuenta. Sin embargo, puede formar vinculos fuertes cuando se siente seguro y protegido.\",\r\n    \"desc_adventurous\": \"Tu calamar es Aventurero. Le encanta explorar y probar cosas nuevas. A menudo es el primero en investigar nuevos objetos o areas en su entorno. Este calamar prospera con la novedad y podria aburrirse mas facilmente en entornos sin cambios.\",\r\n    \"desc_lazy\": \"Tu calamar es Perezoso. Prefiere un estilo de vida relajado y puede ser menos activo que otros calamares. Podria necesitar mas estimulo para participar en actividades, pero puede estar bastante contento simplemente descansando. Este calamar es excelente conservando energia!\",\r\n    \"desc_energetic\": \"Tu calamar es Energico. Siempre esta en movimiento, lleno de vida y vigor. Este calamar necesita mucha estimulacion y actividades para mantenerse feliz. Podria ponerse inquieto si no tiene suficientes oportunidades para quemar su exceso de energia.\",\r\n    \"desc_introvert\": \"Tu calamar es Introvertido. Disfruta de la soledad y podria preferir espacios mas tranquilos y menos concurridos. Aunque puede interactuar con otros, puede necesitar tiempo a solas para recargarse. Este calamar podria ser mas observador y reflexivo en sus acciones.\",\r\n    \"desc_greedy\": \"Tu calamar es Gloton. Tiene un fuerte enfoque en la comida y los recursos. Este calamar podria estar mas motivado por golosinas y recompensas que otros. Aunque puede ser mas exigente, tambien tiende a ser ingenioso y bueno encontrando golosinas escondidas.\",\r\n    \"desc_stubborn\": \"Tu calamar es Terco. Tiene una voluntad fuerte y preferencias definidas. Este calamar podria ser mas resistente al cambio y podria tardar mas en adaptarse a nuevas rutinas. Sin embargo, su determinacion tambien puede hacerlo persistente al resolver problemas.\",\r\n\r\n    # Modificadores Cortos de Personalidad\r\n    \"mod_timid\": \"Mayor probabilidad de volverse ansioso\",\r\n    \"mod_adventurous\": \"Mayor curiosidad y exploracion\",\r\n    \"mod_lazy\": \"Movimiento mas lento y menor consumo de energia\",\r\n    \"mod_energetic\": \"Movimiento mas rapido y niveles de actividad mas altos\",\r\n    \"mod_introvert\": \"Prefiere la soledad y ambientes tranquilos\",\r\n    \"mod_greedy\": \"Mas enfocado en comida y recursos\",\r\n    \"mod_stubborn\": \"Solo come su comida favorita (sushi), puede negarse a dormir\",\r\n\r\n    # Detalles de Modificadores\r\n    \"modifiers_timid\": \"- La ansiedad aumenta 50% mas rapido\\n- La curiosidad aumenta 50% mas lento\\n- La ansiedad disminuye 50% cuando esta cerca de plantas\",\r\n    \"modifiers_adventurous\": \"- La curiosidad aumenta 50% mas rapido\",\r\n    \"modifiers_lazy\": \"- Se mueve mas lento\\n- El consumo de energia es menor\",\r\n    \"modifiers_energetic\": \"- Se mueve mas rapido\\n- El consumo de energia es mayor\",\r\n    \"modifiers_introvert\": \"- Prefiere espacios mas tranquilos y menos concurridos\\n- Puede necesitar mas tiempo a solas para recargarse\",\r\n    \"modifiers_greedy\": \"- Se pone 50% mas ansioso cuando tiene hambre\\n- La satisfaccion aumenta mas al comer\",\r\n    \"modifiers_stubborn\": \"- Prefiere su comida favorita (sushi)\\n- Puede negarse a dormir incluso cuando esta cansado\",\r\n\r\n    # Consejos de Cuidado\r\n    \"tips_timid\": \"- Coloca plantas en el entorno para reducir la ansiedad\\n- Manten el ambiente limpio y tranquilo\\n- Acercate lentamente y evita movimientos bruscos\\n- Manten una rutina consistente\\n- Evita cambiar el tamano de la ventana frecuentemente\",\r\n    \"tips_adventurous\": \"- Introduce nuevos objetos o decoraciones regularmente\\n- Ofrece opciones de comida diversas\\n- Fomenta la exploracion con ubicacion estrategica de comida\\n- Permite mucho espacio para explorar\\n- Alimenta su curiosidad natural con objetos interesantes\",\r\n    \"tips_lazy\": \"- Coloca la comida mas cerca de los lugares de descanso\\n- Limpia el entorno con mas frecuencia\\n- Usa comida tentadora para fomentar el movimiento\\n- No esperes mucha actividad - prefieren relajarse\\n- Asegurate de que sus lugares de descanso esten comodos\",\r\n    \"tips_energetic\": \"- Proporciona un espacio grande y abierto\\n- Ofrece oportunidades frecuentes de alimentacion\\n- Introduce elementos o juegos interactivos\\n- Manten el entorno estimulante con decoraciones variadas\\n- Necesitan mas comida debido al mayor consumo de energia\",\r\n    \"tips_introvert\": \"- Crea areas tranquilas y apartadas con decoraciones\\n- Evita saturar el entorno\\n- Respeta la necesidad del calamar de estar solo\\n- Crea espacios protegidos usando plantas\\n- Acercate con suavidad y dale espacio cuando lo necesite\",\r\n    \"tips_greedy\": \"- Ofrece una variedad de tipos de comida, incluyendo sushi\\n- Usa la comida como recompensa por comportamientos deseados\\n- Ten cuidado de no sobrealimentar\\n- Se pondra mas ansioso cuando tenga hambre\\n- Proporciona oportunidades para recolectar objetos\",\r\n    \"tips_stubborn\": \"- Siempre ten sushi disponible ya que es su comida favorita\\n- Se paciente al introducir cambios\\n- Usa refuerzo positivo para comportamientos deseados\\n- Puede rechazar comida que no sea sushi\\n- Puede resistirse a dormir - crea ambientes tranquilos\",\r\n\r\n    # ===== PESTANA DECISIONES =====\r\n    \"thought_process\": \"Proceso de Pensamiento del Calamar\",\r\n    \"step\": \"Paso\",\r\n    \"step1_title\": \"Percibiendo el Mundo\",\r\n    \"step2_title\": \"Calculando Impulsos Basicos\",\r\n    \"step3_title\": \"Aplicando Personalidad y Memorias\",\r\n    \"step4_title\": \"Tomando la Decision Final\",\r\n    \"final_action\": \"Accion Final:\",\r\n    \"awaiting_thought\": \"Esperando el proximo pensamiento del calamar...\",\r\n    \"awaiting_decision\": \"Esperando Decision...\",\r\n    \"sensing_condition\": \"El calamar evalua su condicion actual y los objetos visibles:\",\r\n    \"visible_objects\": \"Objetos Visibles\",\r\n    \"no_sensory_data\": \"No hay datos sensoriales disponibles.\",\r\n    \"none\": \"Ninguno\",\r\n    \"no_urges\": \"No se calcularon impulsos.\",\r\n    \"strongest_urge\": \"Basado en las necesidades, el impulso mas fuerte es\",\r\n    \"initial_scores\": \"Puntuaciones iniciales:\",\r\n    \"personality_memory_adjust\": \"Los rasgos de personalidad y memorias recientes ajustan estos impulsos:\",\r\n    \"no_adjustments\": \"Sin ajustes significativos de personalidad o memoria esta vez.\",\r\n    \"final_scores_text\": \"Despues de todos los calculos, se suman las puntuaciones finales. La puntuacion mas alta determina la accion.\",\r\n    \"no_final_scores\": \"No hay puntuaciones finales disponibles.\",\r\n    \"squid_decided\": \"El calamar ha decidido\",\r\n    \"with_confidence\": \"con un nivel de confianza de\",\r\n    \"score_increased\": \"aumento\",\r\n    \"score_decreased\": \"disminuyo\",\r\n    \"score_for\": \"La puntuacion para\",\r\n    \"by_amount\": \"en\",\r\n\r\n    # ===== PESTANA APRENDIZAJE (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"Pares de Aprendizaje Activos\",\r\n    \"hebbian_cycle\": \"Ciclo Hebbiano\",\r\n    \"hebbian_paused\": \"EN PAUSA\",\r\n    \"learning_ready\": \"Sistema de Aprendizaje Listo\",\r\n    \"learning_ready_desc\": \"El aprendizaje hebbiano creara asociaciones entre neuronas que se activan juntas.\",\r\n    \"log_cleared\": \"Registro Borrado\",\r\n    \"log_cleared_desc\": \"Los pares de aprendizaje apareceran aqui mientras las neuronas forman nuevas conexiones.\",\r\n    \"hebbian_overview\": \"Resumen del Aprendizaje Hebbiano\",\r\n    \"neurons_fire_together\": \"Las neuronas que se activan juntas, se conectan juntas\",\r\n    \"hebbian_principle\": \"Este principio fundamental describe como las redes neuronales aprenden a traves de la experiencia.\",\r\n    \"hebbian_explanation\": \"El aprendizaje hebbiano es una regla simple pero poderosa utilizada en redes neuronales artificiales. Cuando dos neuronas se activan simultaneamente, la conexion entre ellas se fortalece. Si se activan por separado, la conexion se debilita.\",\r\n    \"excitatory_connections\": \"Conexiones Excitatorias\",\r\n    \"excitatory_desc\": \"Los pesos positivos (0.0-1.0) hacen que las neuronas sean mas propensas a activarse juntas\",\r\n    \"inhibitory_connections\": \"Conexiones Inhibitorias\",\r\n    \"inhibitory_desc\": \"Los pesos negativos (-1.0-0.0) hacen que las neuronas sean menos propensas a activarse juntas\",\r\n    \"very_strong\": \"Muy Fuerte\",\r\n    \"strong\": \"Fuerte\",\r\n    \"moderate\": \"Moderado\",\r\n    \"weak\": \"Debil\",\r\n    \"very_weak\": \"Muy Debil\",\r\n    \"inhibited\": \"Inhibido\",\r\n\r\n    # ===== PESTANA MEMORIA =====\r\n    \"memory\": \"Memoria\",\r\n    \"memories\": \"Memorias\",\r\n    \"short_term_memory\": \"Memoria a Corto Plazo\",\r\n    \"long_term_memory\": \"Memoria a Largo Plazo\",\r\n    \"no_memories\": \"No hay memorias almacenadas todavia.\",\r\n    \"overview\": \"Resumen\",\r\n    \"memory_stats\": \"Estadísticas de Memoria\",\r\n    \"categories\": \"Categorías\",\r\n    \"time_label\": \"Hora:\",\r\n    \"important_label\": \"Importante\",\r\n    \"unknown\": \"Desconocido\",\r\n    \"category_label\": \"Categoría:\",\r\n    \"key_label\": \"Clave:\",\r\n    \"access_count\": \"Conteo de acceso:\",\r\n    \"full_content\": \"Contenido Completo:\",\r\n    \"effects_label\": \"Efectos:\",\r\n    \"positive\": \"Positivo\",\r\n    \"negative\": \"Negativo\",\r\n    \"neutral\": \"Neutral\",\r\n\r\n    # ===== PESTANA RED (ORIGINAL) =====\r\n    \"brain_network\": \"Red Cerebral\",\r\n    \"neurons\": \"Neuronas\",\r\n    \"connections\": \"Conexiones\",\r\n    \"activity\": \"Actividad\",\r\n\r\n    # ===== VENTANA ESTADISTICAS =====\r\n    \"status\": \"Estado\",\r\n    \"health\": \"Salud\",\r\n\r\n    # ===== NEURON NAMES (NEW - SPANISH) =====\r\n    \"hunger\": \"Hambre\",\r\n    \"happiness\": \"Felicidad\",\r\n    \"cleanliness\": \"Limpieza\",\r\n    \"sleepiness\": \"Somnolencia\",\r\n    \"satisfaction\": \"Satisfaccion\",\r\n    \"curiosity\": \"Curiosidad\",\r\n    \"anxiety\": \"Ansiedad\",\r\n    \"can_see_food\": \"Ve Comida\",\r\n    \"is_eating\": \"Comiendo\",\r\n    \"is_sleeping\": \"Durmiendo\",\r\n    \"is_sick\": \"Enfermo\",\r\n    \"is_fleeing\": \"Huyendo\",\r\n    \"is_startled\": \"Asustado\",\r\n    \"pursuing_food\": \"Buscando Comida\",\r\n    \"external_stimulus\": \"Estimulo\",\r\n    \"plant_proximity\": \"Cerca Planta\",\r\n    \"stress\": \"Estres\",\r\n    \"novelty\": \"Novedad\",\r\n    \"reward\": \"Recompensa\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS (NEW - SPANISH) =====\r\n    \"layer_name\": \"Capa\",\r\n    \"layer_input\": \"Entrada\",\r\n    \"layer_output\": \"Salida\",\r\n    \"layer_hidden\": \"Oculta\",\r\n\r\n    # ===== NEUROGENESIS LOGS (NEW - SPANISH) =====\r\n    \"log_created\": \"{time} - una neurona {type} ({name}) fue creada porque el contador de {type} era {value:.2f}\",\r\n    \"log_pruned\": \"{time} - una neurona ({name}) fue PODADA debido a {reason}\",\r\n    \"log_stress_detail\": \"Se hizo una conexión inhibitoria a ANSIEDAD\\nEl valor máximo de ansiedad se ha reducido permanentemente en 10\",\r\n\r\n    # Pildoras de Estado\r\n    \"fleeing\": \"Huyendo!\",\r\n    \"startled\": \"Asustado!\",\r\n    \"eating\": \"Comiendo\",\r\n    \"sleeping\": \"Durmiendo\",\r\n    \"playing\": \"Jugando\",\r\n    \"hiding\": \"Escondido\",\r\n    \"anxious\": \"Ansioso\",\r\n    \"curious\": \"Curioso\",\r\n\r\n    # ===== ACCIONES COMUNES =====\r\n    \"eat\": \"Comer\",\r\n    \"sleep\": \"Dormir\",\r\n    \"play\": \"Jugar\",\r\n    \"explore\": \"Explorar\",\r\n    \"rest\": \"Descansar\",\r\n    \"hide\": \"Esconderse\",\r\n    \"wander\": \"Deambular\",\r\n    \"idle\": \"Inactivo\",\r\n    \"seek_food\": \"Buscar Comida\",\r\n    \"seek_shelter\": \"Buscar Refugio\",\r\n\r\n    # ===== OBJETOS =====\r\n    \"food\": \"Comida\",\r\n    \"rock\": \"Roca\",\r\n    \"poop\": \"Caca\",\r\n    \"plant\": \"Planta\",\r\n    \"sushi\": \"Sushi\",\r\n    \"decoration\": \"Decoración\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"Un calamar ha nacido y debes cuidarlo!\",\r\n    \"tutorial_feed\": \"Alimentalo cuando tenga hambre (Menu Acciones)\",\r\n    \"tutorial_clean\": \"Limpia su tanque cuando se ensucie\",\r\n    \"tutorial_watch\": \"Observa su comportamiento para aprender sobre su personalidad\",\r\n    \"tutorial_neural\": \"RED NEURONAL\",\r\n    \"tutorial_neural_desc\": \"Esta es la red neuronal del calamar. Su comportamiento esta impulsado por sus necesidades (neuronas redondas).\\nLa red se adapta y aprende mientras el calamar interactua con su entorno.\",\r\n    \"tutorial_satisfaction\": \"Manten la satisfaccion alta y la ansiedad baja.\",\r\n    \"tutorial_traits\": \"Tu calamar desarrollara rasgos y comportamientos unicos segun como lo cries.\",\r\n\r\n    # ===== DISENADOR DE CEREBRO =====\r\n    \"designer_title\": \"Disenador de Cerebro\",\r\n    \"required_only\": \"Solo Requeridos\",\r\n    \"dosidicus_default\": \"Dosidicus Predeterminado\",\r\n    \"full_sensors\": \"Suite Completa de Sensores\",\r\n    \"the_insomniac\": \"El Insomne\",\r\n    \"the_hyperactive\": \"El Hiperactivo\",\r\n    \"the_hangry\": \"El Hambriento Furioso\",\r\n    \"the_depressive\": \"El Depresivo\",\r\n    \"the_obsessive\": \"El Obsesivo\",\r\n    \"balanced\": \"Equilibrado\",\r\n    \"minimal\": \"Minimo\",\r\n    \"dense\": \"Denso\",\r\n    \"chaotic\": \"Caotico\",\r\n    \"calm\": \"Calmado\",\r\n\r\n    # ===== PANTALLA DE INICIO =====\r\n    \"squid_hatched\": \"UN CALAMAR HA NACIDO!\",\r\n    \"look_after\": \"NECESITAS CUIDARLO..\",\r\n\r\n    # ===== PASOS DEL TUTORIAL =====\r\n    \"next\": \"Siguiente\",\r\n    \"tutorial_step1_text\": \"Un calamar ha nacido y debes cuidarlo!\\n• Alimentalo cuando tenga hambre (Menu Acciones)\\n• Limpia su tanque cuando se ensucie\\n• Observa su comportamiento para aprender sobre su personalidad\",\r\n    \"tutorial_step2_text\": \"Esta es la red neuronal del calamar. Su comportamiento esta impulsado por sus necesidades (neuronas).\\nLa red se adapta y aprende mientras el calamar interactua con su entorno.\",\r\n    \"tutorial_step3_text\": \"El calamar puede generar nuevas neuronas en respuesta a estimulos ambientales extremos.\\nEstas nuevas neuronas ayudan al calamar a adaptarse a situaciones dificiles.\",\r\n    \"tutorial_step4_text\": \"Cuando un par de neuronas se activan al mismo tiempo, su conexion se fortalece. Esto permite al calamar aprender asociaciones entre diferentes estimulos y respuestas.\",\r\n    \"tutorial_step5_text\": \"La red neuronal toma decisiones basadas en necesidades actuales y memorias pasadas.\\nCada decision afecta el estado del calamar y moldea su comportamiento futuro.\",\r\n    \"tutorial_step6_text\": \"Presiona D en cualquier momento para abrir la ventana de Decoraciones\\nArrastra y suelta decoraciones en el entorno y observa como reacciona el calamar a diferentes cosas. Cada tipo de decoracion afecta el estado mental del calamar de maneras unicas. Haz clic y usa la rueda del raton para redimensionar/SUPR para eliminar\",\r\n    \"tutorial_step7_text\": \"Manten la satisfaccion alta y la ansiedad baja.\\nTu calamar desarrollara rasgos y comportamientos unicos segun como lo cries.\",\r\n\r\n    # ===== NETWORK & LEARNING TABS (NEW - SPANISH) =====\r\n    \"stats_neurons\": \"Neuronas\",\r\n    \"stats_connections\": \"Conexiones\",\r\n    \"stats_health\": \"Salud de Red\",\r\n    \"emergency_alert\": \"🚨 Emergencia: {name}\",\r\n    \"global_cooldown\": \"Enfriamiento\",\r\n    \"style_label\": \"Estilo:\",\r\n    \"chk_links\": \"Ver enlaces\",\r\n    \"chk_weights\": \"Ver pesos\",\r\n    \"chk_pruning\": \"Poda activada\",\r\n    \"tooltip_brain_designer\": \"Abrir Diseñador de Cerebro\",\r\n    \"msg_already_open\": \"Ya abierto\",\r\n    \"msg_designer_running\": \"¡El Diseñador de Cerebro ya se está ejecutando!\",\r\n    \"msg_launch_failed\": \"Error de inicio\",\r\n    \"msg_designer_fail\": \"No se pudo iniciar el Diseñador de Cerebro:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"Falta Cerebro\",\r\n    \"msg_cannot_open_lab\": \"No se puede abrir el Laboratorio: Widget Cerebral no disponible.\",\r\n    \"msg_cannot_open_buffer\": \"No se puede abrir Buffer: Widget Cerebral no disponible.\",\r\n    \"msg_no_neurogenesis\": \"Sin Neurogénesis\",\r\n    \"msg_neurogenesis_not_init\": \"No se puede abrir Buffer: Sistema de neurogénesis no iniciado.\",\r\n    \"msg_decorations_unavailable\": \"Decoraciones no disponibles\",\r\n    \"msg_decorations_fail\": \"No se puede abrir Decoraciones: Ventana no disponible.\",\r\n    \"func_neurons_title\": \"Neuronas Funcionales\",\r\n    \"count_label\": \"Cuenta\",\r\n    \"avg_utility_label\": \"Utilidad Media\",\r\n    \"total_activations_label\": \"Activaciones Totales\",\r\n    \"specialisations_label\": \"Especializaciones\",\r\n    \"buffer_title\": \"Buffer de Experiencia de Neurogénesis\",\r\n    \"buffer_header\": \"Experiencias Recientes\",\r\n    \"col_type\": \"Tipo\",\r\n    \"col_pattern\": \"Patrón\",\r\n    \"col_outcome\": \"Resultado\",\r\n    \"col_time\": \"Tiempo\",\r\n    \"btn_refresh\": \"Actualizar\",\r\n    \"buffer_size\": \"Tamaño del buffer\",\r\n    \"top_patterns\": \"Patrones principales\",\r\n    \"no_patterns\": \"Sin patrones aún\",\r\n\r\n    # Learning Tab Educational Content (Spanish)\r\n    \"learning_pairs_tab\": \"Pares de Aprendizaje\",\r\n    \"mechanics_tab\": \"Mecánica\",\r\n    \"hebbian_quote\": \"\\\"Las neuronas que se activan juntas, se conectan juntas\\\"\",\r\n    \"in_practice_title\": \"En la Práctica\",\r\n    \"in_practice_text\": \"En el cerebro de tu calamar, el aprendizaje hebbiano ayuda a asociar estados como 'hambre' con 'satisfacción' al comer, o 'curiosidad' con 'ansiedad' al explorar.\",\r\n    \"mechanics_title\": \"Mecánica de Aprendizaje\",\r\n    \"mechanics_intro\": \"El aprendizaje hebbiano actualiza la fuerza de conexión (peso) entre neuronas basándose en su actividad. Si se activan juntas, la conexión se fortalece; si no, se debilita.\",\r\n    \"learning_rule_title\": \"La Regla de Aprendizaje\",\r\n    \"where_label\": \"Donde:\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = Cambio en el peso entre dos neuronas\",\r\n    \"eta_desc\": \"<b>η</b> (eta) = Tasa de aprendizaje (velocidad de cambio)\",\r\n    \"activation_desc\": \"<b>x, y</b> = Valores de activación (1 activo, 0 inactivo)\",\r\n    \"example_calc_title\": \"Ejemplo de Cálculo\",\r\n    \"scenario_label\": \"<b>Escenario:</b> 'hambre' y 'satisfacción' se activan\",\r\n    \"calc_result\": \"El peso aumenta en 0.1, fortaleciendo la conexión.\",\r\n    \"over_time_title\": \"Con el Tiempo\",\r\n    \"over_time_text\": \"A través de activaciones repetidas, estos pequeños cambios se acumulan. Los patrones frecuentes desarrollan conexiones fuertes, permitiendo al calamar aprender de la experiencia.\",\r\n    \"str_excitatory\": \"Excitatorio Fuerte\",\r\n    \"weak_excitatory\": \"Excitatorio Débil\",\r\n    \"weak_inhibitory\": \"Inhibitorio Débil\",\r\n    \"str_inhibitory\": \"Inhibitorio Fuerte\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS (SPANISH) =====\r\n    \"distance_rollover\": \"🌊 ¡El contador de distancia se reinició! Ahora en {multiplier}x\",\r\n    \"time_min\": \"min\",\r\n    \"time_mins\": \"mins\",\r\n    \"time_hr\": \"h\",\r\n    \"time_hrs\": \"hs\",\r\n    \"time_fmt_hm\": \"{hours}h {minutes}m\",\r\n    \"stat_squid_age\": \"Edad del Calamar\",\r\n    \"stat_distance\": \"Distancia Nadada (píxeles)\",\r\n    \"stat_cheese\": \"Queso Comido\",\r\n    \"stat_sushi\": \"Sushi Comido\",\r\n    \"stat_poops\": \"Cacas Creadas\",\r\n    \"stat_max_poops\": \"Máx Cacas en Tanque\",\r\n    \"stat_startles\": \"Veces Asustado\",\r\n    \"stat_ink\": \"Nubes de Tinta Creadas\",\r\n    \"stat_colour_change\": \"Veces Cambio de Color\",\r\n    \"stat_rocks\": \"Rocas Lanzadas\",\r\n    \"stat_plants\": \"Interacciones con Plantas\",\r\n    \"stat_sleep\": \"Tiempo Total de Sueño (segundos)\",\r\n    \"stat_sickness\": \"Episodios de Enfermedad\",\r\n    \"stat_novelty_neurons\": \"Neuronas Novedad Creadas\",\r\n    \"stat_stress_neurons\": \"Neuronas Estrés Creadas\",\r\n    \"stat_reward_neurons\": \"Neuronas Recompensa Creadas\",\r\n    \"stat_current_neurons\": \"Neuronas Actuales\",\r\n\r\n    \"reset_stats_title\": \"Reiniciar Estadísticas\",\r\n    \"reset_stats_msg\": \"¿Estás seguro de que quieres reiniciar todas las estadísticas?\",\r\n    \"export_stats_title\": \"Exportar Estadísticas\",\r\n    \"export_file_type\": \"Archivos de Texto (*.txt)\",\r\n    \"export_header\": \"Exportación de Estadísticas del Calamar\",\r\n    \"export_time\": \"Hora de Exportación\",\r\n    \"export_activity_section\": \"Estadísticas de Actividad\",\r\n    \"export_end\": \"Fin de Estadísticas\",\r\n    \"export_success_title\": \"Exportación Exitosa\",\r\n    \"export_success_msg\": \"Estadísticas exportadas a {file_name}\",\r\n    \"export_error_title\": \"Error de Exportación\",\r\n    \"export_error_msg\": \"Error al exportar estadísticas: {error}\",\r\n\r\n    # ===== ACHIEVEMENTS (NEW - SPANISH) =====\r\n    # Categories\r\n    \"cat_feeding\": \"Alimentación\",\r\n    \"cat_neurogenesis\": \"Neurogénesis\",\r\n    \"cat_sleep\": \"Sueño\",\r\n    \"cat_milestones\": \"Hitos\",\r\n    \"cat_exploration\": \"Exploración\",\r\n    \"cat_cleaning\": \"Limpieza\",\r\n    \"cat_health\": \"Salud\",\r\n    \"cat_interaction\": \"Interacción\",\r\n    \"cat_ink\": \"Tinta\",\r\n    \"cat_memory\": \"Memoria\",\r\n    \"cat_emotional\": \"Emocional\",\r\n    \"cat_secret\": \"Secreto\",\r\n    \"cat_meta\": \"Meta\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"Puntos\",\r\n    \"ui_unlocked\": \"Desbloqueado\",\r\n    \"ui_achievement_unlocked\": \"¡Logro Desbloqueado!\",\r\n    \"ui_hidden\": \"Logro oculto\",\r\n    \"ui_all\": \"Todos\",\r\n    \"ui_points_gained\": \"puntos\",\r\n\r\n    # --- Achievements ---\r\n\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"Primer Bocado\",\r\n    \"ach_first_feeding_desc\": \"Alimenta al calamar por primera vez\",\r\n    \"ach_fed_10_times_name\": \"Comidas Regulares\",\r\n    \"ach_fed_10_times_desc\": \"Alimenta al calamar 10 veces\",\r\n    \"ach_fed_50_times_name\": \"Cuidador Dedicado\",\r\n    \"ach_fed_50_times_desc\": \"Alimenta al calamar 50 veces\",\r\n    \"ach_fed_100_times_name\": \"Chef Maestro\",\r\n    \"ach_fed_100_times_desc\": \"Alimenta al calamar 100 veces\",\r\n    \"ach_fed_500_times_name\": \"Leyenda Culinaria\",\r\n    \"ach_fed_500_times_desc\": \"Alimenta al calamar 500 veces\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"Chispa Cerebral\",\r\n    \"ach_first_neuron_desc\": \"Crea la primera neurona de neurogénesis\",\r\n    \"ach_neurons_10_name\": \"Red Neuronal\",\r\n    \"ach_neurons_10_desc\": \"Crea 10 neuronas mediante neurogénesis\",\r\n    \"ach_neurons_50_name\": \"Mente en Expansión\",\r\n    \"ach_neurons_50_desc\": \"Crea 50 neuronas mediante neurogénesis\",\r\n    \"ach_neurons_100_name\": \"Potencia Cerebral\",\r\n    \"ach_neurons_100_desc\": \"Crea 100 neuronas mediante neurogénesis\",\r\n    \"ach_first_neuron_levelup_name\": \"Sinapsis Fortalecida\",\r\n    \"ach_first_neuron_levelup_desc\": \"Sube de nivel una neurona por primera vez\",\r\n    \"ach_neuron_max_level_name\": \"Rendimiento Máximo\",\r\n    \"ach_neuron_max_level_desc\": \"Sube una neurona a su fuerza máxima\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"Dulces Sueños\",\r\n    \"ach_first_sleep_desc\": \"El calamar despierta de su primer sueño\",\r\n    \"ach_slept_10_times_name\": \"Bien Descansado\",\r\n    \"ach_slept_10_times_desc\": \"El calamar ha dormido 10 veces\",\r\n    \"ach_dream_state_name\": \"Soñador Profundo\",\r\n    \"ach_dream_state_desc\": \"El calamar entró en sueño REM\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"Una Hora de Vida\",\r\n    \"ach_age_1_hour_desc\": \"El calamar alcanzó 1 hora de edad\",\r\n    \"ach_age_10_hours_name\": \"Creciendo\",\r\n    \"ach_age_10_hours_desc\": \"El calamar alcanzó 10 horas de edad\",\r\n    \"ach_age_24_hours_name\": \"Maravilla de un Día\",\r\n    \"ach_age_24_hours_desc\": \"El calamar sobrevivió 24 horas\",\r\n    \"ach_age_1_week_name\": \"Veterano Semanal\",\r\n    \"ach_age_1_week_desc\": \"El calamar ha vivido una semana\",\r\n    \"ach_age_1_month_name\": \"Veterano Mensual\",\r\n    \"ach_age_1_month_desc\": \"El calamar ha vivido un mes\",\r\n    \"ach_happiness_100_name\": \"Felicidad Pura\",\r\n    \"ach_happiness_100_desc\": \"Alcanza el 100% de felicidad\",\r\n    \"ach_all_stats_high_name\": \"Equilibrio Perfecto\",\r\n    \"ach_all_stats_high_desc\": \"Todas las estadísticas por encima del 80% simultáneamente\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"Primer Fregado\",\r\n    \"ach_first_clean_desc\": \"Limpia el tanque por primera vez\",\r\n    \"ach_cleaned_25_times_name\": \"Entorno Impecable\",\r\n    \"ach_cleaned_25_times_desc\": \"Limpia el tanque 25 veces\",\r\n    \"ach_germaphobe_name\": \"Germófobo\",\r\n    \"ach_germaphobe_desc\": \"Mantén la limpieza por encima del 90% durante 1 hora seguida\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"Primeros Auxilios\",\r\n    \"ach_first_medicine_desc\": \"Administra medicina por primera vez\",\r\n    \"ach_medicine_10_times_name\": \"Doctor Calamar\",\r\n    \"ach_medicine_10_times_desc\": \"Administra medicina 10 veces\",\r\n    \"ach_comeback_kid_name\": \"El Regreso\",\r\n    \"ach_comeback_kid_desc\": \"Recuperarse de salud crítica (<20%) al máximo\",\r\n\r\n    # Interaction (Rocks)\r\n    \"ach_first_rock_pickup_name\": \"Coleccionista de Rocas\",\r\n    \"ach_first_rock_pickup_desc\": \"Recoge una roca por primera vez\",\r\n    \"ach_rocks_picked_10_name\": \"Recolector de Piedras\",\r\n    \"ach_rocks_picked_10_desc\": \"Recoge 10 rocas\",\r\n    \"ach_rocks_picked_50_name\": \"Acaparador de Rocas\",\r\n    \"ach_rocks_picked_50_desc\": \"Recoge 50 rocas\",\r\n    \"ach_first_rock_throw_name\": \"Haciendo Sapito\",\r\n    \"ach_first_rock_throw_desc\": \"Lanza una roca por primera vez\",\r\n    \"ach_rocks_thrown_25_name\": \"Lanzador de Rocas\",\r\n    \"ach_rocks_thrown_25_desc\": \"Lanza 25 rocas\",\r\n    \"ach_rocks_thrown_100_name\": \"Maestro de Catapulta\",\r\n    \"ach_rocks_thrown_100_desc\": \"Lanza 100 rocas\",\r\n\r\n    # Interaction (Decor)\r\n    \"ach_first_decoration_push_name\": \"Decorador de Interiores\",\r\n    \"ach_first_decoration_push_desc\": \"Empuja una decoración por primera vez\",\r\n    \"ach_decorations_pushed_10_name\": \"Mudanzas\",\r\n    \"ach_decorations_pushed_10_desc\": \"Empuja decoraciones 10 veces\",\r\n    \"ach_decorations_pushed_50_name\": \"Maestro del Feng Shui\",\r\n    \"ach_decorations_pushed_50_desc\": \"Empuja decoraciones 50 veces\",\r\n    \"ach_first_plant_interact_name\": \"Mano Verde\",\r\n    \"ach_first_plant_interact_desc\": \"Interactúa con una planta por primera vez\",\r\n    \"ach_plants_interacted_10_name\": \"Explorador de Jardín\",\r\n    \"ach_plants_interacted_10_desc\": \"Interactúa con plantas 10 veces\",\r\n    \"ach_plants_interacted_50_name\": \"Botánico\",\r\n    \"ach_plants_interacted_50_desc\": \"Interactúa con plantas 50 veces\",\r\n    \"ach_objects_investigated_25_name\": \"Inspector Curioso\",\r\n    \"ach_objects_investigated_25_desc\": \"Investiga 25 objetos diferentes\",\r\n    \"ach_objects_investigated_100_name\": \"Detective Maestro\",\r\n    \"ach_objects_investigated_100_desc\": \"Investiga 100 objetos diferentes\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"Travieso\",\r\n    \"ach_first_poop_throw_desc\": \"El calamar lanzó caca por primera vez\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"Cortina de Humo\",\r\n    \"ach_first_ink_cloud_desc\": \"El calamar suelta una nube de tinta por primera vez\",\r\n    \"ach_ink_clouds_20_name\": \"Maestro de la Tinta\",\r\n    \"ach_ink_clouds_20_desc\": \"Suelta 20 nubes de tinta\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"Primer Recuerdo\",\r\n    \"ach_first_memory_desc\": \"Forma el primer recuerdo\",\r\n    \"ach_memory_long_term_name\": \"Pensamiento a Largo Plazo\",\r\n    \"ach_memory_long_term_desc\": \"Promueve un recuerdo al almacenamiento a largo plazo\",\r\n    \"ach_memories_50_name\": \"Memoria Fotográfica\",\r\n    \"ach_memories_50_desc\": \"Ten 50 recuerdos almacenados\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"Jorge el Curioso\",\r\n    \"ach_curiosity_100_desc\": \"La curiosidad alcanza el 100%\",\r\n    \"ach_zen_master_name\": \"Maestro Zen\",\r\n    \"ach_zen_master_desc\": \"Mantén la ansiedad por debajo del 10% durante 30 minutos\",\r\n    \"ach_first_startle_name\": \"¡Sobresalto!\",\r\n    \"ach_first_startle_desc\": \"Asusta al calamar por primera vez\",\r\n    \"ach_nervous_wreck_name\": \"Manojo de Nervios\",\r\n    \"ach_nervous_wreck_desc\": \"La ansiedad alcanza el 100%\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"Búho Nocturno\",\r\n    \"ach_night_owl_desc\": \"Juega entre la medianoche y las 4 AM\",\r\n    \"ach_early_bird_name\": \"Madrugador\",\r\n    \"ach_early_bird_desc\": \"Juega entre las 5 AM y las 7 AM\",\r\n    \"ach_weekend_warrior_name\": \"Guerrero de Fin de Semana\",\r\n    \"ach_weekend_warrior_desc\": \"Juega tanto el sábado como el domingo\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"Cirujano Cerebral\",\r\n    \"ach_brain_surgeon_desc\": \"Abre la herramienta de visualización cerebral\",\r\n    \"ach_speed_demon_name\": \"Demonio de la Velocidad\",\r\n    \"ach_speed_demon_desc\": \"Ejecuta la simulación a velocidad máxima durante 10 minutos\",\r\n    \"ach_completionist_name\": \"Completista\",\r\n    \"ach_completionist_desc\": \"Desbloquea otros 30 logros\",\r\n\r\n    # Additional Log/Debug Messages (Previously Orphaned)\r\n    \"Hebbian learning chosen pairs:\": \"Pares elegidos por aprendizaje Hebbiano:\",\r\n    \"Main thread: Created neuron\": \"Hilo principal: Neurona creada\",\r\n    \"BrainWidget received external BrainWorker\": \"BrainWidget recibió BrainWorker externo\",\r\n    \"Neurogenesis monitoring timer started\": \"Temporizador de monitoreo de neurogénesis iniciado\",\r\n    \"Brain state export enabled for designer sync\": \"Exportación de estado cerebral habilitada para sincronización con diseñador\",\r\n    \"Animation palette built for style:\": \"Paleta de animación construida para estilo:\",\r\n    \"Animation style changed:\": \"Estilo de animación cambiado:\",\r\n    \"Unknown animation style:\": \"Estilo de animación desconocido:\",\r\n    \"Available:\": \"Disponibles:\",\r\n\r\n    # ===== LABORATORIO DE NEURONAS =====\r\n    \"lab_title\": \"🧠 Laboratorio de Neuronas\",\r\n    \"lab_live_refresh\": \"Actualización en vivo\",\r\n    \"lab_unlock_editing\": \"🔓 Desbloquear edición\",\r\n    \"lab_tab_overview\": \"📊 Resumen en Vivo\",\r\n    \"lab_tab_inspector\": \"🔍 Inspector Profundo\",\r\n    \"lab_tab_edit\": \"🔧 Área de Edición\",\r\n    \"lab_status_ready\": \"Listo\",\r\n    \"lab_status_locked\": \"🔒 {name} bloqueado en {value}\",\r\n    \"lab_status_unlocked\": \"🔓 {name} desbloqueado\",\r\n    \r\n    # Pestaña Resumen\r\n    \"lab_ov_counters\": \"Progreso de contadores\",\r\n    \"lab_ov_newest\": \"Neuronas de neurogénesis más recientes\",\r\n    \"lab_ov_limits\": \"Límites y poda\",\r\n    \"lab_ov_actions\": \"Acciones rápidas\",\r\n    \"lab_force_hebbian\": \"Forzar ciclo Hebbiano\",\r\n    \"lab_pruning_enabled\": \"Poda habilitada:\",\r\n    \"lab_none_yet\": \"Ninguna todavía\",\r\n    \"lab_ago\": \"hace {seconds}s\",\r\n    \r\n    # Pestaña Inspector\r\n    \"lab_pick_neuron\": \"Elige una neurona para inspeccionar:\",\r\n    \"lab_connections_title\": \"Conexiones (excitatorias vs inhibitorias)\",\r\n    \"lab_header_partner\": \"Socio\",\r\n    \"lab_header_weight\": \"Peso\",\r\n    \"lab_header_type\": \"Tipo\",\r\n    \"lab_header_inf\": \"Influencia\",\r\n    \"lab_impact_title\": \"Simulación de impacto funcional\",\r\n    \"lab_header_neuron\": \"Neurona\",\r\n    \"lab_header_delta\": \"Valor Δ\",\r\n    \"lab_no_connections\": \"No hay conexiones activas por el momento\",\r\n    \"lab_did_you_know\": \"¿Sabías que?\",\r\n    \"lab_type_excitatory\": \"Excitatoria\",\r\n    \"lab_type_inhibitory\": \"Inhibitoria\",\r\n    \r\n    # Pestaña Edición\r\n    \"lab_edit_locked_msg\": \"⚠️ La edición está bloqueada – marca 'Desbloquear edición' en la barra de herramientas.\",\r\n    \"lab_edit_header\": \"Valores neuronales (arrastra para cambiar) – clic 🔒 para bloquear\",\r\n    \"lab_unlock_title\": \"¿Desbloquear edición?\",\r\n    \"lab_unlock_msg\": \"Ahora puedes cambiar valores neuronales y forzar eventos de creación. ¡Úsalo con responsabilidad!\",\r\n    \r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"diminuto\",\r\n    \"lab_inf_mild\": \"leve\",\r\n    \"lab_inf_mod\": \"moderado\",\r\n    \"lab_inf_strong\": \"FUERTE\",\r\n    \r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"El hambre es un impulso homeostático. El hambre alta inhibe la satisfacción y aumenta la ansiedad.\",\r\n    \"lab_tip_happiness\": \"La felicidad es reforzada por neuronas de recompensa. Inhibe la ansiedad y promueve la curiosidad.\",\r\n    \"lab_tip_anxiety\": \"La ansiedad se reduce con neuronas de estrés (inhibitorias). La ansiedad alta suprime la curiosidad.\",\r\n    \"lab_tip_curiosity\": \"La curiosidad aumenta cuando la novedad es alta. Fomenta la exploración y reduce la ansiedad.\",\r\n    \"lab_tip_core\": \"Neurona núcleo – fundamental para la supervivencia.\",\r\n    \"lab_tip_neuro_default\": \"Neurona de neurogénesis – propósito inferido del contexto de nacimiento.\",\r\n    \"lab_tip_neuro_fmt\": \"Creada por <b>{trigger}</b> – se especializa en <b>{spec}</b>. Su trabajo es convertir experiencias en comportamiento a largo plazo.\",\r\n\r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"Visión del Calamar\",\r\n    \"vis_logic_unavailable\": \"Lógica del calamar no disponible.\",\r\n    \"vis_nothing_in_view\": \"Nada a la vista actualmente.\",\r\n    \"vis_distance\": \"distancia\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"Especialización\",\r\n    \"tooltip_type\": \"Tipo\",\r\n    \"tooltip_current\": \"Actual\",\r\n    \"tooltip_utility\": \"Utilidad\",\r\n    \"tooltip_activations\": \"Activaciones\",\r\n    \"tooltip_last_active\": \"Última actividad\",\r\n    \"tooltip_age\": \"Edad\",\r\n    \"tooltip_core\": \"Núcleo\",\r\n    \"tooltip_generated\": \"Generada\",\r\n    \"tooltip_functional\": \"Funcional\",\r\n    \"tooltip_connections_header\": \"Conexiones\",\r\n    \"tooltip_connections_stats\": \"{incoming} ent., {outgoing} sal.\",\r\n    \"tooltip_top_incoming\": \"Entradas principales\",\r\n    \"tooltip_top_outgoing\": \"Salidas principales\",\r\n    \"tooltip_hint\": \"Doble clic para inspeccionar • Clic derecho para opciones\",\r\n\r\n    # State values\r\n    \"state_on\": \"ENC\",\r\n    \"state_off\": \"APAG\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"hace {val}s\",\r\n    \"fmt_m_ago\": \"hace {val}m\",\r\n    \"fmt_h_ago\": \"hace {val}h\",\r\n    \"fmt_s_short\": \"{val}s\",\r\n    \"fmt_m_short\": \"{val}m\",\r\n    \"fmt_h_short\": \"{val}h\",\r\n    \"fmt_d_short\": \"{val}d\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW (NEW) =====\r\n    \"designer_window_title\": \"Diseñador de Cerebro - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"Diseñador de Cerebro - Dosidicus-2 [Importado del Juego]\",\r\n    \r\n    \"designer_tab_layers\": \"Capas\",\r\n    \"designer_tab_sensors\": \"Sensores\",\r\n    \"designer_tab_props\": \"Propiedades\",\r\n    \"designer_tab_connections\": \"Conexiones\",\r\n    \"designer_tab_outputs\": \"Salidas\",\r\n    \r\n    \"designer_btn_generate\": \"🎲 Generar Red Dispersa\",\r\n    \"designer_tooltip_generate\": \"Generar conexiones aleatorias entre neuronas núcleo\",\r\n    \"designer_btn_neuron\": \"➕ Neurona\",\r\n    \"designer_tooltip_neuron\": \"Añadir una nueva neurona (Mayús+N)\",\r\n    \"designer_btn_fix\": \"🔧 Auto-Reparar\",\r\n    \"designer_tooltip_fix\": \"Reparar automáticamente neuronas huérfanas y problemas de conectividad\",\r\n    \"designer_btn_validate\": \"✓ Validar\",\r\n    \"designer_tooltip_validate\": \"Verificar diseño en busca de problemas\",\r\n    \"designer_btn_sync\": \"🔄 Sincronizar desde Juego\",\r\n    \"designer_tooltip_sync\": \"Actualizar estado cerebral desde el juego Dosidicus en ejecución\",\r\n    \"designer_btn_clear_conn\": \"🗑 Borrar Conexiones\",\r\n    \"designer_tooltip_clear_conn\": \"Eliminar todas las conexiones (mantiene neuronas)\",\r\n    \"designer_tooltip_dice\": \"Generar instantáneamente una red aleatoria (sin diálogo)\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 <b>Arrastre-Izquierdo</b> desde neurona para crear conexión\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+Arrastre</b> para mover neurona\",\r\n    \"designer_help_pan\": \"<b>Arrastre-Derecho</b> para mover el lienzo\",\r\n    \"designer_help_zoom\": \"<b>Rueda del Ratón</b> para zoom (o ajustar peso en conexión)\",\r\n    \"designer_help_edit_weight\": \"<b>Doble-Clic</b> en conexión para editar peso\",\r\n    \"designer_help_select\": \"<b>Clic</b> en neurona/conexión para seleccionar\",\r\n    \"designer_help_delete\": \"<b>Supr</b> para eliminar selección\",\r\n    \"designer_help_reverse\": \"<b>Espacio</b> para invertir dirección de conexión\",\r\n    \"designer_help_keys_weight\": \"Teclas <b>+/-</b> para ajustar peso (Mayús para pasos más grandes)\",\r\n    \"designer_help_page_weight\": \"<b>Re Pág/Av Pág</b> para ajustar peso (pasos grandes)\",\r\n    \"designer_help_add_neuron\": \"<b>Mayús+N</b> para añadir neurona\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b> para guardar\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b> para abrir\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b> para exportar\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b> para nuevo diseño\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b> para generar red\",\r\n    \"designer_help_dice\": \"🎲 <b>Botón de dado</b> para generación aleatoria instantánea\",\r\n    \"designer_help_outputs\": \"<b>Pestaña Salidas</b> para vincular neuronas a comportamientos\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"Archivo\",\r\n    \"designer_menu_edit\": \"Editar\",\r\n    \"designer_menu_templates\": \"Plantillas\",\r\n    \"designer_menu_generate\": \"Generar\",\r\n    \r\n    \"designer_action_new\": \"Nuevo Diseño\",\r\n    \"designer_action_save\": \"Guardar...\",\r\n    \"designer_action_export\": \"Exportar para Dosidicus...\",\r\n    \"designer_action_open\": \"Abrir...\",\r\n    \"designer_action_gen_sparse\": \"Generar Red Dispersa...\",\r\n    \"designer_action_autofix\": \"Auto-Reparar Conectividad\",\r\n    \"designer_action_validate\": \"Validar Diseño\",\r\n    \"designer_action_clear_conn\": \"Borrar Todas las Conexiones\",\r\n    \"designer_action_clear_outputs\": \"Borrar Todas las Vinculaciones\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"Neuronas: {count}\",\r\n    \"designer_status_connections\": \"Conexiones: {count}\",\r\n    \"designer_status_required\": \"Requerido: {ok}\",\r\n    \"designer_status_outputs\": \"Salidas: {count}\",\r\n    \"designer_status_selected\": \"Selección: {source} → {target} (peso: {weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"Peso actualizado: {source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"Conexión eliminada: {source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"{count} conexiones borradas\",\r\n    \"designer_status_cleared_out\": \"{count} vinculaciones de salida borradas\",\r\n    \"designer_status_generated\": \"Generadas {count} conexiones usando preajuste '{style}'\",\r\n    \"designer_status_random_gen\": \"🎲 Generadas {count} conexiones aleatorias (estilo: {style})\",\r\n    \"designer_status_synced\": \"✨ Sincronizado: {neurons} neuronas, {connections} conexiones\",\r\n    \"designer_status_imported\": \"✨ Cerebro activo importado desde juego en ejecución\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"Juego No Ejecutándose\",\r\n    \"designer_msg_game_not_running\": \"El juego Dosidicus ya no se está ejecutando.\\n\\nInicia el juego de nuevo para sincronizar.\",\r\n    \"designer_msg_sync_confirm_title\": \"Sincronizar desde Juego\",\r\n    \"designer_msg_sync_confirm\": \"¿Reemplazar diseño actual con el último estado cerebral del juego?\",\r\n    \"designer_msg_sync_failed_title\": \"Sincronización Fallida\",\r\n    \"designer_msg_sync_failed\": \"No se pudo importar estado cerebral desde el juego.\",\r\n    \r\n    \"designer_msg_live_import_title\": \"Importación Cerebral en Vivo\",\r\n    \"designer_msg_live_import_header\": \"🧠 Cerebro activo importado desde juego en ejecución\",\r\n    \"designer_msg_live_import_body\": \"El diseñador ahora muestra la red neuronal exacta de tu juego Dosidicus.\\n\\n• {neurons} neuronas\\n• {connections} conexiones\\n\\nLos cambios realizados aquí NO afectarán el juego en ejecución.\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"Borrar Conexiones\",\r\n    \"designer_msg_clear_conn_confirm\": \"¿Eliminar todas las {count} conexiones?\\n\\nLas neuronas se mantendrán.\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"Borrar Vinculaciones de Salida\",\r\n    \"designer_msg_clear_out_empty\": \"No hay vinculaciones para borrar.\",\r\n    \"designer_msg_clear_out_confirm\": \"¿Eliminar todas las {count} vinculaciones de salida?\",\r\n    \r\n    \"designer_msg_new_design_title\": \"Nuevo Diseño\",\r\n    \"designer_msg_new_design_confirm\": \"¿Iniciar un nuevo diseño? Los cambios no guardados se perderán.\",\r\n    \r\n    \"designer_msg_autofix_title\": \"Auto-Reparar\",\r\n    \"designer_msg_autofix_result\": \"Se crearon {count} conexiones:\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"No se encontraron problemas.\",\r\n    \r\n    \"designer_msg_save_title\": \"Guardar Diseño\",\r\n    \"designer_msg_saved_title\": \"Guardado\",\r\n    \"designer_msg_save_success\": \"Diseño guardado exitosamente: {msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n({count} vinculaciones de salida incluidas)\",\r\n    \"designer_msg_error_title\": \"Error\",\r\n    \"designer_msg_save_fail\": \"Error al guardar diseño:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"Exportar\",\r\n    \"designer_msg_exported_title\": \"Exportado\",\r\n    \"designer_msg_export_success\": \"Diseño exportado exitosamente\",\r\n    \"designer_msg_export_fail\": \"Error al exportar diseño:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"Abrir Diseño\",\r\n    \"designer_msg_open_fail\": \"No se pudo cargar el diseño:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"Cargar Plantilla\",\r\n    \"designer_msg_select_template\": \"Selecciona una plantilla:\",\r\n    \"designer_msg_replace_design\": \"¿Reemplazar diseño actual?\",\r\n    \r\n    \"designer_msg_status_title\": \"Estado del Diseño\",\r\n    \"designer_msg_status_ok\": \"\\n✅ Estado: OK\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ PROBLEMAS:\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"Peso de Conexión\",\r\n    \"designer_input_weight_label\": \"Establecer peso para {source} → {target}:\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"Ninguna neurona seleccionada\",\r\n    \"designer_prop_no_selection_disabled\": \"Sin Selección\",\r\n    \"designer_prop_lbl_name\": \"Nombre:\",\r\n    \"designer_prop_lbl_type\": \"Tipo:\",\r\n    \"designer_prop_lbl_x\": \"X:\",\r\n    \"designer_prop_lbl_y\": \"Y:\",\r\n    \"designer_prop_btn_delete\": \"Eliminar Neurona\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"Añadir Neurona\",\r\n    \"designer_add_grp_type\": \"Seleccionar Tipo de Neurona\",\r\n    \"designer_add_btn_custom\": \"✨ Neurona Personalizada / Plugin\",\r\n    \"designer_add_btn_sensor\": \"📡 Sensor de Entrada\",\r\n    \"designer_add_tooltip_custom\": \"Crear neurona con nombre específico para enlazar con plugins\",\r\n    \"designer_add_grp_sensor\": \"Seleccionar Sensor\",\r\n    \"designer_add_grp_custom\": \"Definir Neurona Personalizada\",\r\n    \"designer_add_info_custom\": \"<i>Para afectar al calamar, el <b>Nombre</b> debe coincidir con un ID de plugin.<br>Ejemplo: Nombre <b>'jet_boost'</b> para activar un plugin de propulsión.</i>\",\r\n    \"designer_add_lbl_id\": \"ID de Plugin / Nombre:\",\r\n    \"designer_add_ph_id\": \"ej. modo_turbo\",\r\n    \"designer_add_btn_create\": \"Crear Enlace\",\r\n    \"designer_add_all_added\": \"Todos los sensores añadidos\",\r\n    \"designer_add_err_title\": \"Error\",\r\n    \"designer_add_err_exists\": \"Ya existe\",\r\n    \"designer_add_msg_created\": \"Creado {name}\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"Añadir Capa\",\r\n    \"designer_layer_dlg_title\": \"Nueva Capa\",\r\n    \"designer_layer_dlg_label\": \"Nombre:\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"Sensores de Entrada:\",\r\n    \"designer_sensor_tooltip_refresh\": \"Actualizar lista de sensores (incluye sensores de plugins)\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"Fuente\",\r\n    \"designer_conn_header_target\": \"Objetivo\",\r\n    \"designer_conn_header_weight\": \"Peso\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>Vinculaciones de Salida</b><br><small>Conecta neuronas a comportamientos. Cuando la activación de una neurona supera el umbral, activa la acción vinculada.</small>\",\r\n    \"designer_output_btn_add\": \"➕ Añadir Vinculación\",\r\n    \"designer_output_btn_edit\": \"✏️ Editar\",\r\n    \"designer_output_btn_remove\": \"🗑️ Eliminar\",\r\n    \"designer_output_col_neuron\": \"Neurona\",\r\n    \"designer_output_col_behavior\": \"→ Comportamiento\",\r\n    \"designer_output_col_threshold\": \"Umbral\",\r\n    \"designer_output_col_mode\": \"Modo\",\r\n    \"designer_output_col_enabled\": \"Habilitado\",\r\n    \"designer_output_info\": \"{count} vinculación(es), {enabled} habilitadas\",\r\n    \"designer_output_err_missing\": \"⚠️ Neurona no encontrada en diseño\",\r\n    \"designer_output_dlg_remove_title\": \"Eliminar Vinculación\",\r\n    \"designer_output_dlg_remove_msg\": \"¿Eliminar vinculación: {neuron} → {hook}?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"Añadir Vinculación de Salida\",\r\n    \"designer_binding_title_edit\": \"Configurar Vinculación de Salida\",\r\n    \"designer_binding_grp_neuron\": \"Neurona de Origen\",\r\n    \"designer_binding_lbl_neuron\": \"Neurona:\",\r\n    \"designer_binding_lbl_current\": \"Actual: --\",\r\n    \"designer_binding_grp_hook\": \"Comportamiento de Salida\",\r\n    \"designer_binding_lbl_trigger\": \"Disparador:\",\r\n    \"designer_binding_grp_settings\": \"Ajustes de Disparo\",\r\n    \"designer_binding_lbl_thresh\": \"Umbral:\",\r\n    \"designer_binding_lbl_mode\": \"Modo:\",\r\n    \"designer_binding_lbl_cool\": \"Enfriamiento:\",\r\n    \"designer_binding_chk_enabled\": \"Habilitado\",\r\n    \"designer_binding_err_neuron\": \"Por favor selecciona una neurona\",\r\n    \"designer_binding_err_hook\": \"Por favor selecciona un comportamiento de salida\",\r\n    \"designer_binding_err_duplicate\": \"Ya existe una vinculación para {neuron} → {hook}\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"Flanco de Subida (cruzar umbral subiendo)\",\r\n    \"designer_mode_falling\": \"Flanco de Bajada (cruzar umbral bajando)\",\r\n    \"designer_mode_above\": \"Mientras Encima (continuo mientras > umbral)\",\r\n    \"designer_mode_below\": \"Mientras Debajo (continuo mientras < umbral)\",\r\n    \"designer_mode_change\": \"Al Cambiar (cualquier cambio significativo)\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"Sensor integrado: {name}\",\r\n    \"desc_vision_food\": \"Detecta comida en cono de visión\",\r\n    \"desc_custom_sensor\": \"Sensor personalizado de {plugin}\",\r\n    \"desc_builtin\": \"integrado\",\r\n    \"desc_plugin\": \"plugin\",\r\n    \"desc_other\": \"otro\",\r\n    \"desc_vision\": \"visión\",\r\n    \r\n    # ===== DESIGNER TEMPLATES =====\r\n    \"tmpl_core_name\": \"🟡 Solo Requeridos\",\r\n    \"tmpl_core_desc\": \"8 neuronas requeridas\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Dosidicus Predeterminado\",\r\n    \"tmpl_dosidicus_desc\": \"Diseño estándar\",\r\n    \"tmpl_full_sensors_name\": \"🟡 Suite Completa de Sensores\",\r\n    \"tmpl_full_sensors_desc\": \"Todos los sensores\",\r\n    \"tmpl_insomniac_name\": \"🔴 El Insomne\",\r\n    \"tmpl_insomniac_desc\": \"Ansiedad y Curiosidad bloquean sueño\",\r\n    \"tmpl_hyperactive_name\": \"🔴 El Hiperactivo\",\r\n    \"tmpl_hyperactive_desc\": \"Neuronas de ruido abruman somnolencia\",\r\n    \"tmpl_hangry_name\": \"🔴 El Hambriento Furioso\",\r\n    \"tmpl_hangry_desc\": \"El hambre causa furia extrema\",\r\n    \"tmpl_depressive_name\": \"🔴 El Depresivo\",\r\n    \"tmpl_depressive_desc\": \"Resistente a la felicidad\",\r\n    \"tmpl_obsessive_name\": \"🔴 El Obsesivo\",\r\n    \"tmpl_obsessive_desc\": \"Bucle de retroalimentación Ansiedad/Curiosidad\",\r\n    \"layer_sensors\": \"Sensores\",\r\n    \"layer_core\": \"Núcleo\",\r\n    \"layer_input\": \"Entrada\",\r\n    \"layer_out\": \"Salida\",\r\n    \"layer_racing_mind\": \"Mente Acelerada\",\r\n    \"layer_state\": \"Estado\",\r\n    \"layer_vision\": \"Visión\",\r\n    \"layer_noise\": \"Ruido\",\r\n    \"layer_output\": \"Salida\",\r\n    \"layer_gut_brain\": \"Cerebro-Intestino\",\r\n    \"layer_gray\": \"Gris\",\r\n    \"layer_loop\": \"Bucle\",\r\n    \"layer_stats\": \"Estadísticas\",\r\n    \"layer_emotions\": \"Emociones\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"Eliminar Conexión\",\r\n    \"designer_cnv_del_conn_msg\": \"¿Estás seguro de que quieres eliminar la conexión:\\n{source} → {target}?\",\r\n    \"designer_cnv_chk_dont_ask\": \"No preguntar de nuevo\",\r\n    \"designer_cnv_btn_del\": \"Sí, Eliminar\",\r\n    \"designer_cnv_btn_cancel\": \"Cancelar\",\r\n    \"designer_cnv_dlg_edit_title\": \"Editar Conexión\",\r\n    \"designer_cnv_lbl_conn\": \"Conexión: {source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"Peso:\",\r\n    \"designer_cnv_info_weight\": \"Positivo = Excitatorio (verde), Negativo = Inhibitorio (rojo)\",\r\n    \"designer_cnv_btn_del_conn\": \"Eliminar Conexión\",\r\n    \"designer_cnv_btn_ok\": \"Aceptar\",\r\n    \"designer_cnv_tooltip_invalid\": \"Conexión inválida\",\r\n}"
  },
  {
    "path": "translations/fr.py",
    "content": "LANGUAGE_HEADER = \"fr - Francais\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"Faim\",\r\n    \"happiness\": \"Bonheur\",\r\n    \"cleanliness\": \"Propreté\",\r\n    \"sleepiness\": \"Somnolence\",\r\n    \"satisfaction\": \"Satisfaction\",\r\n    \"anxiety\": \"Anxiété\",\r\n    \"curiosity\": \"Curiosité\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"Peut Voir de la Nourriture\",\r\n    \"is_eating\": \"Mange\",\r\n    \"is_sleeping\": \"Dort\",\r\n    \"is_sick\": \"Malade\",\r\n    \"pursuing_food\": \"Poursuit de la Nourriture\",\r\n    \"is_startled\": \"Effrayé\",\r\n    \"is_fleeing\": \"Fuit\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"Nouveauté\",\r\n    \"stress\": \"Stress\",\r\n    \"reward\": \"Récompense\",\r\n    \r\n    # ===== MENU PRINCIPAL =====\r\n    \"file\": \"Fichier\",\r\n    \"new_game\": \"Nouvelle Partie\",\r\n    \"load_game\": \"Charger Partie\",\r\n    \"save_game\": \"Sauvegarder\",\r\n    \"view\": \"Affichage\",\r\n    \"speed\": \"Vitesse\",\r\n    \"pause\": \"Pause\",\r\n    \"actions\": \"Actions\",\r\n    \"debug\": \"Debogage\",\r\n    \"plugins\": \"Plugins\",\r\n\r\n    # ===== MENU AFFICHAGE =====\r\n    \"brain_designer\": \"Concepteur de Cerveau\",\r\n    \"decorations\": \"Decorations\",\r\n    \"statistics\": \"Statistiques\",\r\n    \"brain_tool\": \"Outil Cerebral\",\r\n    \"neuron_lab\": \"Laboratoire de Neurones\",\r\n    \"task_manager\": \"Gestionnaire de Taches\",\r\n\r\n    # ===== MENU VITESSE =====\r\n    \"normal_speed\": \"Normal (1x)\",\r\n    \"fast_speed\": \"Rapide (2x)\",\r\n    \"very_fast\": \"Tres Rapide (3x)\",\r\n\r\n    # ===== MENU DEBOGAGE =====\r\n    \"toggle_debug\": \"Basculer Mode Debogage\",\r\n    \"toggle_cone\": \"Basculer Cone de Vision\",\r\n    \"squid_vision\": \"Vision du Calmar\",\r\n\r\n    # ===== BOUTONS D'ACTION =====\r\n    \"feed\": \"Nourrir\",\r\n    \"clean\": \"Nettoyer\",\r\n    \"medicine\": \"Medicament\",\r\n    \"feed_btn\": \"NOURRIR\",\r\n    \"clean_btn\": \"NETTOYER\",\r\n    \"medicine_btn\": \"MEDICAMENT\",\r\n\r\n    # ===== MESSAGES =====\r\n    \"feed_msg\": \"Le calmar a besoin de nourriture\",\r\n    \"points\": \"Points\",\r\n    \"dirty\": \"SALE\",\r\n    \"paused_msg\": \"SIMULATION EN PAUSE\",\r\n    \"paused_sub\": \"Utilisez le menu Vitesse pour reprendre\",\r\n\r\n    # ===== DIALOGUES =====\r\n    \"yes\": \"Oui\",\r\n    \"no\": \"Non\",\r\n    \"ok\": \"OK\",\r\n    \"cancel\": \"Annuler\",\r\n    \"close\": \"Fermer\",\r\n    \"save\": \"Sauvegarder\",\r\n    \"load\": \"Charger\",\r\n    \"reset\": \"Reinitialiser\",\r\n    \"apply_changes\": \"Appliquer les Changements\",\r\n    \"got_it\": \"Compris!\",\r\n    \"finish\": \"Terminer\",\r\n    \"confirm_new_game\": \"Commencer une nouvelle partie? La progression actuelle sera perdue.\",\r\n    \"confirm_exit\": \"Etes-vous sur de vouloir quitter?\",\r\n    \"save_successful\": \"Partie sauvegardee avec succes!\",\r\n    \"load_successful\": \"Partie chargee avec succes!\",\r\n    \"error_saving\": \"Erreur lors de la sauvegarde.\",\r\n    \"error_loading\": \"Erreur lors du chargement.\",\r\n    \"no_save_found\": \"Aucun fichier de sauvegarde trouve.\",\r\n\r\n    # ===== ONGLET A PROPOS =====\r\n    \"hello\": \"BONJOUR\",\r\n    \"my_name_is\": \"je m'appelle\",\r\n    \"change_name\": \"Changer le Nom\",\r\n    \"enter_new_name\": \"Entrez un nouveau nom pour votre calmar:\",\r\n    \"change_colour\": \"Changer la Couleur\",\r\n    \"view_certificate\": \"Voir le Certificat\",\r\n    \"care_tips\": \"Conseils de Soin\",\r\n    \"care_tips_for\": \"Conseils de Soin pour les Calmars {personality}\",\r\n    \"dosidicus_title\": \"Dosidicus electronicus\",\r\n    \"dosidicus_desc\": \"Un animal de compagnie numerique style Tamagotchi avec un reseau neuronal simple\",\r\n    \"string_acronym\": \"Reacciones de Tamagotchi Simuladas mediante Inferencia y Neurogenesis (STRINg)\",\r\n    \"research_project\": \"Ceci est un projet de recherche. Veuillez suggerer des fonctionnalites.\",\r\n    \"version_dosidicus\": \"Version Dosidicus :\",\r\n    \"version_brain_tool\": \"Version Outil Cérébral :\",\r\n    \"version_decision\": \"Version Moteur de Décision :\",\r\n    \"version_neuro\": \"Version Neurogenèse :\",\r\n    \"created_by\": \"par\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"Apprentissage\",\r\n    \"tab_decisions\": \"Décisions\",\r\n    \"tab_personality\": \"Personnalité\",\r\n    \"tab_about\": \"À Propos\",\r\n\r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"Inspecteur de Neurones\",\r\n    \"lbl_name\": \"Nom :\",\r\n    \"lbl_value\": \"Valeur Actuelle :\",\r\n    \"lbl_position\": \"Position :\",\r\n    \"lbl_type\": \"Type :\",\r\n    \"grp_neurogenesis\": \"Détails Neurogenèse\",\r\n    \"lbl_created\": \"Créé Le :\",\r\n    \"lbl_trigger\": \"Type Déclencheur :\",\r\n    \"lbl_trigger_val\": \"Valeur Déclencheur :\",\r\n    \"lbl_state\": \"État Associé :\",\r\n    \"col_connected\": \"Connecté À\",\r\n    \"col_weight\": \"Poids\",\r\n    \"col_direction\": \"Direction\",\r\n    \"btn_refresh_data\": \"Actualiser Données\",\r\n    \"type_core\": \"Noyau\",\r\n    \"type_neuro\": \"Neurogenèse\",\r\n    \"type_system\": \"Statut Système\",\r\n    \"direction_incoming\": \"Entrant\",\r\n    \"direction_outgoing\": \"Sortant\",\r\n\r\n    # ===== PERSONNALITE =====\r\n    \"squid_personality\": \"Personnalite du Calmar\",\r\n    \"personality_modifier\": \"Modificateur de Personnalite\",\r\n    \"description\": \"Description:\",\r\n    \"personality_modifiers\": \"Modificateurs de Personnalite:\",\r\n    \"care_tips_label\": \"Conseils de Soin:\",\r\n    \"personality_note\": \"Note: La personnalite est generee aleatoirement au debut d'une nouvelle partie\",\r\n\r\n    # Types de Personnalite\r\n    \"personality_timid\": \"Timide\",\r\n    \"personality_adventurous\": \"Aventurier\",\r\n    \"personality_lazy\": \"Paresseux\",\r\n    \"personality_energetic\": \"Energique\",\r\n    \"personality_introvert\": \"Introverti\",\r\n    \"personality_greedy\": \"Gourmand\",\r\n    \"personality_stubborn\": \"Tetu\",\r\n\r\n    # Descriptions de Personnalite\r\n    \"desc_timid\": \"Votre calmar est Timide. Il a tendance a etre plus facilement effraye et anxieux, surtout dans les nouvelles situations. Il peut preferer des environnements calmes et pourrait etre moins enclin a explorer seul. Cependant, il peut former des liens forts quand il se sent en securite.\",\r\n    \"desc_adventurous\": \"Votre calmar est Aventurier. Il adore explorer et essayer de nouvelles choses. Il est souvent le premier a examiner de nouveaux objets dans son environnement. Ce calmar s'epanouit avec la nouveaute et pourrait s'ennuyer facilement dans des environnements sans changement.\",\r\n    \"desc_lazy\": \"Votre calmar est Paresseux. Il prefere un style de lifestyle detendu et peut etre moins actif que les autres calmars. Il pourrait avoir besoin d'encouragement supplementaire pour participer aux activites, mais peut etre tres content de simplement se prelasser. Ce calmar est excellent pour economiser l'energie!\",\r\n    \"desc_energetic\": \"Votre calmar est Energique. Il est toujours en mouvement, plein de vie et de vigueur. Ce calmar a besoin de beaucoup de stimulation et d'activites pour etre heureux. Il pourrait devenir agite s'il n'a pas assez d'opportunites pour depenser son exces d'energie.\",\r\n    \"desc_introvert\": \"Votre calmar est Introverti. Il apprecie la solitude et pourrait preferer des espaces plus calmes et moins bondes. Bien qu'il puisse interagir avec les autres, il peut avoir besoin de temps seul pour se ressourcer. Ce calmar pourrait etre plus observateur et reflechi.\",\r\n    \"desc_greedy\": \"Votre calmar est Gourmand. Il a un fort interet pour la nourriture et les ressources. Ce calmar pourrait etre plus motive par les friandises et les recompenses que les autres. Bien qu'il puisse etre plus exigeant, il tend aussi a etre debrouillard et bon pour trouver des friandises cachees!\",\r\n    \"desc_stubborn\": \"Votre calmar est Tetu. Il a une forte volonte et des preferences definies. Ce calmar pourrait etre plus resistant au changement et pourrait prendre plus de temps a s'adapter aux nouvelles routines. Cependant, sa determination peut aussi le rendre persistant pour resoudre les problemes.\",\r\n\r\n    # Modificateurs Courts\r\n    \"mod_timid\": \"Plus grande chance de devenir anxieux\",\r\n    \"mod_adventurous\": \"Curiosite et exploration accrues\",\r\n    \"mod_lazy\": \"Mouvement plus lent et consommation d'energie reduite\",\r\n    \"mod_energetic\": \"Mouvement plus rapide et niveaux d'activite plus eleves\",\r\n    \"mod_introvert\": \"Prefiere la solitude et les environnements calmes\",\r\n    \"mod_greedy\": \"Plus concentre sur la nourriture et les ressources\",\r\n    \"mod_stubborn\": \"Ne mange que sa nourriture preferee (sushi), peut refuser de dormir\",\r\n\r\n    # Details des Modificateurs\r\n    \"modifiers_timid\": \"- L'anxiete augmente 50% plus vite\\n- La curiosite augmente 50% plus lentement\\n- L'anxiete diminue de 50% pres des plantes\",\r\n    \"modifiers_adventurous\": \"- La curiosite augmente 50% plus vite\",\r\n    \"modifiers_lazy\": \"- Se deplace plus lentement\\n- La consommation d'energie est plus faible\",\r\n    \"modifiers_energetic\": \"- Se deplace plus rapidement\\n- La consommation d'energie est plus elevee\",\r\n    \"modifiers_introvert\": \"- Prefere les espaces plus calmes et moins bondes\\n- Peut avoir besoin de plus de temps seul\",\r\n    \"modifiers_greedy\": \"- Devient 50% plus anxieux quand il a faim\\n- La satisfaction augmente davantage en mangeant\",\r\n    \"modifiers_stubborn\": \"- Prefere sa nourriture preferee (sushi)\\n- Peut refuser de dormir meme quand fatigue\",\r\n\r\n    # Conseils de Soin\r\n    \"tips_timid\": \"- Placez des plantes dans l'environnement pour reduire l'anxiete\\n- Gardez l'environnement propre et calme\\n- Approchez lentement et evitez les mouvements brusques\\n- Maintenez une routine constante\\n- Evitez de redimensionner la fenetre frequemment\",\r\n    \"tips_adventurous\": \"- Introduisez regulierement de nouveaux objets ou decorations\\n- Offrez des options de nourriture diversifiees\\n- Encouragez l'exploration avec un placement strategique de la nourriture\\n- Permettez beaucoup d'espace pour explorer\\n- Nourrissez sa curiosite naturelle avec des objets interessants\",\r\n    \"tips_lazy\": \"- Placez la nourriture plus pres des endroits de repos du calmar\\n- Nettoyez l'environnement plus frequemment\\n- Utilisez de la nourriture tentante pour encourager le mouvement\\n- N'attendez pas beaucoup d'activite - ils preferent la relaxation\\n- Assurez-vous que leurs endroits de repos sont confortables\",\r\n    \"tips_energetic\": \"- Fournissez un grand espace ouvert pour le mouvement\\n- Offrez des opportunites frequentes de nourriture\\n- Introduisez des elements ou jeux interactifs\\n- Gardez l'environnement stimulant avec des decorations variees\\n- Ils ont besoin de plus de nourriture en raison d'une consommation d'energie plus elevee\",\r\n    \"tips_introvert\": \"- Creez des zones calmes et isolees avec des decorations\\n- Evitez de surcharger l'environnement\\n- Respectez le besoin du calmar d'etre seul\\n- Creez des espaces abrites avec des plantes\\n- Approchez doucement et donnez de l'espace quand necessaire\",\r\n    \"tips_greedy\": \"- Offrez une variete de types de nourriture, y compris du sushi\\n- Utilisez la nourriture comme recompense pour les comportements souhaites\\n- Attention a ne pas suralimenter\\n- Deviendra plus anxieux quand il a faim\\n- Fournissez des opportunites de collecter des objets\",\r\n    \"tips_stubborn\": \"- Ayez toujours du sushi disponible car c'est sa nourriture preferee\\n- Soyez patient lors de l'introduction de changements\\n- Utilisez le renforcement positif pour les comportements souhaites\\n- Ce calmar peut refuser la nourriture non-sushi\\n- Peut resister au sommeil - creez des environnements calmes\",\r\n\r\n    # ===== ONGLET DECISIONS =====\r\n    \"thought_process\": \"Processus de Pensee du Calmar\",\r\n    \"step\": \"Etape\",\r\n    \"step1_title\": \"Perception du Monde\",\r\n    \"step2_title\": \"Calcul des Pulsions de Base\",\r\n    \"step3_title\": \"Application de la Personnalite et des Souvenirs\",\r\n    \"step4_title\": \"Prise de la Decision Finale\",\r\n    \"final_action\": \"Action Finale:\",\r\n    \"awaiting_thought\": \"En attente de la prochaine pensee du calamar...\",\r\n    \"awaiting_decision\": \"En Attente de Decision...\",\r\n    \"sensing_condition\": \"Le calmar evalue sa condition actuelle et les objets visibles:\",\r\n    \"visible_objects\": \"Objets Visibles\",\r\n    \"no_sensory_data\": \"Aucune donnee sensorielle disponible.\",\r\n    \"none\": \"Aucun\",\r\n    \"no_urges\": \"Aucune pulsion calculee.\",\r\n    \"strongest_urge\": \"Selon les besoins, la pulsion la plus forte est\",\r\n    \"initial_scores\": \"Scores initiaux:\",\r\n    \"personality_memory_adjust\": \"Les traits de personnalite et les souvenirs recents ajustent ces pulsions:\",\r\n    \"no_adjustments\": \"Aucun ajustement significatif de la personnalite ou de la memoire cette fois.\",\r\n    \"final_scores_text\": \"Apres tous les calculs, les scores finaux sont comptabilises. Le score le plus eleve determine l'action.\",\r\n    \"no_final_scores\": \"Aucun score final disponible.\",\r\n    \"squid_decided\": \"Le calmar a decide\",\r\n    \"with_confidence\": \"avec un niveau de confiance de\",\r\n    \"score_increased\": \"a augmente\",\r\n    \"score_decreased\": \"a diminue\",\r\n    \"score_for\": \"Le score pour\",\r\n    \"by_amount\": \"de\",\r\n\r\n    # ===== ONGLET APPRENTISSAGE (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"Paires d'Apprentissage Actives\",\r\n    \"hebbian_cycle\": \"Cycle Hebbien\",\r\n    \"hebbian_paused\": \"EN PAUSE\",\r\n    \"learning_ready\": \"Systeme d'Apprentissage Pret\",\r\n    \"learning_ready_desc\": \"L'apprentissage hebbien creara des associations entre les neurones qui s'activent ensemble.\",\r\n    \"log_cleared\": \"Journal Efface\",\r\n    \"log_cleared_desc\": \"Les paires d'apprentissage apparaitront ici lorsque les neurones formeront de nouvelles connexions.\",\r\n    \"hebbian_overview\": \"Apercu de l'Apprentissage Hebbien\",\r\n    \"neurons_fire_together\": \"Les neurones qui s'activent ensemble se connectent ensemble\",\r\n    \"hebbian_principle\": \"Ce principe fondamental decrit comment les reseaux neuronaux apprennent par l'experience.\",\r\n    \"hebbian_explanation\": \"L'apprentissage hebbien est une regle simple mais puissante utilisee dans les reseaux neuronaux artificiels. Lorsque deux neurones s'activent simultanement, la connexion entre eux se renforce. S'ils s'activent separement, la connexion s'affaiblit.\",\r\n    \"excitatory_connections\": \"Connexions Excitatrices\",\r\n    \"excitatory_desc\": \"Les poids positifs (0.0-1.0) rendent les neurones plus susceptibles de s'activer ensemble\",\r\n    \"inhibitory_connections\": \"Connexions Inhibitrices\",\r\n    \"inhibitory_desc\": \"Les poids negatifs (-1.0-0.0) rendent les neurones moins susceptibles de s'activer ensemble\",\r\n    \"very_strong\": \"Tres Fort\",\r\n    \"strong\": \"Fort\",\r\n    \"moderate\": \"Modere\",\r\n    \"weak\": \"Faible\",\r\n    \"very_weak\": \"Tres Faible\",\r\n    \"inhibited\": \"Inhibe\",\r\n\r\n    # ===== ONGLET MEMOIRE =====\r\n    \"memory\": \"Memoire\",\r\n    \"memories\": \"Souvenirs\",\r\n    \"short_term_memory\": \"Memoire a Court Terme\",\r\n    \"long_term_memory\": \"Memoire a Long Terme\",\r\n    \"no_memories\": \"Aucun souvenir stocke pour l'instant.\",\r\n    \"overview\": \"Aperçu\",\r\n    \"memory_stats\": \"Statistiques de Mémoire\",\r\n    \"categories\": \"Catégories\",\r\n    \"time_label\": \"Heure :\",\r\n    \"important_label\": \"Important\",\r\n    \"unknown\": \"Inconnu\",\r\n    \"category_label\": \"Catégorie :\",\r\n    \"key_label\": \"Clé :\",\r\n    \"access_count\": \"Nombre d'accès :\",\r\n    \"full_content\": \"Contenu Complet :\",\r\n    \"effects_label\": \"Effets :\",\r\n    \"positive\": \"Positif\",\r\n    \"negative\": \"Négatif\",\r\n    \"neutral\": \"Neutre\",\r\n\r\n    # ===== ONGLET RESEAU (ORIGINAL) =====\r\n    \"brain_network\": \"Reseau Cerebral\",\r\n    \"neurons\": \"Neurones\",\r\n    \"connections\": \"Connexions\",\r\n    \"activity\": \"Activite\",\r\n\r\n    # ===== FENETRE STATISTIQUES =====\r\n    \"status\": \"Statut\",\r\n    \"health\": \"Sante\",\r\n\r\n    # ===== NEURON NAMES =====\r\n    \"hunger\": \"Faim\",\r\n    \"happiness\": \"Bonheur\",\r\n    \"cleanliness\": \"Proprete\",\r\n    \"sleepiness\": \"Somnolence\",\r\n    \"satisfaction\": \"Satisfaction\",\r\n    \"curiosity\": \"Curiosite\",\r\n    \"anxiety\": \"Anxiete\",\r\n    \"can_see_food\": \"Voit Nourriture\",\r\n    \"is_eating\": \"Mange\",\r\n    \"is_sleeping\": \"Dort\",\r\n    \"is_sick\": \"Malade\",\r\n    \"is_fleeing\": \"Fuit\",\r\n    \"is_startled\": \"Effrayé\",\r\n    \"pursuing_food\": \"Cherche Nourriture\",\r\n    \"external_stimulus\": \"Stimulus\",\r\n    \"plant_proximity\": \"Pres Plante\",\r\n    \"stress\": \"Stress\",\r\n    \"novelty\": \"Nouveaute\",\r\n    \"reward\": \"Recompense\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS =====\r\n    \"layer_name\": \"Couche\",\r\n    \"layer_input\": \"Entrée\",\r\n    \"layer_output\": \"Sortie\",\r\n    \"layer_hidden\": \"Cachée\",\r\n\r\n    # ===== NEUROGENESIS LOGS =====\r\n    \"log_created\": \"{time} - une neurone {type} ({name}) a été créée car le compteur {type} était à {value:.2f}\",\r\n    \"log_pruned\": \"{time} - une neurone ({name}) a été ÉLAGUÉE à cause de {reason}\",\r\n    \"log_stress_detail\": \"Une connexion inhibitrice a été faite vers ANXIÉTÉ\\nLa valeur maximale d'anxiété a été réduite de façon permanente de 10\",\r\n\r\n    # State Pills\r\n    \"fleeing\": \"En Fuite!\",\r\n    \"startled\": \"Effraye!\",\r\n    \"eating\": \"En Train de Manger\",\r\n    \"sleeping\": \"Endormi\",\r\n    \"playing\": \"En Train de Jouer\",\r\n    \"hiding\": \"Cache\",\r\n    \"anxious\": \"Anxieux\",\r\n    \"curious\": \"Curieux\",\r\n\r\n    # ===== ACTIONS COMMUNES =====\r\n    \"eat\": \"Manger\",\r\n    \"sleep\": \"Dormir\",\r\n    \"play\": \"Jouer\",\r\n    \"explore\": \"Explorer\",\r\n    \"rest\": \"Se Reposer\",\r\n    \"hide\": \"Se Cacher\",\r\n    \"wander\": \"Errer\",\r\n    \"idle\": \"Inactif\",\r\n    \"seek_food\": \"Chercher Nourriture\",\r\n    \"seek_shelter\": \"Chercher Abri\",\r\n\r\n    # ===== OBJETS =====\r\n    \"food\": \"Nourriture\",\r\n    \"rock\": \"Rocher\",\r\n    \"poop\": \"Caca\",\r\n    \"plant\": \"Plante\",\r\n    \"sushi\": \"Sushi\",\r\n    \"decoration\": \"Decoration\",\r\n\r\n    # ===== TUTORIEL =====\r\n    \"tutorial_hatched\": \"Un calamar est ne et vous devez vous en occuper!\",\r\n    \"tutorial_feed\": \"Nourrissez-le quand il a faim (Menu Actions)\",\r\n    \"tutorial_clean\": \"Nettoyez son aquarium quand il devient sale\",\r\n    \"tutorial_watch\": \"Observez son comportement pour decouvrir sa personnalite\",\r\n    \"tutorial_neural\": \"RESEAU NEURONAL\",\r\n    \"tutorial_neural_desc\": \"Ceci est le reseau neuronal du calamar. Son comportement est guide par ses besoins (neurones ronds).\\nLe reseau s'adapte et apprend au fur et a mesure que le calmar interagit avec son environnement.\",\r\n    \"tutorial_satisfaction\": \"Gardez la satisfaction haute et l'anxiete basse.\",\r\n    \"tutorial_traits\": \"Votre calmar developpera des traits et des comportements uniques selon la facon dont vous l'elevez.\",\r\n\r\n    # ===== CONCEPTEUR DE CERVEAU (PRESETS) =====\r\n    \"designer_title\": \"Concepteur de Cerveau\",\r\n    \"required_only\": \"Requis Seulement\",\r\n    \"dosidicus_default\": \"Dosidicus Par Defaut\",\r\n    \"full_sensors\": \"Suite Complete de Capteurs\",\r\n    \"the_insomniac\": \"L'Insomniaque\",\r\n    \"the_hyperactive\": \"L'Hyperactif\",\r\n    \"the_hangry\": \"L'Affame Furieux\",\r\n    \"the_depressive\": \"Le Depressif\",\r\n    \"the_obsessive\": \"L'Obsessif\",\r\n    \"balanced\": \"Equilibre\",\r\n    \"minimal\": \"Minimal\",\r\n    \"dense\": \"Dense\",\r\n    \"chaotic\": \"Chaotique\",\r\n    \"calm\": \"Calme\",\r\n\r\n    # ===== ECRAN DE DEMARRAGE =====\r\n    \"squid_hatched\": \"UN CALMAR EST NE!\",\r\n    \"look_after\": \"VOUS DEVEZ VOUS EN OCCUPER..\",\r\n\r\n    # ===== ETAPES DU TUTORIEL =====\r\n    \"next\": \"Suivant\",\r\n    \"tutorial_step1_text\": \"Un calamar est ne et vous devez vous en occuper!\\n• Nourrissez-le quand il a faim (Menu Actions)\\n• Nettoyez son aquarium quand il devient sale\\n• Observez son comportement pour decouvrir sa personnalite\",\r\n    \"tutorial_step2_text\": \"Ceci est le reseau neuronal du calamar. Son comportement est guide par ses besoins (neurones).\\nLe reseau s'adapte et apprend au fur et a mesure que le calmar interagit avec son environnement.\",\r\n    \"tutorial_step3_text\": \"Le calamar peut generer de nouveaux neurones en reponse a des stimuli environnementaux extremes.\\nCes nouveaux neurones aident le calamar a s'adapter aux situations difficiles.\",\r\n    \"tutorial_step4_text\": \"Quand une paire de neurones s'active en meme temps, leur connexion se renforce. Cela permet au calamar d'apprendre des associations entre differents stimuli et reponses.\",\r\n    \"tutorial_step5_text\": \"Le reseau neuronal prend des decisions basees sur les besoins actuels et les souvenirs passes.\\nChaque decision affecte l'etat du calamar et faconne son comportement futur.\",\r\n    \"tutorial_step6_text\": \"Appuyez sur D a tout moment pour ouvrir la fenetre Decorations\\nGlissez et deposez des decorations dans l'environnement et observez comment le calmar reagit. Chaque type de decoration affecte l'etat mental du calamar de maniere unique. Cliquez et utilisez la molette pour redimensionner/SUPPR pour supprimer\",\r\n    \"tutorial_step7_text\": \"Gardez la satisfaction haute et l'anxiete basse.\\nVotre calmar developpera des traits et comportements uniques selon la facon dont vous l'elevez.\",\r\n\r\n    # ===== NETWORK & LEARNING TABS =====\r\n    \"stats_neurons\": \"Neurones\",\r\n    \"stats_connections\": \"Connexions\",\r\n    \"stats_health\": \"Sante du Reseau\",\r\n    \"emergency_alert\": \"🚨 Urgence: {name}\",\r\n    \"global_cooldown\": \"Recup\",\r\n    \"style_label\": \"Style:\",\r\n    \"chk_links\": \"Liens\",\r\n    \"chk_weights\": \"Poids\",\r\n    \"chk_pruning\": \"Elagage\",\r\n    \"tooltip_brain_designer\": \"Ouvrir Concepteur de Cerveau\",\r\n    \"msg_already_open\": \"Deja Ouvert\",\r\n    \"msg_designer_running\": \"Le Concepteur de Cerveau est deja en cours d'execution!\",\r\n    \"msg_launch_failed\": \"Echec du lancement\",\r\n    \"msg_designer_fail\": \"Impossible de lancer le Concepteur:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"Cerveau Manquant\",\r\n    \"msg_cannot_open_lab\": \"Impossible d'ouvrir le Laboratoire: Widget Cerebral indisponible.\",\r\n    \"msg_cannot_open_buffer\": \"Impossible d'ouvrir le Buffer: Widget Cerebral indisponible.\",\r\n    \"msg_no_neurogenesis\": \"Pas de Neurogenese\",\r\n    \"msg_neurogenesis_not_init\": \"Impossible d'ouvrir le Buffer: Neurogenese non initialisee.\",\r\n    \"msg_decorations_unavailable\": \"Decorations Indisponibles\",\r\n    \"msg_decorations_fail\": \"Impossible d'ouvrir Decorations: Fenetre non disponible.\",\r\n    \"func_neurons_title\": \"Neurones Fonctionnels\",\r\n    \"count_label\": \"Nombre\",\r\n    \"avg_utility_label\": \"Utilite Moyenne\",\r\n    \"total_activations_label\": \"Activations Totales\",\r\n    \"specialisations_label\": \"Specialisations\",\r\n    \"buffer_title\": \"Buffer d'Experience Neurogenese\",\r\n    \"buffer_header\": \"Experiences Recientes\",\r\n    \"col_type\": \"Type\",\r\n    \"col_pattern\": \"Modele\",\r\n    \"col_outcome\": \"Resultat\",\r\n    \"col_time\": \"Temps\",\r\n    \"btn_refresh\": \"Actualiser\",\r\n    \"buffer_size\": \"Taille Buffer\",\r\n    \"top_patterns\": \"Top Modeles\",\r\n    \"no_patterns\": \"Aucun modele encore\",\r\n\r\n    # Learning Tab Educational Content\r\n    \"learning_pairs_tab\": \"Paires d'Apprentissage\",\r\n    \"mechanics_tab\": \"Mecanique\",\r\n    \"hebbian_quote\": \"\\\"Les neurones qui s'activent ensemble se connectent ensemble\\\"\",\r\n    \"in_practice_title\": \"En Pratique\",\r\n    \"in_practice_text\": \"Dans le cerveau de votre calamar, l'apprentissage hebbien associe 'faim' et 'satisfaction' lors des repas, ou 'curiosite' et 'anxiete' lors de l'exploration.\",\r\n    \"mechanics_title\": \"Mecanique d'Apprentissage\",\r\n    \"mechanics_intro\": \"L'apprentissage hebbien met a jour la force de connexion (poids) entre les neurones selon leur activite. S'ils s'activent ensemble, le lien se renforce.\",\r\n    \"learning_rule_title\": \"La Règle d'Apprentissage\",\r\n    \"where_label\": \"Ou:\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = Changement de poids entre deux neurones\",\r\n    \"eta_desc\": \"<b>η</b> (eta) = Taux d'apprentissage (vitesse de changement)\",\r\n    \"activation_desc\": \"<b>x, y</b> = Valeurs d'activation (1 actif, 0 inactif)\",\r\n    \"example_calc_title\": \"Exemple de Calcul\",\r\n    \"scenario_label\": \"<b>Scenario:</b> 'faim' et 'satisfaction' s'activent\",\r\n    \"calc_result\": \"Le poids augmente de 0.1, renforçant la connexion.\",\r\n    \"over_time_title\": \"Avec le Temps\",\r\n    \"over_time_text\": \"Par des activations repetees, ces petits changements s'accumulent. Les schemas frequents creent des connexions fortes, permettant au calmar d'apprendre.\",\r\n    \"str_excitatory\": \"Excitateur Fort\",\r\n    \"weak_excitatory\": \"Excitateur Faible\",\r\n    \"weak_inhibitory\": \"Inhibiteur Faible\",\r\n    \"str_inhibitory\": \"Inhibiteur Fort\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS =====\r\n    \"distance_rollover\": \"🌊 Compteur de distance réinitialisé ! Maintenant à {multiplier}x\",\r\n    \"time_min\": \"min\",\r\n    \"time_mins\": \"mins\",\r\n    \"time_hr\": \"h\",\r\n    \"time_hrs\": \"h\",\r\n    \"time_fmt_hm\": \"{hours}h {minutes}m\",\r\n    \"stat_squid_age\": \"Age du Calmar\",\r\n    \"stat_distance\": \"Distance Nagée (pixels)\",\r\n    \"stat_cheese\": \"Fromage Mangé\",\r\n    \"stat_sushi\": \"Sushi Mangé\",\r\n    \"stat_poops\": \"Cacas Créés\",\r\n    \"stat_max_poops\": \"Max Cacas dans Réservoir\",\r\n    \"stat_startles\": \"Fois Effrayé\",\r\n    \"stat_ink\": \"Nuages d'Encre Créés\",\r\n    \"stat_colour_change\": \"Fois Changement Couleur\",\r\n    \"stat_rocks\": \"Rochers Lancés\",\r\n    \"stat_plants\": \"Interactions Plantes\",\r\n    \"stat_sleep\": \"Temps Total Sommeil (secondes)\",\r\n    \"stat_sickness\": \"Episodes Maladie\",\r\n    \"stat_novelty_neurons\": \"Neurones Nouveauté Créés\",\r\n    \"stat_stress_neurons\": \"Neurones Stress Créés\",\r\n    \"stat_reward_neurons\": \"Neurones Récompense Créés\",\r\n    \"stat_current_neurons\": \"Neurones Actuels\",\r\n\r\n    \"reset_stats_title\": \"Réinitialiser Statistiques\",\r\n    \"reset_stats_msg\": \"Etes-vous sûr de vouloir réinitialiser toutes les statistiques ?\",\r\n    \"export_stats_title\": \"Exporter Statistiques\",\r\n    \"export_file_type\": \"Fichiers Texte (*.txt)\",\r\n    \"export_header\": \"Export Statistiques Calmar\",\r\n    \"export_time\": \"Heure Export\",\r\n    \"export_activity_section\": \"Statistiques Activité\",\r\n    \"export_end\": \"Fin des Statistiques\",\r\n    \"export_success_title\": \"Export Réussi\",\r\n    \"export_success_msg\": \"Statistiques exportées vers {file_name}\",\r\n    \"export_error_title\": \"Erreur Export\",\r\n    \"export_error_msg\": \"Erreur lors de l'export des statistiques : {error}\",\r\n\r\n    # ===== ACHIEVEMENTS =====\r\n    # Categories\r\n    \"cat_feeding\": \"Alimentation\",\r\n    \"cat_neurogenesis\": \"Neurogenèse\",\r\n    \"cat_sleep\": \"Sommeil\",\r\n    \"cat_milestones\": \"Jalons\",\r\n    \"cat_exploration\": \"Exploration\",\r\n    \"cat_cleaning\": \"Nettoyage\",\r\n    \"cat_health\": \"Santé\",\r\n    \"cat_interaction\": \"Interaction\",\r\n    \"cat_ink\": \"Encre\",\r\n    \"cat_memory\": \"Mémoire\",\r\n    \"cat_emotional\": \"Émotionnel\",\r\n    \"cat_secret\": \"Secret\",\r\n    \"cat_meta\": \"Méta\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"Points\",\r\n    \"ui_unlocked\": \"Débloqué\",\r\n    \"ui_achievement_unlocked\": \"Succès Débloqué !\",\r\n    \"ui_hidden\": \"Succès caché\",\r\n    \"ui_all\": \"Tous\",\r\n    \"ui_points_gained\": \"points\",\r\n\r\n    # --- Achievements ---\r\n\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"Première Bouchée\",\r\n    \"ach_first_feeding_desc\": \"Nourrissez le calmar pour la première fois\",\r\n    \"ach_fed_10_times_name\": \"Repas Réguliers\",\r\n    \"ach_fed_10_times_desc\": \"Nourrissez le calmar 10 fois\",\r\n    \"ach_fed_50_times_name\": \"Soigneur Dévoué\",\r\n    \"ach_fed_50_times_desc\": \"Nourrissez le calmar 50 fois\",\r\n    \"ach_fed_100_times_name\": \"Chef Étoilé\",\r\n    \"ach_fed_100_times_desc\": \"Nourrissez le calmar 100 fois\",\r\n    \"ach_fed_500_times_name\": \"Légende Culinaire\",\r\n    \"ach_fed_500_times_desc\": \"Nourrissez le calmar 500 fois\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"Étincelle Cérébrale\",\r\n    \"ach_first_neuron_desc\": \"Créez le premier neurone par neurogenèse\",\r\n    \"ach_neurons_10_name\": \"Réseau Neuronal\",\r\n    \"ach_neurons_10_desc\": \"Créez 10 neurones par neurogenèse\",\r\n    \"ach_neurons_50_name\": \"Esprit en Expansion\",\r\n    \"ach_neurons_50_desc\": \"Créez 50 neurones par neurogenèse\",\r\n    \"ach_neurons_100_name\": \"Puissance Cérébrale\",\r\n    \"ach_neurons_100_desc\": \"Créez 100 neurones par neurogenèse\",\r\n    \"ach_first_neuron_levelup_name\": \"Synapse Renforcée\",\r\n    \"ach_first_neuron_levelup_desc\": \"Améliorez un neurone pour la première fois\",\r\n    \"ach_neuron_max_level_name\": \"Performance Maximale\",\r\n    \"ach_neuron_max_level_desc\": \"Améliorez un neurone à sa puissance maximale\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"Fais de Beaux Rêves\",\r\n    \"ach_first_sleep_desc\": \"Le calmar se réveille de son premier sommeil\",\r\n    \"ach_slept_10_times_name\": \"Bien Reposé\",\r\n    \"ach_slept_10_times_desc\": \"Le calmar a dormi 10 fois\",\r\n    \"ach_dream_state_name\": \"Rêveur Profond\",\r\n    \"ach_dream_state_desc\": \"Le calmar est entré en sommeil paradoxal\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"Une Heure\",\r\n    \"ach_age_1_hour_desc\": \"Le calmar a atteint 1 heure d'âge\",\r\n    \"ach_age_10_hours_name\": \"Ça Grandit\",\r\n    \"ach_age_10_hours_desc\": \"Le calmar a atteint 10 heures d'âge\",\r\n    \"ach_age_24_hours_name\": \"Merveille d'un Jour\",\r\n    \"ach_age_24_hours_desc\": \"Le calmar a survécu 24 heures\",\r\n    \"ach_age_1_week_name\": \"Vétéran de la Semaine\",\r\n    \"ach_age_1_week_desc\": \"Le calmar a vécu une semaine\",\r\n    \"ach_age_1_month_name\": \"Vétéran du Mois\",\r\n    \"ach_age_1_month_desc\": \"Le calmar a vécu un mois\",\r\n    \"ach_happiness_100_name\": \"Bonheur Pur\",\r\n    \"ach_happiness_100_desc\": \"Atteignez 100% de bonheur\",\r\n    \"ach_all_stats_high_name\": \"Équilibre Parfait\",\r\n    \"ach_all_stats_high_desc\": \"Toutes les statistiques au-dessus de 80% simultanément\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"Premier Nettoyage\",\r\n    \"ach_first_clean_desc\": \"Nettoyez l'aquarium pour la première fois\",\r\n    \"ach_cleaned_25_times_name\": \"Environnement Impeccable\",\r\n    \"ach_cleaned_25_times_desc\": \"Nettoyez l'aquarium 25 fois\",\r\n    \"ach_germaphobe_name\": \"Germophobe\",\r\n    \"ach_germaphobe_desc\": \"Gardez la propreté au-dessus de 90% pendant 1 heure\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"Premiers Secours\",\r\n    \"ach_first_medicine_desc\": \"Donnez des médicaments pour la première fois\",\r\n    \"ach_medicine_10_times_name\": \"Docteur Calmar\",\r\n    \"ach_medicine_10_times_desc\": \"Donnez des médicaments 10 fois\",\r\n    \"ach_comeback_kid_name\": \"Le Survivant\",\r\n    \"ach_comeback_kid_desc\": \"Récupérez d'une santé critique (<20%) au maximum\",\r\n\r\n    # Interaction (Rocks)\r\n    \"ach_first_rock_pickup_name\": \"Collectionneur de Pierres\",\r\n    \"ach_first_rock_pickup_desc\": \"Ramassez une pierre pour la première fois\",\r\n    \"ach_rocks_picked_10_name\": \"Ramasseur de Pierres\",\r\n    \"ach_rocks_picked_10_desc\": \"Ramassez 10 pierres\",\r\n    \"ach_rocks_picked_50_name\": \"Amasseur de Rochers\",\r\n    \"ach_rocks_picked_50_desc\": \"Ramassez 50 pierres\",\r\n    \"ach_first_rock_throw_name\": \"Ricochets\",\r\n    \"ach_first_rock_throw_desc\": \"Lancez une pierre pour la première fois\",\r\n    \"ach_rocks_thrown_25_name\": \"Lanceur de Pierres\",\r\n    \"ach_rocks_thrown_25_desc\": \"Lancez 25 pierres\",\r\n    \"ach_rocks_thrown_100_name\": \"Maître de la Catapulte\",\r\n    \"ach_rocks_thrown_100_desc\": \"Lancez 100 pierres\",\r\n\r\n    # Interaction (Decor)\r\n    \"ach_first_decoration_push_name\": \"Décorateur d'Intérieur\",\r\n    \"ach_first_decoration_push_desc\": \"Poussez une décoration pour la première fois\",\r\n    \"ach_decorations_pushed_10_name\": \"Déménageur\",\r\n    \"ach_decorations_pushed_10_desc\": \"Poussez des décorations 10 fois\",\r\n    \"ach_decorations_pushed_50_name\": \"Maître Feng Shui\",\r\n    \"ach_decorations_pushed_50_desc\": \"Poussez des décorations 50 fois\",\r\n    \"ach_first_plant_interact_name\": \"Main Verte\",\r\n    \"ach_first_plant_interact_desc\": \"Interagissez avec une plante pour la première fois\",\r\n    \"ach_plants_interacted_10_name\": \"Explorateur de Jardin\",\r\n    \"ach_plants_interacted_10_desc\": \"Interagissez avec des plantes 10 fois\",\r\n    \"ach_plants_interacted_50_name\": \"Botaniste\",\r\n    \"ach_plants_interacted_50_desc\": \"Interagissez avec des plantes 50 fois\",\r\n    \"ach_objects_investigated_25_name\": \"Inspecteur Curieux\",\r\n    \"ach_objects_investigated_25_desc\": \"Enquêtez sur 25 objets différents\",\r\n    \"ach_objects_investigated_100_name\": \"Maître Détective\",\r\n    \"ach_objects_investigated_100_desc\": \"Enquêtez sur 100 objets différents\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"Fouteur de Trouble\",\r\n    \"ach_first_poop_throw_desc\": \"Le calmar a lancé du caca pour la première fois\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"Écran de Fumée\",\r\n    \"ach_first_ink_cloud_desc\": \"Le calmar libère un nuage d'encre pour la première fois\",\r\n    \"ach_ink_clouds_20_name\": \"Maître de l'Encre\",\r\n    \"ach_ink_clouds_20_desc\": \"Libérez 20 nuages d'encre\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"Premier Souvenir\",\r\n    \"ach_first_memory_desc\": \"Formez le premier souvenir\",\r\n    \"ach_memory_long_term_name\": \"Vision à Long Terme\",\r\n    \"ach_memory_long_term_desc\": \"Promouvez un souvenir dans la mémoire à long terme\",\r\n    \"ach_memories_50_name\": \"Mémoire Photographique\",\r\n    \"ach_memories_50_desc\": \"Ayez 50 souvenirs stockés\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"Georges le Curieux\",\r\n    \"ach_curiosity_100_desc\": \"La curiosité atteint 100%\",\r\n    \"ach_zen_master_name\": \"Maître Zen\",\r\n    \"ach_zen_master_desc\": \"Gardez l'anxiété en dessous de 10% pendant 30 minutes\",\r\n    \"ach_first_startle_name\": \"Sursaut !\",\r\n    \"ach_first_startle_desc\": \"Effrayez le calmar pour la première fois\",\r\n    \"ach_nervous_wreck_name\": \"Épave Nerveuse\",\r\n    \"ach_nervous_wreck_desc\": \"L'anxiété atteint 100%\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"Oiseau de Nuit\",\r\n    \"ach_night_owl_desc\": \"Jouez entre minuit et 4h du matin\",\r\n    \"ach_early_bird_name\": \"Lève-tôt\",\r\n    \"ach_early_bird_desc\": \"Jouez entre 5h et 7h du matin\",\r\n    \"ach_weekend_warrior_name\": \"Guerrier du Week-end\",\r\n    \"ach_weekend_warrior_desc\": \"Jouez le samedi et le dimanche\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"Chirurgien du Cerveau\",\r\n    \"ach_brain_surgeon_desc\": \"Ouvrez l'outil de visualisation du cerveau\",\r\n    \"ach_speed_demon_name\": \"Démon de la Vitesse\",\r\n    \"ach_speed_demon_desc\": \"Lancez la simulation à vitesse maximale pendant 10 minutes\",\r\n    \"ach_completionist_name\": \"Collectionneur\",\r\n    \"ach_completionist_desc\": \"Débloquez 30 autres succès\",\r\n\r\n    # Additional Log/Debug Messages\r\n    \"Hebbian learning chosen pairs:\": \"Paires choisies par apprentissage Hebbien :\",\r\n    \"Main thread: Created neuron\": \"Thread principal : Neurone créé\",\r\n    \"BrainWidget received external BrainWorker\": \"BrainWidget a reçu un BrainWorker externe\",\r\n    \"Neurogenesis monitoring timer started\": \"Minuteur de surveillance de neurogenèse démarré\",\r\n    \"Brain state export enabled for designer sync\": \"Export d'état cérébral activé pour synchronisation designer\",\r\n    \"Animation palette built for style:\": \"Palette d'animation construite pour le style :\",\r\n    \"Animation style changed:\": \"Style d'animation changé :\",\r\n    \"Unknown animation style:\": \"Style d'animation inconnu :\",\r\n    \"Available:\": \"Disponibles :\",\r\n\r\n    # ===== LABORATOIRE DE NEURONES =====\r\n    \"lab_title\": \"🧠 Laboratoire de Neurones\",\r\n    \"lab_live_refresh\": \"Actualisation en direct\",\r\n    \"lab_unlock_editing\": \"🔓 Déverrouiller l'édition\",\r\n    \"lab_tab_overview\": \"📊 Aperçu en Direct\",\r\n    \"lab_tab_inspector\": \"🔍 Inspecteur Approfondi\",\r\n    \"lab_tab_edit\": \"🔧 Bac à Sable d'Édition\",\r\n    \"lab_status_ready\": \"Prêt\",\r\n    \"lab_status_locked\": \"🔒 {name} verrouillé à {value}\",\r\n    \"lab_status_unlocked\": \"🔓 {name} déverrouillé\",\r\n    \r\n    # Onglet Aperçu\r\n    \"lab_ov_counters\": \"Progression des compteurs\",\r\n    \"lab_ov_newest\": \"Neurones de neurogenèse récents\",\r\n    \"lab_ov_limits\": \"Limites et élagage\",\r\n    \"lab_ov_actions\": \"Actions rapides\",\r\n    \"lab_force_hebbian\": \"Forcer cycle Hebbien\",\r\n    \"lab_pruning_enabled\": \"Élagage activé :\",\r\n    \"lab_none_yet\": \"Aucun pour l'instant\",\r\n    \"lab_ago\": \"il y a {seconds}s\",\r\n    \r\n    # Onglet Inspecteur\r\n    \"lab_pick_neuron\": \"Choisissez un neurone à inspecter :\",\r\n    \"lab_connections_title\": \"Connexions (excitatrices vs inhibitrices)\",\r\n    \"lab_header_partner\": \"Partenaire\",\r\n    \"lab_header_weight\": \"Poids\",\r\n    \"lab_header_type\": \"Type\",\r\n    \"lab_header_inf\": \"Influence\",\r\n    \"lab_impact_title\": \"Simulation d'impact fonctionnel\",\r\n    \"lab_header_neuron\": \"Neurone\",\r\n    \"lab_header_delta\": \"Valeur Δ\",\r\n    \"lab_no_connections\": \"Aucune connexion active pour le moment\",\r\n    \"lab_did_you_know\": \"Le saviez-vous ?\",\r\n    \"lab_type_excitatory\": \"Excitateur\",\r\n    \"lab_type_inhibitory\": \"Inhibiteur\",\r\n    \r\n    # Onglet Édition\r\n    \"lab_edit_locked_msg\": \"⚠️ L'édition est verrouillée – cochez 'Déverrouiller l'édition' dans la barre d'outils.\",\r\n    \"lab_edit_header\": \"Valeurs neuronales (glisser pour changer) – clic 🔒 pour verrouiller\",\r\n    \"lab_unlock_title\": \"Déverrouiller l'édition ?\",\r\n    \"lab_unlock_msg\": \"Vous pouvez maintenant changer les valeurs et forcer la création. À utiliser avec responsabilité !\",\r\n    \r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"minuscule\",\r\n    \"lab_inf_mild\": \"léger\",\r\n    \"lab_inf_mod\": \"modéré\",\r\n    \"lab_inf_strong\": \"FORT\",\r\n    \r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"La faim est une pulsion homéostatique. Une faim élevée inhibe la satisfaction et augmente l'anxiété.\",\r\n    \"lab_tip_happiness\": \"Le bonheur est renforcé par les neurones de récompense. Il inhibe l'anxiété et favorise la curiosité.\",\r\n    \"lab_tip_anxiety\": \"L'anxiété est réduite par les neurones de stress (inhibiteurs). Une forte anxiété supprime la curiosité.\",\r\n    \"lab_tip_curiosity\": \"La curiosité augmente quand la nouveauté est élevée. Elle encourage l'exploration et réduit l'anxiété.\",\r\n    \"lab_tip_core\": \"Neurone noyau – fondamental pour la survie.\",\r\n    \"lab_tip_neuro_default\": \"Neurone de neurogenèse – but déduit du contexte de naissance.\",\r\n    \"lab_tip_neuro_fmt\": \"Créé par <b>{trigger}</b> – spécialisé en <b>{spec}</b>. Son travail est de transformer l'expérience en comportement.\",\r\n    \r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"Vision du Calmar\",\r\n    \"vis_logic_unavailable\": \"Logique du calmar indisponible.\",\r\n    \"vis_nothing_in_view\": \"Rien en vue actuellement.\",\r\n    \"vis_distance\": \"distance\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"Spécialisation\",\r\n    \"tooltip_type\": \"Type\",\r\n    \"tooltip_current\": \"Actuel\",\r\n    \"tooltip_utility\": \"Utilité\",\r\n    \"tooltip_activations\": \"Activations\",\r\n    \"tooltip_last_active\": \"Dernière activité\",\r\n    \"tooltip_age\": \"Âge\",\r\n    \"tooltip_core\": \"Noyau\",\r\n    \"tooltip_generated\": \"Générée\",\r\n    \"tooltip_functional\": \"Fonctionnelle\",\r\n    \"tooltip_connections_header\": \"Connexions\",\r\n    \"tooltip_connections_stats\": \"{incoming} entr., {outgoing} sort.\",\r\n    \"tooltip_top_incoming\": \"Entrées principales\",\r\n    \"tooltip_top_outgoing\": \"Sorties principales\",\r\n    \"tooltip_hint\": \"Double-clic pour inspecter • Clic droit pour options\",\r\n\r\n    # State values\r\n    \"state_on\": \"ON\",\r\n    \"state_off\": \"OFF\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"il y a {val}s\",\r\n    \"fmt_m_ago\": \"il y a {val}m\",\r\n    \"fmt_h_ago\": \"il y a {val}h\",\r\n    \"fmt_s_short\": \"{val}s\",\r\n    \"fmt_m_short\": \"{val}m\",\r\n    \"fmt_h_short\": \"{val}h\",\r\n    \"fmt_d_short\": \"{val}j\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW =====\r\n    \"designer_window_title\": \"Concepteur de Cerveau - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"Concepteur de Cerveau - Dosidicus-2 [Importé du Jeu]\",\r\n    \r\n    \"designer_tab_layers\": \"Couches\",\r\n    \"designer_tab_sensors\": \"Capteurs\",\r\n    \"designer_tab_props\": \"Propriétés\",\r\n    \"designer_tab_connections\": \"Connexions\",\r\n    \"designer_tab_outputs\": \"Sorties\",\r\n    \r\n    \"designer_btn_generate\": \"🎲 Générer Réseau Dispersé\",\r\n    \"designer_tooltip_generate\": \"Générer des connexions aléatoires entre les neurones noyaux\",\r\n    \"designer_btn_neuron\": \"➕ Neurone\",\r\n    \"designer_tooltip_neuron\": \"Ajouter un nouveau neurone (Maj+N)\",\r\n    \"designer_btn_fix\": \"🔧 Auto-Correction\",\r\n    \"designer_tooltip_fix\": \"Corriger automatiquement les neurones orphelins et les problèmes de connectivité\",\r\n    \"designer_btn_validate\": \"✓ Valider\",\r\n    \"designer_tooltip_validate\": \"Vérifier la conception pour des problèmes\",\r\n    \"designer_btn_sync\": \"🔄 Sync depuis Jeu\",\r\n    \"designer_tooltip_sync\": \"Actualiser l'état du cerveau depuis le jeu Dosidicus en cours\",\r\n    \"designer_btn_clear_conn\": \"🗑 Effacer Connexions\",\r\n    \"designer_tooltip_clear_conn\": \"Supprimer toutes les connexions (garde les neurones)\",\r\n    \"designer_tooltip_dice\": \"Générer instantanément un réseau aléatoire (pas de dialogue)\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 <b>Glisser-Gauche</b> d'un neurone pour créer une connexion\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+Glisser</b> un neurone pour le déplacer\",\r\n    \"designer_help_pan\": \"<b>Clic-Droit+Glisser</b> pour déplacer le canevas\",\r\n    \"designer_help_zoom\": \"<b>Molette</b> pour zoomer (ou ajuster le poids sur une connexion)\",\r\n    \"designer_help_edit_weight\": \"<b>Double-Clic</b> sur une connexion pour éditer le poids\",\r\n    \"designer_help_select\": \"<b>Clic</b> sur neurone/connexion pour sélectionner\",\r\n    \"designer_help_delete\": \"<b>Suppr</b> pour supprimer la sélection\",\r\n    \"designer_help_reverse\": \"<b>Espace</b> pour inverser la direction de la connexion\",\r\n    \"designer_help_keys_weight\": \"Touches <b>+/-</b> pour ajuster le poids (Maj pour grands pas)\",\r\n    \"designer_help_page_weight\": \"<b>Page Haut/Bas</b> pour ajuster le poids (grands pas)\",\r\n    \"designer_help_add_neuron\": \"<b>Maj+N</b> pour ajouter un neurone\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b> pour sauvegarder\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b> pour ouvrir\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b> pour exporter\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b> pour nouveau design\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b> pour générer réseau\",\r\n    \"designer_help_dice\": \"🎲 <b>Bouton Dé</b> pour génération aléatoire instantanée\",\r\n    \"designer_help_outputs\": \"<b>Onglet Sorties</b> pour lier les neurones aux comportements\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"Fichier\",\r\n    \"designer_menu_edit\": \"Édition\",\r\n    \"designer_menu_templates\": \"Modèles\",\r\n    \"designer_menu_generate\": \"Générer\",\r\n    \r\n    \"designer_action_new\": \"Nouveau Design\",\r\n    \"designer_action_save\": \"Sauvegarder...\",\r\n    \"designer_action_export\": \"Exporter pour Dosidicus...\",\r\n    \"designer_action_open\": \"Ouvrir...\",\r\n    \"designer_action_gen_sparse\": \"Générer Réseau Dispersé...\",\r\n    \"designer_action_autofix\": \"Auto-Correction Connectivité\",\r\n    \"designer_action_validate\": \"Valider Design\",\r\n    \"designer_action_clear_conn\": \"Effacer Toutes les Connexions\",\r\n    \"designer_action_clear_outputs\": \"Effacer Toutes les Liaisons de Sortie\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"Neurones : {count}\",\r\n    \"designer_status_connections\": \"Connexions : {count}\",\r\n    \"designer_status_required\": \"Requis : {ok}\",\r\n    \"designer_status_outputs\": \"Sorties : {count}\",\r\n    \"designer_status_selected\": \"Sélection : {source} → {target} (poids : {weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"Poids mis à jour : {source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"Connexion supprimée : {source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"{count} connexions effacées\",\r\n    \"designer_status_cleared_out\": \"{count} liaisons de sortie effacées\",\r\n    \"designer_status_generated\": \"Généré {count} connexions utilisant le preset '{style}'\",\r\n    \"designer_status_random_gen\": \"🎲 Généré {count} connexions aléatoires (style : {style})\",\r\n    \"designer_status_synced\": \"✨ Synchronisé : {neurons} neurones, {connections} connexions\",\r\n    \"designer_status_imported\": \"✨ Cerveau actif importé du jeu en cours\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"Jeu Non Lancé\",\r\n    \"designer_msg_game_not_running\": \"Le jeu Dosidicus ne tourne plus.\\n\\nRelancez le jeu pour synchroniser.\",\r\n    \"designer_msg_sync_confirm_title\": \"Synchroniser depuis le Jeu\",\r\n    \"designer_msg_sync_confirm\": \"Remplacer le design actuel par le dernier état du cerveau du jeu ?\",\r\n    \"designer_msg_sync_failed_title\": \"Échec de Synchronisation\",\r\n    \"designer_msg_sync_failed\": \"Impossible d'importer l'état du cerveau depuis le jeu.\",\r\n    \r\n    \"designer_msg_live_import_title\": \"Import Cerveau en Direct\",\r\n    \"designer_msg_live_import_header\": \"🧠 Cerveau actif importé du jeu en cours\",\r\n    \"designer_msg_live_import_body\": \"Le concepteur affiche maintenant le réseau neuronal exact de votre jeu Dosidicus.\\n\\n• {neurons} neurones\\n• {connections} connexions\\n\\nLes changements faits ici n'affecteront PAS le jeu en cours.\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"Effacer Connexions\",\r\n    \"designer_msg_clear_conn_confirm\": \"Supprimer les {count} connexions ?\\n\\nLes neurones seront conservés.\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"Effacer Liaisons de Sortie\",\r\n    \"designer_msg_clear_out_empty\": \"Aucune liaison de sortie à effacer.\",\r\n    \"designer_msg_clear_out_confirm\": \"Supprimer les {count} liaisons de sortie ?\",\r\n    \r\n    \"designer_msg_new_design_title\": \"Nouveau Design\",\r\n    \"designer_msg_new_design_confirm\": \"Commencer un nouveau design ? Les changements non sauvegardés seront perdus.\",\r\n    \r\n    \"designer_msg_autofix_title\": \"Auto-Correction\",\r\n    \"designer_msg_autofix_result\": \"Créé {count} connexions :\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"Aucun problème trouvé.\",\r\n    \r\n    \"designer_msg_save_title\": \"Sauvegarder Design\",\r\n    \"designer_msg_saved_title\": \"Sauvegardé\",\r\n    \"designer_msg_save_success\": \"Design sauvegardé avec succès : {msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n({count} liaisons de sortie incluses)\",\r\n    \"designer_msg_error_title\": \"Erreur\",\r\n    \"designer_msg_save_fail\": \"Échec de la sauvegarde du design :\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"Exporter\",\r\n    \"designer_msg_exported_title\": \"Exporté\",\r\n    \"designer_msg_export_success\": \"Design exporté avec succès\",\r\n    \"designer_msg_export_fail\": \"Échec de l'export du design :\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"Ouvrir Design\",\r\n    \"designer_msg_open_fail\": \"Impossible de charger le design :\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"Charger Modèle\",\r\n    \"designer_msg_select_template\": \"Sélectionnez un modèle :\",\r\n    \"designer_msg_replace_design\": \"Remplacer le design actuel ?\",\r\n    \r\n    \"designer_msg_status_title\": \"Statut du Design\",\r\n    \"designer_msg_status_ok\": \"\\n✅ Statut : OK\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ PROBLÈMES :\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"Poids de la Connexion\",\r\n    \"designer_input_weight_label\": \"Définir le poids pour {source} → {target} :\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"Aucun neurone sélectionné\",\r\n    \"designer_prop_no_selection_disabled\": \"Aucune Sélection\",\r\n    \"designer_prop_lbl_name\": \"Nom :\",\r\n    \"designer_prop_lbl_type\": \"Type :\",\r\n    \"designer_prop_lbl_x\": \"X :\",\r\n    \"designer_prop_lbl_y\": \"Y :\",\r\n    \"designer_prop_btn_delete\": \"Supprimer Neurone\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"Ajouter Neurone\",\r\n    \"designer_add_grp_type\": \"Sélectionner Type de Neurone\",\r\n    \"designer_add_btn_custom\": \"✨ Custom / Plugin Neurone\",\r\n    \"designer_add_btn_sensor\": \"📡 Capteur d'Entrée\",\r\n    \"designer_add_tooltip_custom\": \"Créer un neurone avec un nom spécifique pour lier avec des plugins du jeu\",\r\n    \"designer_add_grp_sensor\": \"Sélectionner Capteur\",\r\n    \"designer_add_grp_custom\": \"Définir Neurone Personnalisé\",\r\n    \"designer_add_info_custom\": \"<i>Pour affecter le calmar, le <b>Nom</b> doit correspondre à un ID de plugin.<br>Exemple: Nommez-le <b>'jet_boost'</b> pour activer un plugin jetpack.</i>\",\r\n    \"designer_add_lbl_id\": \"ID Plugin / Nom :\",\r\n    \"designer_add_ph_id\": \"ex. mode_turbo\",\r\n    \"designer_add_btn_create\": \"Créer Lien\",\r\n    \"designer_add_all_added\": \"Tous les capteurs ajoutés\",\r\n    \"designer_add_err_title\": \"Erreur\",\r\n    \"designer_add_err_exists\": \"Existe\",\r\n    \"designer_add_msg_created\": \"Créé {name}\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"Ajouter Couche\",\r\n    \"designer_layer_dlg_title\": \"Nouvelle Couche\",\r\n    \"designer_layer_dlg_label\": \"Nom :\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"Capteurs d'Entrée :\",\r\n    \"designer_sensor_tooltip_refresh\": \"Actualiser la liste des capteurs (inclut ceux enregistrés par plugin)\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"Source\",\r\n    \"designer_conn_header_target\": \"Cible\",\r\n    \"designer_conn_header_weight\": \"Poids\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>Liaisons de Sortie</b><br><small>Connectez les neurones aux comportements du calmar. Quand l'activation d'un neurone dépasse le seuil, l'action liée est déclenchée.</small>\",\r\n    \"designer_output_btn_add\": \"➕ Ajouter Liaison\",\r\n    \"designer_output_btn_edit\": \"✏️ Éditer\",\r\n    \"designer_output_btn_remove\": \"🗑️ Supprimer\",\r\n    \"designer_output_col_neuron\": \"Neurone\",\r\n    \"designer_output_col_behavior\": \"→ Comportement\",\r\n    \"designer_output_col_threshold\": \"Seuil\",\r\n    \"designer_output_col_mode\": \"Mode\",\r\n    \"designer_output_col_enabled\": \"Activé\",\r\n    \"designer_output_info\": \"{count} liaison(s), {enabled} activée(s)\",\r\n    \"designer_output_err_missing\": \"⚠️ Neurone introuvable dans le design\",\r\n    \"designer_output_dlg_remove_title\": \"Supprimer Liaison\",\r\n    \"designer_output_dlg_remove_msg\": \"Supprimer la liaison : {neuron} → {hook} ?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"Ajouter Liaison de Sortie\",\r\n    \"designer_binding_title_edit\": \"Configurer Liaison de Sortie\",\r\n    \"designer_binding_grp_neuron\": \"Neurone Source\",\r\n    \"designer_binding_lbl_neuron\": \"Neurone :\",\r\n    \"designer_binding_lbl_current\": \"Actuel : --\",\r\n    \"designer_binding_grp_hook\": \"Comportement de Sortie\",\r\n    \"designer_binding_lbl_trigger\": \"Déclencheur :\",\r\n    \"designer_binding_grp_settings\": \"Paramètres de Déclenchement\",\r\n    \"designer_binding_lbl_thresh\": \"Seuil :\",\r\n    \"designer_binding_lbl_mode\": \"Mode :\",\r\n    \"designer_binding_lbl_cool\": \"Refroidissement :\",\r\n    \"designer_binding_chk_enabled\": \"Activé\",\r\n    \"designer_binding_err_neuron\": \"Veuillez sélectionner un neurone\",\r\n    \"designer_binding_err_hook\": \"Veuillez sélectionner un comportement de sortie\",\r\n    \"designer_binding_err_duplicate\": \"Une liaison pour {neuron} → {hook} existe déjà\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"Front Montant (traverse le seuil en montant)\",\r\n    \"designer_mode_falling\": \"Front Descendant (traverse le seuil en descendant)\",\r\n    \"designer_mode_above\": \"Tant que Supérieur (continu tant que > seuil)\",\r\n    \"designer_mode_below\": \"Tant que Inférieur (continu tant que < seuil)\",\r\n    \"designer_mode_change\": \"Au Changement (tout changement significatif)\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"Capteur intégré : {name}\",\r\n    \"desc_vision_food\": \"Détecte nourriture dans cône de vision\",\r\n    \"desc_custom_sensor\": \"Capteur personnalisé de {plugin}\",\r\n    \"desc_builtin\": \"intégré\",\r\n    \"desc_plugin\": \"plugin\",\r\n    \"desc_other\": \"autre\",\r\n    \"desc_vision\": \"vision\",\r\n    \r\n    # ===== DESIGNER TEMPLATES =====\r\n    \"tmpl_core_name\": \"🟡 Requis Seulement\",\r\n    \"tmpl_core_desc\": \"8 neurones requis\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Dosidicus Par Défaut\",\r\n    \"tmpl_dosidicus_desc\": \"Disposition standard\",\r\n    \"tmpl_full_sensors_name\": \"🟡 Suite Complète Capteurs\",\r\n    \"tmpl_full_sensors_desc\": \"Tous les capteurs\",\r\n    \"tmpl_insomniac_name\": \"🔴 L'Insomniaque\",\r\n    \"tmpl_insomniac_desc\": \"Anxiété & Curiosité bloquent le sommeil\",\r\n    \"tmpl_hyperactive_name\": \"🔴 L'Hyperactif\",\r\n    \"tmpl_hyperactive_desc\": \"Les neurones de bruit submergent la somnolence\",\r\n    \"tmpl_hangry_name\": \"🔴 L'Affamé Furieux\",\r\n    \"tmpl_hangry_desc\": \"La faim cause une rage extrême\",\r\n    \"tmpl_depressive_name\": \"🔴 Le Dépressif\",\r\n    \"tmpl_depressive_desc\": \"Résistant au bonheur\",\r\n    \"tmpl_obsessive_name\": \"🔴 L'Obsessif\",\r\n    \"tmpl_obsessive_desc\": \"Boucle de rétroaction Anxiété/Curiosité\",\r\n    \"layer_sensors\": \"Capteurs\",\r\n    \"layer_core\": \"Noyau\",\r\n    \"layer_input\": \"Entrée\",\r\n    \"layer_out\": \"Sortie\",\r\n    \"layer_racing_mind\": \"Esprit Galopant\",\r\n    \"layer_state\": \"État\",\r\n    \"layer_vision\": \"Vision\",\r\n    \"layer_noise\": \"Bruit\",\r\n    \"layer_output\": \"Sortie\",\r\n    \"layer_gut_brain\": \"Cerveau-Intestin\",\r\n    \"layer_gray\": \"Gris\",\r\n    \"layer_loop\": \"Boucle\",\r\n    \"layer_stats\": \"Stats\",\r\n    \"layer_emotions\": \"Émotions\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"Supprimer Connexion\",\r\n    \"designer_cnv_del_conn_msg\": \"Êtes-vous sûr de vouloir supprimer la connexion :\\n{source} → {target} ?\",\r\n    \"designer_cnv_chk_dont_ask\": \"Ne plus demander\",\r\n    \"designer_cnv_btn_del\": \"Oui, Supprimer\",\r\n    \"designer_cnv_btn_cancel\": \"Annuler\",\r\n    \"designer_cnv_dlg_edit_title\": \"Éditer Connexion\",\r\n    \"designer_cnv_lbl_conn\": \"Connexion : {source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"Poids :\",\r\n    \"designer_cnv_info_weight\": \"Positif = Excitateur (vert), Négatif = Inhibiteur (rouge)\",\r\n    \"designer_cnv_btn_del_conn\": \"Supprimer Connexion\",\r\n    \"designer_cnv_btn_ok\": \"OK\",\r\n    \"designer_cnv_tooltip_invalid\": \"Connexion invalide\",\r\n}"
  },
  {
    "path": "translations/ja.py",
    "content": "LANGUAGE_HEADER = \"ja - 日本語\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"空腹\",\r\n    \"happiness\": \"幸福\",\r\n    \"cleanliness\": \"清潔さ\",\r\n    \"sleepiness\": \"眠気\",\r\n    \"satisfaction\": \"満足度\",\r\n    \"anxiety\": \"不安\",\r\n    \"curiosity\": \"好奇心\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"餌が見える\",\r\n    \"is_eating\": \"食事中\",\r\n    \"is_sleeping\": \"睡眠中\",\r\n    \"is_sick\": \"病気\",\r\n    \"pursuing_food\": \"餌を追跡中\",\r\n    \"is_startled\": \"驚き\",\r\n    \"is_fleeing\": \"逃走中\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"新規性\",\r\n    \"stress\": \"ストレス\",\r\n    \"reward\": \"報酬\",\r\n\r\n    # ===== MAIN MENU =====\r\n    \"file\": \"ファイル\",\r\n    \"new_game\": \"新規ゲーム\",\r\n    \"load_game\": \"ロード\",\r\n    \"save_game\": \"セーブ\",\r\n    \"view\": \"表示\",\r\n    \"speed\": \"速度\",\r\n    \"pause\": \"一時停止\",\r\n    \"actions\": \"アクション\",\r\n    \"debug\": \"デバッグ\",\r\n    \"plugins\": \"プラグイン\",\r\n\r\n    # ===== VIEW MENU =====\r\n    \"brain_designer\": \"脳デザイナー\",\r\n    \"decorations\": \"装飾\",\r\n    \"statistics\": \"統計\",\r\n    \"brain_tool\": \"脳ツール\",\r\n    \"neuron_lab\": \"ニューロン研究所\",\r\n    \"task_manager\": \"タスクマネージャー\",\r\n\r\n    # ===== SPEED MENU =====\r\n    \"normal_speed\": \"通常 (1x)\",\r\n    \"fast_speed\": \"高速 (2x)\",\r\n    \"very_fast\": \"超高速 (3x)\",\r\n\r\n    # ===== DEBUG MENU =====\r\n    \"toggle_debug\": \"デバッグモード切替\",\r\n    \"toggle_cone\": \"視界コーン切替\",\r\n    \"squid_vision\": \"イカの視界\",\r\n\r\n    # ===== ACTION BUTTONS =====\r\n    \"feed\": \"餌やり\",\r\n    \"clean\": \"掃除\",\r\n    \"medicine\": \"薬\",\r\n    \"feed_btn\": \"餌をやる\",\r\n    \"clean_btn\": \"掃除する\",\r\n    \"medicine_btn\": \"薬をやる\",\r\n\r\n    # ===== MESSAGES =====\r\n    \"feed_msg\": \"イカがお腹を空かせています\",\r\n    \"points\": \"ポイント\",\r\n    \"dirty\": \"汚染\",\r\n    \"paused_msg\": \"シミュレーション停止中\",\r\n    \"paused_sub\": \"再開するには速度メニューを使用してください\",\r\n\r\n    # ===== DIALOGS =====\r\n    \"yes\": \"はい\",\r\n    \"no\": \"いいえ\",\r\n    \"ok\": \"OK\",\r\n    \"cancel\": \"キャンセル\",\r\n    \"close\": \"閉じる\",\r\n    \"save\": \"保存\",\r\n    \"load\": \"読み込み\",\r\n    \"reset\": \"リセット\",\r\n    \"apply_changes\": \"変更を適用\",\r\n    \"got_it\": \"了解！\",\r\n    \"finish\": \"完了\",\r\n    \"confirm_new_game\": \"新しいゲームを始めますか？現在の進行状況は失われます。\",\r\n    \"confirm_exit\": \"終了してもよろしいですか？\",\r\n    \"save_successful\": \"ゲームを保存しました！\",\r\n    \"load_successful\": \"ゲームを読み込みました！\",\r\n    \"error_saving\": \"保存中にエラーが発生しました。\",\r\n    \"error_loading\": \"読み込み中にエラーが発生しました。\",\r\n    \"no_save_found\": \"セーブデータが見つかりません。\",\r\n    \"startup\": \"起動\",\r\n    \"show_tutorial_q\": \"チュートリアルを表示しますか？\",\r\n    \"auto_decline\": \"({seconds}秒後に自動で閉じます)\",\r\n    \"tutorial_title\": \"チュートリアル\",\r\n    \"tutorial_query\": \"チュートリアルを見ますか？\",\r\n\r\n    # ===== ABOUT TAB =====\r\n    \"hello\": \"こんにちは\",\r\n    \"my_name_is\": \"私の名前は\",\r\n    \"change_name\": \"名前変更\",\r\n    \"enter_new_name\": \"イカの新しい名前を入力してください:\",\r\n    \"change_colour\": \"色変更\",\r\n    \"view_certificate\": \"証明書を見る\",\r\n    \"care_tips\": \"飼育のヒント\",\r\n    \"care_tips_for\": \"{personality}なイカの飼育ヒント\",\r\n    \"dosidicus_title\": \"Dosidicus electronicae\",\r\n    \"dosidicus_desc\": \"シンプルなニューラルネットワークを持つたまごっち風デジタルペット\",\r\n    \"string_acronym\": \"推論と神経発生によるシミュレートされたたまごっち反応 (STRINg)\",\r\n    \"research_project\": \"これは研究プロジェクトです。機能の提案をお願いします。\",\r\n    \"version_dosidicus\": \"Dosidicus バージョン:\",\r\n    \"version_brain_tool\": \"脳ツール バージョン:\",\r\n    \"version_decision\": \"意思決定エンジン:\",\r\n    \"version_neuro\": \"神経発生エンジン:\",\r\n    \"created_by\": \"作成者\",\r\n\r\n    # ===== PERSONALITY =====\r\n    \"squid_personality\": \"イカの性格\",\r\n    \"personality_modifier\": \"性格補正\",\r\n    \"description\": \"説明:\",\r\n    \"personality_modifiers\": \"性格による補正:\",\r\n    \"care_tips_label\": \"アドバイス:\",\r\n    \"personality_note\": \"注: 性格は新規ゲーム開始時にランダムに生成されます\",\r\n\r\n    # Personality Types\r\n    \"personality_timid\": \"臆病\",\r\n    \"personality_adventurous\": \"冒険好き\",\r\n    \"personality_lazy\": \"怠け者\",\r\n    \"personality_energetic\": \"エネルギッシュ\",\r\n    \"personality_introvert\": \"内向的\",\r\n    \"personality_greedy\": \"食いしん坊\",\r\n    \"personality_stubborn\": \"頑固\",\r\n\r\n    # Personality Descriptions\r\n    \"desc_timid\": \"あなたのイカは臆病です。新しい状況では驚きやすく、不安になりやすい傾向があります。静かで落ち着いた環境を好み、自分から探索することは少ないかもしれません。しかし、安全だと感じれば強い絆を結ぶことができます。\",\r\n    \"desc_adventurous\": \"あなたのイカは冒険好きです。探索や新しいことを試すのが大好きです。新しい物体や場所を真っ先に調べに行きます。変化のない環境では退屈してしまうかもしれません。\",\r\n    \"desc_lazy\": \"あなたのイカは怠け者です。リラックスした生活を好み、他のイカよりも活動的ではありません。動くには強い動機付けが必要ですが、ただのんびりしているだけで満足することもあります。エネルギーの節約が得意です！\",\r\n    \"desc_energetic\": \"あなたのイカはエネルギッシュです。常に動き回り、活力に満ち溢れています。幸せを保つには多くの刺激と活動が必要です。エネルギーを発散する機会がないと落ち着きがなくなるかもしれません。\",\r\n    \"desc_introvert\": \"あなたのイカは内向的です。孤独を楽しみ、静かで混雑していない場所を好みます。他者と交流することもありますが、「充電」のために一人の時間を必要とします。観察力があり、思慮深い行動をとることがあります。\",\r\n    \"desc_greedy\": \"あなたのイカは食いしん坊です。食べ物や資源に対して強い執着があります。他のイカよりもご褒美に弱いです。要求が多いこともありますが、隠されたおやつを見つけるのが得意です！\",\r\n    \"desc_stubborn\": \"あなたのイカは頑固です。強い意志とはっきりとした好みを持っています。変化に抵抗し、新しいルーチンに慣れるのに時間がかかるかもしれません。しかし、その粘り強さで問題解決に取り組むこともあります。\",\r\n\r\n    # Personality Short Modifiers\r\n    \"mod_timid\": \"不安になりやすい\",\r\n    \"mod_adventurous\": \"好奇心と探索欲が強い\",\r\n    \"mod_lazy\": \"動きが遅く、エネルギー消費が少ない\",\r\n    \"mod_energetic\": \"動きが速く、活動レベルが高い\",\r\n    \"mod_introvert\": \"孤独と静かな環境を好む\",\r\n    \"mod_greedy\": \"食べ物への執着が強い\",\r\n    \"mod_stubborn\": \"好きな食べ物（寿司）しか食べず、寝ないことがある\",\r\n\r\n    # Personality Modifier Details\r\n    \"modifiers_timid\": \"- 不安が50%速く上昇\\n- 好奇心が50%遅く上昇\\n- 植物の近くで不安が50%減少\",\r\n    \"modifiers_adventurous\": \"- 好奇心が50%速く上昇\",\r\n    \"modifiers_lazy\": \"- 移動速度が遅い\\n- エネルギー消費が低い\",\r\n    \"modifiers_energetic\": \"- 移動速度が速い\\n- エネルギー消費が高い\",\r\n    \"modifiers_introvert\": \"- 静かな場所を好む\\n- 一人の時間が必要\",\r\n    \"modifiers_greedy\": \"- 空腹時に不安が50%増大\\n- 食事時の満足度上昇量が大きい\",\r\n    \"modifiers_stubborn\": \"- 寿司を好む\\n- 疲れていても睡眠を拒否することがある\",\r\n\r\n    # Care Tips\r\n    \"tips_timid\": \"- 植物を置いて不安を和らげる\\n- 環境を清潔で静かに保つ\\n- 急な動きを避ける\\n- 一定のルーチンを保つ\\n- 頻繁なウィンドウサイズ変更を避ける\",\r\n    \"tips_adventurous\": \"- 新しい装飾を定期的に導入する\\n- 様々な食べ物を与える\\n- 餌を隠して探索を促す\\n- 広い探索スペースを確保する\",\r\n    \"tips_lazy\": \"- 休憩場所の近くに餌を置く\\n- こまめに掃除する\\n- 美味しい餌で運動を促す\\n- あまり活発に動くことを期待しない\",\r\n    \"tips_energetic\": \"- 動き回れる広いスペースを用意する\\n- 頻繁に食事の機会を与える\\n- おもちゃや装飾で刺激を与える\\n- エネルギー消費が多いため多くの餌が必要\",\r\n    \"tips_introvert\": \"- 装飾で隠れられる場所を作る\\n- 環境を詰め込みすぎない\\n- 一人の時間を尊重する\\n- 優しく接する\",\r\n    \"tips_greedy\": \"- 寿司を含む多様な食事を与える\\n- 良い行動への報酬として餌を使う\\n- 食べ過ぎに注意\\n- 空腹時は特に不安になりやすい\",\r\n    \"tips_stubborn\": \"- 常に好物の寿司を用意しておく\\n- 変化を加えるときは根気よく\\n- 褒めて伸ばす\\n- 落ち着ける環境を作って睡眠を促す\",\r\n\r\n    # ===== DECISIONS TAB =====\r\n    \"thought_process\": \"イカの思考プロセス\",\r\n    \"step\": \"ステップ\",\r\n    \"step1_title\": \"世界の認識\",\r\n    \"step2_title\": \"基本的欲求の計算\",\r\n    \"step3_title\": \"性格と記憶の適用\",\r\n    \"step4_title\": \"最終決定\",\r\n    \"final_action\": \"最終アクション:\",\r\n    \"awaiting_thought\": \"次の思考を待機中...\",\r\n    \"awaiting_decision\": \"決定を待機中...\",\r\n    \"sensing_condition\": \"現在の状態と視界内の物体を評価中:\",\r\n    \"visible_objects\": \"視界内の物体\",\r\n    \"no_sensory_data\": \"感覚データなし\",\r\n    \"none\": \"なし\",\r\n    \"no_urges\": \"欲求なし\",\r\n    \"strongest_urge\": \"最も強い欲求は\",\r\n    \"initial_scores\": \"初期スコア:\",\r\n    \"personality_memory_adjust\": \"性格と最近の記憶による調整:\",\r\n    \"no_adjustments\": \"性格や記憶による大きな調整はありません。\",\r\n    \"final_scores_text\": \"最終スコアを集計し、最も高いスコアのアクションを実行します。\",\r\n    \"no_final_scores\": \"最終スコアなし\",\r\n    \"squid_decided\": \"イカの決定:\",\r\n    \"with_confidence\": \"(確信度:\",\r\n    \"score_increased\": \"増加\",\r\n    \"score_decreased\": \"減少\",\r\n    \"score_for\": \"対象:\",\r\n    \"by_amount\": \"量:\",\r\n\r\n    # ===== LEARNING TAB (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"アクティブな学習ペア\",\r\n    \"hebbian_cycle\": \"ヘブ学習サイクル\",\r\n    \"hebbian_paused\": \"一時停止中\",\r\n    \"learning_ready\": \"学習システム準備完了\",\r\n    \"learning_ready_desc\": \"同時に活性化したニューロン間の結合を強化します。\",\r\n    \"log_cleared\": \"ログ消去済み\",\r\n    \"log_cleared_desc\": \"新しい結合が形成されるとここに表示されます。\",\r\n    \"hebbian_overview\": \"ヘブ学習の概要\",\r\n    \"neurons_fire_together\": \"共に発火するニューロンは結合する\",\r\n    \"hebbian_principle\": \"ニューラルネットワークが経験を通じて学習する基本原理です。\",\r\n    \"hebbian_explanation\": \"2つのニューロンが同時に活性化すると、その間の結合（重み）が強化されます。別々に活性化すると弱まります。\",\r\n    \"excitatory_connections\": \"興奮性結合\",\r\n    \"excitatory_desc\": \"正の重み (0.0-1.0) - 一緒に活性化しやすくなる\",\r\n    \"inhibitory_connections\": \"抑制性結合\",\r\n    \"inhibitory_desc\": \"負の重み (-1.0-0.0) - 相手の活性化を抑える\",\r\n    \"very_strong\": \"非常に強い\",\r\n    \"strong\": \"強い\",\r\n    \"moderate\": \"中程度\",\r\n    \"weak\": \"弱い\",\r\n    \"very_weak\": \"非常に弱い\",\r\n    \"inhibited\": \"抑制\",\r\n\r\n    # ===== MEMORY TAB =====\r\n    \"memory\": \"記憶\",\r\n    \"memories\": \"記憶リスト\",\r\n    \"short_term_memory\": \"短期記憶\",\r\n    \"long_term_memory\": \"長期記憶\",\r\n    \"no_memories\": \"記憶はまだありません。\",\r\n    \"overview\": \"概要\",\r\n    \"memory_stats\": \"記憶統計\",\r\n    \"categories\": \"カテゴリー\",\r\n    \"time_label\": \"時間:\",\r\n    \"important_label\": \"重要\",\r\n    \"unknown\": \"不明\",\r\n    \"category_label\": \"カテゴリー:\",\r\n    \"key_label\": \"キー:\",\r\n    \"access_count\": \"アクセス数:\",\r\n    \"full_content\": \"内容:\",\r\n    \"effects_label\": \"効果:\",\r\n    \"positive\": \"ポジティブ\",\r\n    \"negative\": \"ネガティブ\",\r\n    \"neutral\": \"中立\",\r\n\r\n    # ===== NETWORK TAB (ORIGINAL) =====\r\n    \"brain_network\": \"脳ネットワーク\",\r\n    \"neurons\": \"ニューロン\",\r\n    \"connections\": \"結合\",\r\n    \"activity\": \"活動\",\r\n\r\n    # ===== STATISTICS WINDOW =====\r\n    \"status\": \"ステータス\",\r\n    \"health\": \"健康状態\",\r\n\r\n    # ===== NEURON NAMES (NEW) =====\r\n    \"hunger\": \"空腹\",\r\n    \"happiness\": \"幸福\",\r\n    \"cleanliness\": \"清潔さ\",\r\n    \"sleepiness\": \"眠気\",\r\n    \"satisfaction\": \"満足\",\r\n    \"curiosity\": \"好奇心\",\r\n    \"anxiety\": \"不安\",\r\n    \"can_see_food\": \"餌視認\",\r\n    \"is_eating\": \"食事中\",\r\n    \"is_sleeping\": \"睡眠中\",\r\n    \"is_sick\": \"病気\",\r\n    \"is_fleeing\": \"逃走中\",\r\n    \"is_startled\": \"驚き\",\r\n    \"pursuing_food\": \"餌追跡\",\r\n    \"external_stimulus\": \"外部刺激\",\r\n    \"plant_proximity\": \"植物近接\",\r\n    \"stress\": \"ストレス\",\r\n    \"novelty\": \"新規性\",\r\n    \"reward\": \"報酬\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS (NEW) =====\r\n    \"layer_name\": \"層\",\r\n    \"layer_input\": \"入力\",\r\n    \"layer_output\": \"出力\",\r\n    \"layer_hidden\": \"隠れ層\",\r\n\r\n    # ===== NEUROGENESIS LOGS (NEW) =====\r\n    \"log_created\": \"{time} - {type}ニューロン ({name}) が生成されました ({type}カウンター: {value:.2f})\",\r\n    \"log_pruned\": \"{time} - ニューロン ({name}) が剪定（削除）されました。理由: {reason}\",\r\n    \"log_stress_detail\": \"不安への抑制性結合が形成されました\\n最大不安値が永続的に10減少しました\",\r\n\r\n    # State Pills\r\n    \"fleeing\": \"逃走中！\",\r\n    \"startled\": \"びっくり！\",\r\n    \"eating\": \"食事中\",\r\n    \"sleeping\": \"睡眠中\",\r\n    \"playing\": \"遊んでいます\",\r\n    \"hiding\": \"隠れています\",\r\n    \"anxious\": \"不安\",\r\n    \"curious\": \"興味津々\",\r\n\r\n    # ===== COMMON ACTIONS =====\r\n    \"eat\": \"食べる\",\r\n    \"sleep\": \"寝る\",\r\n    \"play\": \"遊ぶ\",\r\n    \"explore\": \"探索\",\r\n    \"rest\": \"休む\",\r\n    \"hide\": \"隠れる\",\r\n    \"wander\": \"徘徊\",\r\n    \"idle\": \"待機\",\r\n    \"seek_food\": \"餌を探す\",\r\n    \"seek_shelter\": \"避難所を探す\",\r\n\r\n    # ===== OBJECTS =====\r\n    \"food\": \"餌\",\r\n    \"rock\": \"石\",\r\n    \"poop\": \"フン\",\r\n    \"plant\": \"植物\",\r\n    \"sushi\": \"寿司\",\r\n    \"decoration\": \"装飾\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"イカが孵化しました！世話をしてあげましょう。\",\r\n    \"tutorial_feed\": \"お腹が空いたら餌をあげてください（アクションメニュー）\",\r\n    \"tutorial_clean\": \"汚れたら水槽を掃除してください\",\r\n    \"tutorial_watch\": \"行動を観察して性格を理解しましょう\",\r\n    \"tutorial_neural\": \"ニューラルネットワーク\",\r\n    \"tutorial_neural_desc\": \"これはイカの脳です。行動は欲求（丸いニューロン）によって決定されます。\\n環境との相互作用を通じてネットワークは適応・学習します。\",\r\n    \"tutorial_satisfaction\": \"満足度を高く、不安を低く保ちましょう。\",\r\n    \"tutorial_traits\": \"育て方によって独自の特性や行動が発達します。\",\r\n\r\n    # ===== BRAIN DESIGNER (Templates) =====\r\n    \"designer_title\": \"脳デザイナー\",\r\n    \"required_only\": \"必須のみ\",\r\n    \"dosidicus_default\": \"Dosidicus デフォルト\",\r\n    \"full_sensors\": \"全センサー搭載\",\r\n    \"the_insomniac\": \"不眠症\",\r\n    \"the_hyperactive\": \"多動性\",\r\n    \"the_hangry\": \"ハングリー・怒り\",\r\n    \"the_depressive\": \"抑うつ\",\r\n    \"the_obsessive\": \"強迫性\",\r\n    \"balanced\": \"バランス型\",\r\n    \"minimal\": \"最小構成\",\r\n    \"dense\": \"高密度\",\r\n    \"chaotic\": \"カオス\",\r\n    \"calm\": \"平穏\",\r\n\r\n    # ===== SPLASH SCREEN =====\r\n    \"squid_hatched\": \"イカが孵化しました！\",\r\n    \"look_after\": \"大切に育ててください...\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"学習\",\r\n    \"tab_decisions\": \"意思決定\",\r\n    \"tab_personality\": \"性格\",\r\n    \"tab_about\": \"情報\",\r\n\r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"ニューロンインスペクター\",\r\n    \"lbl_name\": \"名前:\",\r\n    \"lbl_value\": \"現在値:\",\r\n    \"lbl_position\": \"位置:\",\r\n    \"lbl_type\": \"タイプ:\",\r\n    \"grp_neurogenesis\": \"神経発生の詳細\",\r\n    \"lbl_created\": \"作成日時:\",\r\n    \"lbl_trigger\": \"トリガータイプ:\",\r\n    \"lbl_trigger_val\": \"トリガー値:\",\r\n    \"lbl_state\": \"関連状態:\",\r\n    \"col_connected\": \"接続先\",\r\n    \"col_weight\": \"重み\",\r\n    \"col_direction\": \"方向\",\r\n    \"btn_refresh_data\": \"データ更新\",\r\n    \"type_core\": \"コア\",\r\n    \"type_neuro\": \"神経発生\",\r\n    \"type_system\": \"システム状態\",\r\n    \"direction_incoming\": \"入力\",\r\n    \"direction_outgoing\": \"出力\",\r\n\r\n    # ===== TUTORIAL STEPS =====\r\n    \"next\": \"次へ\",\r\n    \"tutorial_step1_text\": \"イカが孵化しました！世話をしてあげましょう。\\n• お腹が空いたら餌をあげる（アクションメニュー）\\n• 汚れたら掃除する\\n• 行動を観察して性格を知る\",\r\n    \"tutorial_step2_text\": \"これはイカのニューラルネットワークです。行動は欲求（ニューロン）によって決まります。\\nネットワークは環境との相互作用を通じて適応し、学習します。\",\r\n    \"tutorial_step3_text\": \"イカは極端な環境刺激に反応して新しいニューロンを生成することがあります。\\nこれらは困難な状況に適応するのを助けます。\",\r\n    \"tutorial_step4_text\": \"2つのニューロンが同時に発火すると、その結合が強化されます。これにより刺激と反応の関連付けを学習します。\",\r\n    \"tutorial_step5_text\": \"ニューラルネットワークは現在の欲求と過去の記憶に基づいて決定を下します。\\n各決定はイカの状態と将来の行動に影響を与えます。\",\r\n    \"tutorial_step6_text\": \"「D」キーを押すと装飾ウィンドウが開きます。\\nドラッグ＆ドロップで配置し、イカの反応を見てみましょう。クリック＋ホイールでサイズ変更、DELキーで削除できます。\",\r\n    \"tutorial_step7_text\": \"満足度を高く、不安を低く保ってください。\\n育て方次第で、あなたのイカは独自の特性を発達させます。\",\r\n\r\n    # ===== NETWORK & LEARNING TABS (NEW) =====\r\n    \"stats_neurons\": \"ニューロン数\",\r\n    \"stats_connections\": \"結合数\",\r\n    \"stats_health\": \"ネットワーク健全性\",\r\n    \"emergency_alert\": \"🚨 緊急警報: {name}\",\r\n    \"global_cooldown\": \"クールダウン\",\r\n    \"style_label\": \"スタイル:\",\r\n    \"chk_links\": \"リンクを表示\",\r\n    \"chk_weights\": \"重みを表示\",\r\n    \"chk_pruning\": \"剪定を有効化\",\r\n    \"tooltip_brain_designer\": \"脳デザイナーを開く\",\r\n    \"msg_already_open\": \"既に開いています\",\r\n    \"msg_designer_running\": \"脳デザイナーは既に起動しています！\",\r\n    \"msg_launch_failed\": \"起動失敗\",\r\n    \"msg_designer_fail\": \"脳デザイナーを開始できませんでした:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"脳が見つかりません\",\r\n    \"msg_cannot_open_lab\": \"ニューロン研究所を開けません: 脳ウィジェットが利用できません。\",\r\n    \"msg_cannot_open_buffer\": \"経験バッファを開けません: 脳ウィジェットが利用できません。\",\r\n    \"msg_no_neurogenesis\": \"神経発生なし\",\r\n    \"msg_neurogenesis_not_init\": \"バッファを開けません: 神経発生システムが初期化されていません。\",\r\n    \"msg_decorations_unavailable\": \"装飾利用不可\",\r\n    \"msg_decorations_fail\": \"装飾を開けません: ウィンドウが利用できません。\",\r\n    \"func_neurons_title\": \"機能的ニューロン\",\r\n    \"count_label\": \"数\",\r\n    \"avg_utility_label\": \"平均有用性\",\r\n    \"total_activations_label\": \"総活性化数\",\r\n    \"specialisations_label\": \"専門化\",\r\n    \"buffer_title\": \"神経発生経験バッファ\",\r\n    \"buffer_header\": \"最近の経験\",\r\n    \"col_type\": \"タイプ\",\r\n    \"col_pattern\": \"パターン\",\r\n    \"col_outcome\": \"結果\",\r\n    \"col_time\": \"時間\",\r\n    \"btn_refresh\": \"更新\",\r\n    \"buffer_size\": \"バッファサイズ\",\r\n    \"top_patterns\": \"トップパターン\",\r\n    \"no_patterns\": \"パターンなし\",\r\n\r\n    # Learning Tab Educational Content\r\n    \"learning_pairs_tab\": \"学習ペア\",\r\n    \"mechanics_tab\": \"メカニズム\",\r\n    \"hebbian_quote\": \"「共に発火するニューロンは、結合する」\",\r\n    \"in_practice_title\": \"実践において\",\r\n    \"in_practice_text\": \"あなたのイカの脳内では、ヘブ学習が「空腹」と食事時の「満足」、あるいは探索時の「好奇心」と「不安」のような状態を関連付けます。\",\r\n    \"mechanics_title\": \"学習の仕組み\",\r\n    \"mechanics_intro\": \"ヘブ学習は、活動パターンに基づいてニューロン間の結合強度（重み）を更新します。同時に活性化すれば強化され、別々なら弱まります。\",\r\n    \"learning_rule_title\": \"学習ルール\",\r\n    \"where_label\": \"定義:\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = 重みの変化量\",\r\n    \"eta_desc\": \"<b>η</b> (eta) = 学習率（変化の速さ）\",\r\n    \"activation_desc\": \"<b>x, y</b> = 活性化値 (1: 活性, 0: 非活性)\",\r\n    \"example_calc_title\": \"計算例\",\r\n    \"scenario_label\": \"<b>シナリオ:</b> 「空腹」と「満足」が両方活性化\",\r\n    \"calc_result\": \"重みが0.1増加し、結合が強化されます。\",\r\n    \"over_time_title\": \"時間の経過とともに\",\r\n    \"over_time_text\": \"繰り返しの活性化により、小さな重みの変化が蓄積します。頻繁に起こるパターンは強い結合を作り、これが学習となります！\",\r\n    \"str_excitatory\": \"強い興奮性\",\r\n    \"weak_excitatory\": \"弱い興奮性\",\r\n    \"weak_inhibitory\": \"弱い抑制性\",\r\n    \"str_inhibitory\": \"強い抑制性\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS =====\r\n    \"distance_rollover\": \"🌊 距離カウンターが一周しました！ 現在 {multiplier}周目\",\r\n    \"time_min\": \"分\",\r\n    \"time_mins\": \"分\",\r\n    \"time_hr\": \"時間\",\r\n    \"time_hrs\": \"時間\",\r\n    \"time_fmt_hm\": \"{hours}時間 {minutes}分\",\r\n    \"stat_squid_age\": \"イカの年齢\",\r\n    \"stat_distance\": \"泳いだ距離 (px)\",\r\n    \"stat_cheese\": \"食べたチーズ\",\r\n    \"stat_sushi\": \"食べた寿司\",\r\n    \"stat_poops\": \"フンの数\",\r\n    \"stat_max_poops\": \"最大フン蓄積数\",\r\n    \"stat_startles\": \"驚いた回数\",\r\n    \"stat_ink\": \"吐いた墨の数\",\r\n    \"stat_colour_change\": \"色が変わった回数\",\r\n    \"stat_rocks\": \"投げた石\",\r\n    \"stat_plants\": \"植物との接触\",\r\n    \"stat_sleep\": \"総睡眠時間 (秒)\",\r\n    \"stat_sickness\": \"病気になった回数\",\r\n    \"stat_novelty_neurons\": \"新規性ニューロン作成数\",\r\n    \"stat_stress_neurons\": \"ストレスニューロン作成数\",\r\n    \"stat_reward_neurons\": \"報酬ニューロン作成数\",\r\n    \"stat_current_neurons\": \"現在のニューロン数\",\r\n\r\n    \"reset_stats_title\": \"統計のリセット\",\r\n    \"reset_stats_msg\": \"本当にすべての統計をリセットしますか？\",\r\n    \"export_stats_title\": \"統計のエクスポート\",\r\n    \"export_file_type\": \"テキストファイル (*.txt)\",\r\n    \"export_header\": \"イカ統計エクスポート\",\r\n    \"export_time\": \"エクスポート日時\",\r\n    \"export_activity_section\": \"活動統計\",\r\n    \"export_end\": \"統計終了\",\r\n    \"export_success_title\": \"エクスポート成功\",\r\n    \"export_success_msg\": \"統計を {file_name} にエクスポートしました\",\r\n    \"export_error_title\": \"エクスポートエラー\",\r\n    \"export_error_msg\": \"エクスポート中にエラーが発生しました: {error}\",\r\n\r\n    # ===== ACHIEVEMENTS (NEW) =====\r\n    # Categories\r\n    \"cat_feeding\": \"給餌\",\r\n    \"cat_neurogenesis\": \"神経発生\",\r\n    \"cat_sleep\": \"睡眠\",\r\n    \"cat_milestones\": \"マイルストーン\",\r\n    \"cat_exploration\": \"探索\",\r\n    \"cat_cleaning\": \"掃除\",\r\n    \"cat_health\": \"健康\",\r\n    \"cat_interaction\": \"交流\",\r\n    \"cat_ink\": \"墨\",\r\n    \"cat_memory\": \"記憶\",\r\n    \"cat_emotional\": \"感情\",\r\n    \"cat_secret\": \"秘密\",\r\n    \"cat_meta\": \"メタ\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"ポイント\",\r\n    \"ui_unlocked\": \"解除済み\",\r\n    \"ui_achievement_unlocked\": \"実績解除！\",\r\n    \"ui_hidden\": \"隠し実績\",\r\n    \"ui_all\": \"すべて\",\r\n    \"ui_points_gained\": \"ポイント\",\r\n\r\n    # --- Achievements ---\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"初めての一口\",\r\n    \"ach_first_feeding_desc\": \"初めてイカに餌をやる\",\r\n    \"ach_fed_10_times_name\": \"定食\",\r\n    \"ach_fed_10_times_desc\": \"10回餌をやる\",\r\n    \"ach_fed_50_times_name\": \"専属シェフ\",\r\n    \"ach_fed_50_times_desc\": \"50回餌をやる\",\r\n    \"ach_fed_100_times_name\": \"巨匠\",\r\n    \"ach_fed_100_times_desc\": \"100回餌をやる\",\r\n    \"ach_fed_500_times_name\": \"伝説の料理人\",\r\n    \"ach_fed_500_times_desc\": \"500回餌をやる\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"脳のひらめき\",\r\n    \"ach_first_neuron_desc\": \"最初の神経発生ニューロンを作成する\",\r\n    \"ach_neurons_10_name\": \"ニューラルネットワーク\",\r\n    \"ach_neurons_10_desc\": \"神経発生で10個のニューロンを作成\",\r\n    \"ach_neurons_50_name\": \"拡張する精神\",\r\n    \"ach_neurons_50_desc\": \"神経発生で50個のニューロンを作成\",\r\n    \"ach_neurons_100_name\": \"脳力発電所\",\r\n    \"ach_neurons_100_desc\": \"神経発生で100個のニューロンを作成\",\r\n    \"ach_first_neuron_levelup_name\": \"シナプス強化\",\r\n    \"ach_first_neuron_levelup_desc\": \"初めてニューロンをレベルアップさせる\",\r\n    \"ach_neuron_max_level_name\": \"最高性能\",\r\n    \"ach_neuron_max_level_desc\": \"ニューロンを最大レベルまで強化する\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"良い夢を\",\r\n    \"ach_first_sleep_desc\": \"初めての睡眠から目覚める\",\r\n    \"ach_slept_10_times_name\": \"快眠\",\r\n    \"ach_slept_10_times_desc\": \"10回眠る\",\r\n    \"ach_dream_state_name\": \"深い夢\",\r\n    \"ach_dream_state_desc\": \"レム睡眠に入る\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"生後1時間\",\r\n    \"ach_age_1_hour_desc\": \"1時間生存する\",\r\n    \"ach_age_10_hours_name\": \"成長\",\r\n    \"ach_age_10_hours_desc\": \"10時間生存する\",\r\n    \"ach_age_24_hours_name\": \"一日の奇跡\",\r\n    \"ach_age_24_hours_desc\": \"24時間生存する\",\r\n    \"ach_age_1_week_name\": \"週間ベテラン\",\r\n    \"ach_age_1_week_desc\": \"1週間生存する\",\r\n    \"ach_age_1_month_name\": \"月間ベテラン\",\r\n    \"ach_age_1_month_desc\": \"1ヶ月生存する\",\r\n    \"ach_happiness_100_name\": \"純粋な至福\",\r\n    \"ach_happiness_100_desc\": \"幸福度100%に到達\",\r\n    \"ach_all_stats_high_name\": \"完全な調和\",\r\n    \"ach_all_stats_high_desc\": \"全ステータスが同時に80%を超える\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"最初の一掃き\",\r\n    \"ach_first_clean_desc\": \"初めて水槽を掃除する\",\r\n    \"ach_cleaned_25_times_name\": \"ピカピカの環境\",\r\n    \"ach_cleaned_25_times_desc\": \"25回掃除する\",\r\n    \"ach_germaphobe_name\": \"潔癖症\",\r\n    \"ach_germaphobe_desc\": \"清潔さを1時間以上90%以上に保つ\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"応急処置\",\r\n    \"ach_first_medicine_desc\": \"初めて薬を与える\",\r\n    \"ach_medicine_10_times_name\": \"ドクター・スクイッド\",\r\n    \"ach_medicine_10_times_desc\": \"10回薬を与える\",\r\n    \"ach_comeback_kid_name\": \"起死回生\",\r\n    \"ach_comeback_kid_desc\": \"危機的状況（健康<20%）から全快する\",\r\n\r\n    # Interaction\r\n    \"ach_first_rock_pickup_name\": \"石集め\",\r\n    \"ach_first_rock_pickup_desc\": \"初めて石を拾う\",\r\n    \"ach_rocks_picked_10_name\": \"石収集家\",\r\n    \"ach_rocks_picked_10_desc\": \"石を10個拾う\",\r\n    \"ach_rocks_picked_50_name\": \"岩の貯蔵庫\",\r\n    \"ach_rocks_picked_50_desc\": \"石を50個拾う\",\r\n    \"ach_first_rock_throw_name\": \"水切り\",\r\n    \"ach_first_rock_throw_desc\": \"初めて石を投げる\",\r\n    \"ach_rocks_thrown_25_name\": \"投石機\",\r\n    \"ach_rocks_thrown_25_desc\": \"石を25回投げる\",\r\n    \"ach_rocks_thrown_100_name\": \"投石マスター\",\r\n    \"ach_rocks_thrown_100_desc\": \"石を100回投げる\",\r\n\r\n    # Decor\r\n    \"ach_first_decoration_push_name\": \"インテリアコーディネーター\",\r\n    \"ach_first_decoration_push_desc\": \"初めて装飾を押す\",\r\n    \"ach_decorations_pushed_10_name\": \"模様替え\",\r\n    \"ach_decorations_pushed_10_desc\": \"装飾を10回押す\",\r\n    \"ach_decorations_pushed_50_name\": \"風水マスター\",\r\n    \"ach_decorations_pushed_50_desc\": \"装飾を50回押す\",\r\n    \"ach_first_plant_interact_name\": \"緑の指\",\r\n    \"ach_first_plant_interact_desc\": \"初めて植物に触れる\",\r\n    \"ach_plants_interacted_10_name\": \"庭の探検家\",\r\n    \"ach_plants_interacted_10_desc\": \"植物に10回触れる\",\r\n    \"ach_plants_interacted_50_name\": \"植物学者\",\r\n    \"ach_plants_interacted_50_desc\": \"植物に50回触れる\",\r\n    \"ach_objects_investigated_25_name\": \"好奇心旺盛な検査官\",\r\n    \"ach_objects_investigated_25_desc\": \"25種類の物体を調査する\",\r\n    \"ach_objects_investigated_100_name\": \"名探偵\",\r\n    \"ach_objects_investigated_100_desc\": \"100種類の物体を調査する\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"いたずらっ子\",\r\n    \"ach_first_poop_throw_desc\": \"初めてフンを投げる\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"煙幕\",\r\n    \"ach_first_ink_cloud_desc\": \"初めて墨を吐く\",\r\n    \"ach_ink_clouds_20_name\": \"インクマスター\",\r\n    \"ach_ink_clouds_20_desc\": \"墨を20回吐く\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"最初の記憶\",\r\n    \"ach_first_memory_desc\": \"最初の記憶を形成する\",\r\n    \"ach_memory_long_term_name\": \"長期的思考\",\r\n    \"ach_memory_long_term_desc\": \"記憶を長期記憶に昇格させる\",\r\n    \"ach_memories_50_name\": \"写真的記憶\",\r\n    \"ach_memories_50_desc\": \"50個の記憶を保存する\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"おさるのジョージ\",\r\n    \"ach_curiosity_100_desc\": \"好奇心が100%に到達\",\r\n    \"ach_zen_master_name\": \"禅マスター\",\r\n    \"ach_zen_master_desc\": \"不安を10%以下に30分間保つ\",\r\n    \"ach_first_startle_name\": \"びっくり！\",\r\n    \"ach_first_startle_desc\": \"初めてイカを驚かせる\",\r\n    \"ach_nervous_wreck_name\": \"神経衰弱\",\r\n    \"ach_nervous_wreck_desc\": \"不安が100%に到達\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"夜更かし\",\r\n    \"ach_night_owl_desc\": \"深夜0時から4時の間にプレイ\",\r\n    \"ach_early_bird_name\": \"早起き\",\r\n    \"ach_early_bird_desc\": \"早朝5時から7時の間にプレイ\",\r\n    \"ach_weekend_warrior_name\": \"週末戦士\",\r\n    \"ach_weekend_warrior_desc\": \"土曜と日曜の両方にプレイ\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"脳外科医\",\r\n    \"ach_brain_surgeon_desc\": \"脳の可視化ツールを開く\",\r\n    \"ach_speed_demon_name\": \"スピード狂\",\r\n    \"ach_speed_demon_desc\": \"最高速度で10分間シミュレーションを実行\",\r\n    \"ach_completionist_name\": \"コンプリート\",\r\n    \"ach_completionist_desc\": \"他の実績を30個解除\",\r\n\r\n    # ===== NEURON LABORATORY =====\r\n    \"lab_title\": \"🧠 ニューロン研究所\",\r\n    \"lab_live_refresh\": \"ライブ更新\",\r\n    \"lab_unlock_editing\": \"🔓 編集ロック解除\",\r\n    \"lab_tab_overview\": \"📊 ライブ概要\",\r\n    \"lab_tab_inspector\": \"🔍 詳細インスペクター\",\r\n    \"lab_tab_edit\": \"🔧 編集サンドボックス\",\r\n    \"lab_status_ready\": \"準備完了\",\r\n    \"lab_status_locked\": \"🔒 {name} ロック中 (値: {value})\",\r\n    \"lab_status_unlocked\": \"🔓 {name} ロック解除\",\r\n\r\n    # Overview Tab\r\n    \"lab_ov_counters\": \"カウンター進行状況\",\r\n    \"lab_ov_newest\": \"最新の神経発生\",\r\n    \"lab_ov_limits\": \"制限と剪定\",\r\n    \"lab_ov_actions\": \"クイックアクション\",\r\n    \"lab_force_hebbian\": \"ヘブ学習を強制実行\",\r\n    \"lab_pruning_enabled\": \"剪定有効:\",\r\n    \"lab_none_yet\": \"まだありません\",\r\n    \"lab_ago\": \"{seconds}秒前\",\r\n\r\n    # Inspector Tab\r\n    \"lab_pick_neuron\": \"調査するニューロンを選択:\",\r\n    \"lab_connections_title\": \"結合 (興奮性 vs 抑制性)\",\r\n    \"lab_header_partner\": \"パートナー\",\r\n    \"lab_header_weight\": \"重み\",\r\n    \"lab_header_type\": \"タイプ\",\r\n    \"lab_header_inf\": \"影響力\",\r\n    \"lab_impact_title\": \"機能的影響シミュレーション\",\r\n    \"lab_header_neuron\": \"ニューロン\",\r\n    \"lab_header_delta\": \"Δ値\",\r\n    \"lab_no_connections\": \"現在アクティブな結合はありません\",\r\n    \"lab_did_you_know\": \"豆知識:\",\r\n    \"lab_type_excitatory\": \"興奮性\",\r\n    \"lab_type_inhibitory\": \"抑制性\",\r\n\r\n    # Edit Tab\r\n    \"lab_edit_locked_msg\": \"⚠️ 編集はロックされています - ツールバーの「編集ロック解除」を確認してください。\",\r\n    \"lab_edit_header\": \"ニューロン値 (ドラッグで変更) - 🔒でロック\",\r\n    \"lab_unlock_title\": \"編集を解除しますか？\",\r\n    \"lab_unlock_msg\": \"ニューロンの値を変更したり、生成イベントを強制したりできます。自己責任で使用してください！\",\r\n\r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"極小\",\r\n    \"lab_inf_mild\": \"軽微\",\r\n    \"lab_inf_mod\": \"中程度\",\r\n    \"lab_inf_strong\": \"強力\",\r\n\r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"空腹は恒常的な欲求です。高い空腹度は満足感を抑制し、不安を高めます。\",\r\n    \"lab_tip_happiness\": \"幸福感は報酬ニューロンによって強化されます。不安を抑制し、好奇心を促進します。\",\r\n    \"lab_tip_anxiety\": \"不安はストレスニューロン（抑制性）によって減少します。高い不安は好奇心を抑制します。\",\r\n    \"lab_tip_curiosity\": \"好奇心は新規性が高いときに急上昇します。探索を促し、不安を軽減します。\",\r\n    \"lab_tip_core\": \"コアニューロン - 生存に不可欠です。\",\r\n    \"lab_tip_neuro_default\": \"神経発生ニューロン - 生成時の状況から目的が推測されます。\",\r\n    \"lab_tip_neuro_fmt\": \"<b>{trigger}</b>により生成 – 専門: <b>{spec}</b>。経験を長期的な行動に変換します。\",\r\n\r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"イカの視界\",\r\n    \"vis_logic_unavailable\": \"ロジック利用不可\",\r\n    \"vis_nothing_in_view\": \"視界に何もありません。\",\r\n    \"vis_distance\": \"距離\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"専門\",\r\n    \"tooltip_type\": \"タイプ\",\r\n    \"tooltip_current\": \"現在値\",\r\n    \"tooltip_utility\": \"有用性\",\r\n    \"tooltip_activations\": \"活性化数\",\r\n    \"tooltip_last_active\": \"最終活動\",\r\n    \"tooltip_age\": \"年齢\",\r\n    \"tooltip_core\": \"コア\",\r\n    \"tooltip_generated\": \"生成済\",\r\n    \"tooltip_functional\": \"機能的\",\r\n    \"tooltip_connections_header\": \"結合\",\r\n    \"tooltip_connections_stats\": \"入力 {incoming}, 出力 {outgoing}\",\r\n    \"tooltip_top_incoming\": \"主な入力\",\r\n    \"tooltip_top_outgoing\": \"主な出力\",\r\n    \"tooltip_hint\": \"ダブルクリックで詳細 • 右クリックでオプション\",\r\n\r\n    # State values\r\n    \"state_on\": \"オン\",\r\n    \"state_off\": \"オフ\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"{val}秒前\",\r\n    \"fmt_m_ago\": \"{val}分前\",\r\n    \"fmt_h_ago\": \"{val}時間前\",\r\n    \"fmt_s_short\": \"{val}秒\",\r\n    \"fmt_m_short\": \"{val}分\",\r\n    \"fmt_h_short\": \"{val}時間\",\r\n    \"fmt_d_short\": \"{val}日\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW UI =====\r\n    \"designer_window_title\": \"脳デザイナー - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"脳デザイナー - Dosidicus-2 [ゲームからインポート]\",\r\n    \r\n    # Tabs\r\n    \"designer_tab_layers\": \"レイヤー\",\r\n    \"designer_tab_sensors\": \"センサー\",\r\n    \"designer_tab_props\": \"プロパティ\",\r\n    \"designer_tab_connections\": \"結合\",\r\n    \"designer_tab_outputs\": \"出力\",\r\n    \r\n    # Toolbar\r\n    \"designer_btn_generate\": \"🎲 疎なネットワークを生成\",\r\n    \"designer_tooltip_generate\": \"コアニューロン間にランダムな結合を生成\",\r\n    \"designer_btn_neuron\": \"➕ ニューロン\",\r\n    \"designer_tooltip_neuron\": \"新しいニューロンを追加 (Shift+N)\",\r\n    \"designer_btn_fix\": \"🔧 自動修正\",\r\n    \"designer_tooltip_fix\": \"孤立したニューロンと接続の問題を自動修正\",\r\n    \"designer_btn_validate\": \"✓ 検証\",\r\n    \"designer_tooltip_validate\": \"デザインの問題をチェック\",\r\n    \"designer_btn_sync\": \"🔄 ゲームから同期\",\r\n    \"designer_tooltip_sync\": \"実行中のゲームから脳の状態を更新\",\r\n    \"designer_btn_clear_conn\": \"🗑 結合をクリア\",\r\n    \"designer_tooltip_clear_conn\": \"すべての結合を削除（ニューロンは保持）\",\r\n    \"designer_tooltip_dice\": \"即座にランダムなネットワークを生成（ダイアログなし）\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 ニューロンから<b>左ドラッグ</b>で結合を作成\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+ドラッグ</b>でニューロンを移動\",\r\n    \"designer_help_pan\": \"<b>右ドラッグ</b>でキャンバスを移動\",\r\n    \"designer_help_zoom\": \"<b>ホイール</b>でズーム（または結合の重みを調整）\",\r\n    \"designer_help_edit_weight\": \"結合を<b>ダブルクリック</b>で重みを編集\",\r\n    \"designer_help_select\": \"<b>クリック</b>で選択\",\r\n    \"designer_help_delete\": \"<b>Del</b>で選択項目を削除\",\r\n    \"designer_help_reverse\": \"<b>スペース</b>で結合の向きを反転\",\r\n    \"designer_help_keys_weight\": \"<b>+/-</b>キーで重みを調整 (Shiftで大きく)\",\r\n    \"designer_help_page_weight\": \"<b>Page Up/Down</b>で重みを調整（大きく）\",\r\n    \"designer_help_add_neuron\": \"<b>Shift+N</b>でニューロン追加\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b>で保存\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b>で開く\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b>でエクスポート\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b>で新規作成\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b>でネットワーク生成\",\r\n    \"designer_help_dice\": \"🎲 <b>サイコロ</b>で即時ランダム生成\",\r\n    \"designer_help_outputs\": \"<b>出力タブ</b>で行動とバインド\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"ファイル\",\r\n    \"designer_menu_edit\": \"編集\",\r\n    \"designer_menu_templates\": \"テンプレート\",\r\n    \"designer_menu_generate\": \"生成\",\r\n    \r\n    # Actions\r\n    \"designer_action_new\": \"新規デザイン\",\r\n    \"designer_action_save\": \"保存...\",\r\n    \"designer_action_export\": \"Dosidicus用にエクスポート...\",\r\n    \"designer_action_open\": \"開く...\",\r\n    \"designer_action_gen_sparse\": \"疎なネットワークを生成...\",\r\n    \"designer_action_autofix\": \"接続を自動修正\",\r\n    \"designer_action_validate\": \"デザインを検証\",\r\n    \"designer_action_clear_conn\": \"全ての結合をクリア\",\r\n    \"designer_action_clear_outputs\": \"全ての出力バインドをクリア\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"ニューロン: {count}\",\r\n    \"designer_status_connections\": \"結合: {count}\",\r\n    \"designer_status_required\": \"必須: {ok}\",\r\n    \"designer_status_outputs\": \"出力: {count}\",\r\n    \"designer_status_selected\": \"選択: {source} → {target} (重み: {weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"重み更新: {source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"結合削除: {source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"{count}個の結合をクリア\",\r\n    \"designer_status_cleared_out\": \"{count}個の出力バインドをクリア\",\r\n    \"designer_status_generated\": \"'{style}'プリセットで{count}個の結合を生成\",\r\n    \"designer_status_random_gen\": \"🎲 {count}個のランダム結合を生成 (スタイル: {style})\",\r\n    \"designer_status_synced\": \"✨ 同期完了: ニューロン{neurons}, 結合{connections}\",\r\n    \"designer_status_imported\": \"✨ 実行中のゲームから脳をインポートしました\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"ゲームが実行されていません\",\r\n    \"designer_msg_game_not_running\": \"Dosidicusゲームが実行されていません。\\n\\n同期するにはゲームを起動してください。\",\r\n    \"designer_msg_sync_confirm_title\": \"ゲームから同期\",\r\n    \"designer_msg_sync_confirm\": \"現在のデザインをゲーム内の最新の脳状態で置き換えますか？\",\r\n    \"designer_msg_sync_failed_title\": \"同期失敗\",\r\n    \"designer_msg_sync_failed\": \"ゲームから脳の状態をインポートできませんでした。\",\r\n    \r\n    \"designer_msg_live_import_title\": \"ライブ脳インポート\",\r\n    \"designer_msg_live_import_header\": \"🧠 実行中のゲームから脳をインポート\",\r\n    \"designer_msg_live_import_body\": \"デザイナーは現在、実行中のDosidicusゲームのニューラルネットワークを表示しています。\\n\\n• {neurons} ニューロン\\n• {connections} 結合\\n\\nここでの変更は実行中のゲームには影響しません。\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"結合のクリア\",\r\n    \"designer_msg_clear_conn_confirm\": \"{count}個の結合をすべて削除しますか？\\n\\nニューロンは保持されます。\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"出力バインドのクリア\",\r\n    \"designer_msg_clear_out_empty\": \"クリアする出力バインドがありません。\",\r\n    \"designer_msg_clear_out_confirm\": \"{count}個の出力バインドをすべて削除しますか？\",\r\n    \r\n    \"designer_msg_new_design_title\": \"新規デザイン\",\r\n    \"designer_msg_new_design_confirm\": \"新しいデザインを開始しますか？保存されていない変更は失われます。\",\r\n    \r\n    \"designer_msg_autofix_title\": \"自動修正\",\r\n    \"designer_msg_autofix_result\": \"{count}個の結合を作成しました:\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"問題は見つかりませんでした。\",\r\n    \r\n    \"designer_msg_save_title\": \"デザインを保存\",\r\n    \"designer_msg_saved_title\": \"保存完了\",\r\n    \"designer_msg_save_success\": \"デザインを保存しました: {msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n({count}個の出力バインドを含む)\",\r\n    \"designer_msg_error_title\": \"エラー\",\r\n    \"designer_msg_save_fail\": \"保存に失敗しました:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"エクスポート\",\r\n    \"designer_msg_exported_title\": \"エクスポート完了\",\r\n    \"designer_msg_export_success\": \"デザインを正常にエクスポートしました\",\r\n    \"designer_msg_export_fail\": \"エクスポートに失敗しました:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"デザインを開く\",\r\n    \"designer_msg_open_fail\": \"読み込めませんでした:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"テンプレート読み込み\",\r\n    \"designer_msg_select_template\": \"テンプレートを選択:\",\r\n    \"designer_msg_replace_design\": \"現在のデザインを置き換えますか？\",\r\n    \r\n    \"designer_msg_status_title\": \"デザインステータス\",\r\n    \"designer_msg_status_ok\": \"\\n✅ ステータス: OK\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ 問題:\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"結合の重み\",\r\n    \"designer_input_weight_label\": \"{source} → {target} の重みを設定:\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"ニューロン未選択\",\r\n    \"designer_prop_no_selection_disabled\": \"選択なし\",\r\n    \"designer_prop_lbl_name\": \"名前:\",\r\n    \"designer_prop_lbl_type\": \"タイプ:\",\r\n    \"designer_prop_lbl_x\": \"X:\",\r\n    \"designer_prop_lbl_y\": \"Y:\",\r\n    \"designer_prop_btn_delete\": \"ニューロン削除\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"ニューロン追加\",\r\n    \"designer_add_grp_type\": \"ニューロンタイプを選択\",\r\n    \"designer_add_btn_custom\": \"✨ カスタム / プラグイン\",\r\n    \"designer_add_btn_sensor\": \"📡 入力センサー\",\r\n    \"designer_add_tooltip_custom\": \"プラグインとリンクするための特定の名前を持つニューロンを作成\",\r\n    \"designer_add_grp_sensor\": \"センサーを選択\",\r\n    \"designer_add_grp_custom\": \"カスタムニューロン定義\",\r\n    \"designer_add_info_custom\": \"<i>イカに影響を与えるには、<b>名前</b>がプラグインIDと一致する必要があります。<br>例: <b>'jet_boost'</b>と名付けてジェットパックプラグインを起動。</i>\",\r\n    \"designer_add_lbl_id\": \"プラグインID / 名前:\",\r\n    \"designer_add_ph_id\": \"例: turbo_mode\",\r\n    \"designer_add_btn_create\": \"リンク作成\",\r\n    \"designer_add_all_added\": \"全センサー追加済み\",\r\n    \"designer_add_err_title\": \"エラー\",\r\n    \"designer_add_err_exists\": \"既に存在します\",\r\n    \"designer_add_msg_created\": \"{name} を作成しました\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"層を追加\",\r\n    \"designer_layer_dlg_title\": \"新しい層\",\r\n    \"designer_layer_dlg_label\": \"名前:\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"入力センサー:\",\r\n    \"designer_sensor_tooltip_refresh\": \"センサーリスト更新 (プラグインを含む)\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"ソース\",\r\n    \"designer_conn_header_target\": \"ターゲット\",\r\n    \"designer_conn_header_weight\": \"重み\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>出力バインド</b><br><small>ニューロンを行動に接続します。活性化が閾値を超えると行動がトリガーされます。</small>\",\r\n    \"designer_output_btn_add\": \"➕ バインド追加\",\r\n    \"designer_output_btn_edit\": \"✏️ 編集\",\r\n    \"designer_output_btn_remove\": \"🗑️ 削除\",\r\n    \"designer_output_col_neuron\": \"ニューロン\",\r\n    \"designer_output_col_behavior\": \"→ 行動\",\r\n    \"designer_output_col_threshold\": \"閾値\",\r\n    \"designer_output_col_mode\": \"モード\",\r\n    \"designer_output_col_enabled\": \"有効\",\r\n    \"designer_output_info\": \"{count}バインド, {enabled}有効\",\r\n    \"designer_output_err_missing\": \"⚠️ ニューロンが見つかりません\",\r\n    \"designer_output_dlg_remove_title\": \"バインド削除\",\r\n    \"designer_output_dlg_remove_msg\": \"バインドを削除しますか: {neuron} → {hook}?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"出力バインド追加\",\r\n    \"designer_binding_title_edit\": \"出力バインド設定\",\r\n    \"designer_binding_grp_neuron\": \"ソースニューロン\",\r\n    \"designer_binding_lbl_neuron\": \"ニューロン:\",\r\n    \"designer_binding_lbl_current\": \"現在: --\",\r\n    \"designer_binding_grp_hook\": \"出力行動\",\r\n    \"designer_binding_lbl_trigger\": \"トリガー:\",\r\n    \"designer_binding_grp_settings\": \"トリガー設定\",\r\n    \"designer_binding_lbl_thresh\": \"閾値:\",\r\n    \"designer_binding_lbl_mode\": \"モード:\",\r\n    \"designer_binding_lbl_cool\": \"クールダウン:\",\r\n    \"designer_binding_chk_enabled\": \"有効\",\r\n    \"designer_binding_err_neuron\": \"ニューロンを選択してください\",\r\n    \"designer_binding_err_hook\": \"出力行動を選択してください\",\r\n    \"designer_binding_err_duplicate\": \"{neuron} → {hook} のバインドは既に存在します\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"立ち上がり (閾値を超えた瞬間)\",\r\n    \"designer_mode_falling\": \"立ち下がり (閾値を下回った瞬間)\",\r\n    \"designer_mode_above\": \"閾値以上 (継続)\",\r\n    \"designer_mode_below\": \"閾値以下 (継続)\",\r\n    \"designer_mode_change\": \"変化時 (有意な変化)\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"内蔵センサー: {name}\",\r\n    \"desc_vision_food\": \"視界内の餌を検出\",\r\n    \"desc_custom_sensor\": \"{plugin}からのカスタムセンサー\",\r\n    \"desc_builtin\": \"内蔵\",\r\n    \"desc_plugin\": \"プラグイン\",\r\n    \"desc_other\": \"その他\",\r\n    \"desc_vision\": \"視覚\",\r\n    \r\n    # ===== DESIGNER TEMPLATES (Extra Keys) =====\r\n    \"tmpl_core_name\": \"🟡 必須のみ\",\r\n    \"tmpl_core_desc\": \"必須8ニューロン\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Dosidicus デフォルト\",\r\n    \"tmpl_dosidicus_desc\": \"標準レイアウト\",\r\n    \"tmpl_full_sensors_name\": \"🟡 全センサー搭載\",\r\n    \"tmpl_full_sensors_desc\": \"全てのセンサー\",\r\n    \"tmpl_insomniac_name\": \"🔴 不眠症\",\r\n    \"tmpl_insomniac_desc\": \"不安と好奇心が睡眠を阻害\",\r\n    \"tmpl_hyperactive_name\": \"🔴 多動性\",\r\n    \"tmpl_hyperactive_desc\": \"ノイズが眠気を圧倒\",\r\n    \"tmpl_hangry_name\": \"🔴 ハングリー・怒り\",\r\n    \"tmpl_hangry_desc\": \"空腹が激しい怒りを引き起こす\",\r\n    \"tmpl_depressive_name\": \"🔴 抑うつ\",\r\n    \"tmpl_depressive_desc\": \"幸福感に抵抗\",\r\n    \"tmpl_obsessive_name\": \"🔴 強迫性\",\r\n    \"tmpl_obsessive_desc\": \"不安/好奇心のフィードバックループ\",\r\n    \"layer_sensors\": \"センサー\",\r\n    \"layer_core\": \"コア\",\r\n    \"layer_input\": \"入力\",\r\n    \"layer_out\": \"出力\",\r\n    \"layer_racing_mind\": \"思考の暴走\",\r\n    \"layer_state\": \"状態\",\r\n    \"layer_vision\": \"視覚\",\r\n    \"layer_noise\": \"ノイズ\",\r\n    \"layer_output\": \"出力\",\r\n    \"layer_gut_brain\": \"脳腸相関\",\r\n    \"layer_gray\": \"灰白質\",\r\n    \"layer_loop\": \"ループ\",\r\n    \"layer_stats\": \"統計\",\r\n    \"layer_emotions\": \"感情\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"結合削除\",\r\n    \"designer_cnv_del_conn_msg\": \"次の結合を削除してもよろしいですか？\\n{source} → {target}\",\r\n    \"designer_cnv_chk_dont_ask\": \"今後確認しない\",\r\n    \"designer_cnv_btn_del\": \"はい、削除\",\r\n    \"designer_cnv_btn_cancel\": \"キャンセル\",\r\n    \"designer_cnv_dlg_edit_title\": \"結合の編集\",\r\n    \"designer_cnv_lbl_conn\": \"結合: {source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"重み:\",\r\n    \"designer_cnv_info_weight\": \"正 = 興奮性 (緑), 負 = 抑制性 (赤)\",\r\n    \"designer_cnv_btn_del_conn\": \"結合を削除\",\r\n    \"designer_cnv_btn_ok\": \"OK\",\r\n    \"designer_cnv_tooltip_invalid\": \"無効な結合\",\r\n}"
  },
  {
    "path": "translations/ml.py",
    "content": "LANGUAGE_HEADER = \"ml - Millennial\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"Hangry Level\",\r\n    \"happiness\": \"Vibes\",\r\n    \"cleanliness\": \"Aesthetic\",\r\n    \"sleepiness\": \"Nap Cravings\",\r\n    \"satisfaction\": \"Dopamine\",\r\n    \"anxiety\": \"Existential Dread\",\r\n    \"curiosity\": \"FOMO\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"Spotted Snacks\",\r\n    \"is_eating\": \"Nomming\",\r\n    \"is_sleeping\": \"Zonked Out\",\r\n    \"is_sick\": \"Not A Vibe\",\r\n    \"pursuing_food\": \"Chasing Snaccs\",\r\n    \"is_startled\": \"Shook\",\r\n    \"is_fleeing\": \"Nope-ing Out\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"New Shiny Thing\",\r\n    \"stress\": \"Panic\",\r\n    \"reward\": \"Treat Yo Self\",\r\n\r\n    # ===== MAIN MENU =====\r\n    \"file\": \"Stuff\",\r\n    \"new_game\": \"New Era\",\r\n    \"load_game\": \"Flashback\",\r\n    \"save_game\": \"Save Receipts\",\r\n    \"view\": \"Peep\",\r\n    \"speed\": \"Pacing\",\r\n    \"pause\": \"Hol' Up\",\r\n    \"actions\": \"Do Things\",\r\n    \"debug\": \"Hacker Mode\",\r\n    \"plugins\": \"Mods\",\r\n\r\n    # ===== VIEW MENU =====\r\n    \"brain_designer\": \"Big Brain Energy\",\r\n    \"decorations\": \"Room Makeover\",\r\n    \"statistics\": \"The Receipts\",\r\n    \"brain_tool\": \"Brain Cell Inspector\",\r\n    \"neuron_lab\": \"The Lab\",\r\n    \"task_manager\": \"Adulting Manager\",\r\n\r\n    # ===== SPEED MENU =====\r\n    \"normal_speed\": \"Chill (1x)\",\r\n    \"fast_speed\": \"Zoomies (2x)\",\r\n    \"very_fast\": \"Ludicrous Speed (3x)\",\r\n\r\n    # ===== DEBUG MENU =====\r\n    \"toggle_debug\": \"Toggle Matrix Mode\",\r\n    \"toggle_cone\": \"Show Eye Cone\",\r\n    \"squid_vision\": \"POV Mode\",\r\n\r\n    # ===== ACTION BUTTONS =====\r\n    \"feed\": \"Noms\",\r\n    \"clean\": \"Tidy\",\r\n    \"medicine\": \"Meds\",\r\n    \"feed_btn\": \"FEED ME\",\r\n    \"clean_btn\": \"CLEAN UP\",\r\n    \"medicine_btn\": \"SELF CARE\",\r\n\r\n    # ===== MESSAGES =====\r\n    \"feed_msg\": \"Bestie is starving\",\r\n    \"points\": \"Clout\",\r\n    \"dirty\": \"GROSS\",\r\n    \"paused_msg\": \"VIBE CHECK PAUSED\",\r\n    \"paused_sub\": \"Hit the Speed menu to unfreeze time\",\r\n\r\n    # ===== DIALOGS =====\r\n    \"yes\": \"Yasss\",\r\n    \"no\": \"Nah\",\r\n    \"ok\": \"Kk\",\r\n    \"cancel\": \"Nevermind\",\r\n    \"close\": \"Bye\",\r\n    \"save\": \"Stash It\",\r\n    \"load\": \"Bring It Back\",\r\n    \"reset\": \"Rage Quit\",\r\n    \"apply_changes\": \"Make It Happen\",\r\n    \"got_it\": \"Understood the Assignment\",\r\n    \"finish\": \"Donezo\",\r\n    \"confirm_new_game\": \"Start fresh? You'll lose all your progress rn.\",\r\n    \"confirm_exit\": \"You tryin' to leave?\",\r\n    \"save_successful\": \"Saved to the cloud (jk, local disk).\",\r\n    \"load_successful\": \"We back in business!\",\r\n    \"error_saving\": \"Epic fail saving game.\",\r\n    \"error_loading\": \"Epic fail loading game.\",\r\n    \"no_save_found\": \"No receipts found.\",\r\n    \"startup\": \"Waking Up\",\r\n    \"show_tutorial_q\": \"Need a guide?\",\r\n    \"auto_decline\": \"(Ghosting in {seconds}s)\",\r\n    \"tutorial_title\": \"The 101\",\r\n    \"tutorial_query\": \"Do you need me to explain how this works?\",\r\n\r\n    # ===== ABOUT TAB =====\r\n    \"hello\": \"SUP\",\r\n    \"my_name_is\": \"call me\",\r\n    \"change_name\": \"Rebrand\",\r\n    \"enter_new_name\": \"New handle for your squid:\",\r\n    \"change_colour\": \"Glow Up\",\r\n    \"view_certificate\": \"Flex Certificate\",\r\n    \"care_tips\": \"Life Hacks\",\r\n    \"care_tips_for\": \"Life Hacks for {personality} Squids\",\r\n    \"dosidicus_title\": \"Dosidicus electronicae\",\r\n    \"dosidicus_desc\": \"Basically a Tamagotchi but with actual AI anxiety.\",\r\n    \"string_acronym\": \"Simulated Tamagotchi Reactions via Inferencing and Neurogenesis (STRINg)\",\r\n    \"research_project\": \"It's for science. Slide into the DMs with feature requests.\",\r\n    \"version_dosidicus\": \"Build:\",\r\n    \"version_brain_tool\": \"Brain Tool:\",\r\n    \"version_decision\": \"Choice Engine:\",\r\n    \"version_neuro\": \"Growth Engine:\",\r\n    \"created_by\": \"built by\",\r\n\r\n    # ===== PERSONALITY =====\r\n    \"squid_personality\": \"Squid Vibe\",\r\n    \"personality_modifier\": \"Vibe Check\",\r\n    \"description\": \"The Tea:\",\r\n    \"personality_modifiers\": \"Stat Buffs/Debuffs:\",\r\n    \"care_tips_label\": \"Pro Tips:\",\r\n    \"personality_note\": \"Note: Personality is RNG at the start.\",\r\n\r\n    # Personality Types\r\n    \"personality_timid\": \"Smol Bean\",\r\n    \"personality_adventurous\": \"Wanderlust\",\r\n    \"personality_lazy\": \"Potato\",\r\n    \"personality_energetic\": \"No Chill\",\r\n    \"personality_introvert\": \"Loner\",\r\n    \"personality_greedy\": \"Boujee\",\r\n    \"personality_stubborn\": \"Karen\",\r\n\r\n    # Personality Descriptions\r\n    \"desc_timid\": \"Your squid is a Smol Bean. Literally terrified of everything. Needs a safe space and good vibes only. 10/10 would protect.\",\r\n    \"desc_adventurous\": \"Your squid has Wanderlust. Wants to travel the world (or the tank). Gets bored if things aren't lit. Total main character energy.\",\r\n    \"desc_lazy\": \"Your squid is a Potato. Mood: doing absolutely nothing. Energy saving mode is always on. Relatable content.\",\r\n    \"desc_energetic\": \"Your squid has No Chill. Always zooming. Needs constant entertainment or will destroy things. Probably drinks too much coffee.\",\r\n    \"desc_introvert\": \"Your squid is a Loner. 'Social battery drained' is their permanent state. Likes watching from the corner. Weird but valid.\",\r\n    \"desc_greedy\": \"Your squid is Boujee. Wants all the snacks. High maintenance. Treat them like royalty or get roasted.\",\r\n    \"desc_stubborn\": \"Your squid is a Karen. Wants to speak to the manager. Hates change. Will only eat specific sushi. Good luck with this one.\",\r\n\r\n    # Personality Short Modifiers\r\n    \"mod_timid\": \"High anxiety, needs hugs\",\r\n    \"mod_adventurous\": \"Exploring > Everything\",\r\n    \"mod_lazy\": \"Slow moving, stays in bed\",\r\n    \"mod_energetic\": \"Fast af boi\",\r\n    \"mod_introvert\": \"Hates crowds, loves quiet\",\r\n    \"mod_greedy\": \"Hangry all the time\",\r\n    \"mod_stubborn\": \"Picky eater, refuses to sleep\",\r\n\r\n    # Personality Modifier Details\r\n    \"modifiers_timid\": \"- Dread spikes 50% faster\\n- FOMO drops 50% slower\\n- Plants make everything better\",\r\n    \"modifiers_adventurous\": \"- FOMO spikes 50% faster\",\r\n    \"modifiers_lazy\": \"- Slow motion\\n- Burns barely any calories\",\r\n    \"modifiers_energetic\": \"- Zoomies enabled\\n- Burns calories like crazy\",\r\n    \"modifiers_introvert\": \"- Needs space\\n- Social battery drains fast\",\r\n    \"modifiers_greedy\": \"- Gets super Hangry\\n- Food hits different (more dopamine)\",\r\n    \"modifiers_stubborn\": \"- Sushi or starve\\n- Insomniac tendencies\",\r\n\r\n    # Care Tips\r\n    \"tips_timid\": \"- Add plants for zen vibes\\n- Keep it chill, no loud noises\\n- Don't resize the window too fast, it's scary\\n- Routine is key\",\r\n    \"tips_adventurous\": \"- Change up the decor often\\n- Hide snacks to make them hunt\\n- Give them room to roam\\n- Don't let them get bored\",\r\n    \"tips_lazy\": \"- Put food right next to their face\\n- Keep the place tidy\\n- Don't expect them to do cardio\\n- Comfy spots are a must\",\r\n    \"tips_energetic\": \"- Needs a big tank for activities\\n- Feed often, they burn through it\\n- Use interactive toys\\n- They will bounce off the walls\",\r\n    \"tips_introvert\": \"- Make hiding spots\\n- Don't crowd them with junk\\n- Let them vibe alone\\n- Plants = privacy\",\r\n    \"tips_greedy\": \"- Sushi is the way to their heart\\n- Bribe them with food\\n- Watch the weight gain\\n- Let them hoard stuff\",\r\n    \"tips_stubborn\": \"- Stock up on sushi\\n- Be patient, they're difficult\\n- Good luck getting them to sleep\\n- Don't force it\",\r\n\r\n    # ===== DECISIONS TAB =====\r\n    \"thought_process\": \"Squid's Internal Monologue\",\r\n    \"step\": \"Step\",\r\n    \"step1_title\": \"Reading the Room\",\r\n    \"step2_title\": \"Checking the Vibe\",\r\n    \"step3_title\": \"Consulting the Trauma\",\r\n    \"step4_title\": \"Sending It\",\r\n    \"final_action\": \"The Move:\",\r\n    \"awaiting_thought\": \"Head empty, no thoughts...\",\r\n    \"awaiting_decision\": \"Buffering...\",\r\n    \"sensing_condition\": \"Squid is checking the scene:\",\r\n    \"visible_objects\": \"What's visible\",\r\n    \"no_sensory_data\": \"Blind as a bat rn.\",\r\n    \"none\": \"Nada\",\r\n    \"no_urges\": \"Zero motivation.\",\r\n    \"strongest_urge\": \"The biggest mood right now is\",\r\n    \"initial_scores\": \"Base stats:\",\r\n    \"personality_memory_adjust\": \"Personality and PTSD adjusting the score:\",\r\n    \"no_adjustments\": \"Brain is basic, no adjustments.\",\r\n    \"final_scores_text\": \"Final tally. Winner takes all.\",\r\n    \"no_final_scores\": \"No scores, head empty.\",\r\n    \"squid_decided\": \"Squid decided to\",\r\n    \"with_confidence\": \"with a certainty of\",\r\n    \"score_increased\": \"hyped up\",\r\n    \"score_decreased\": \"nerfed\",\r\n    \"score_for\": \"The hype for\",\r\n    \"by_amount\": \"by\",\r\n\r\n    # ===== LEARNING TAB (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"Active Collabs\",\r\n    \"hebbian_cycle\": \"Hebb Cycle\",\r\n    \"hebbian_paused\": \"ON HOLD\",\r\n    \"learning_ready\": \"Brain is Ready\",\r\n    \"learning_ready_desc\": \"We wiring things together fam.\",\r\n    \"log_cleared\": \"Log Yeeted\",\r\n    \"log_cleared_desc\": \"New connections will drop here.\",\r\n    \"hebbian_overview\": \"The Science Bit\",\r\n    \"neurons_fire_together\": \"Neurons that vibe together, wire together\",\r\n    \"hebbian_principle\": \"Basically how we learn stuff.\",\r\n    \"hebbian_explanation\": \"If two brain cells light up at the same party, they become besties. If they never hang out, they unfollow each other.\",\r\n    \"excitatory_connections\": \"Hype Squad (Excitatory)\",\r\n    \"excitatory_desc\": \"Positive vibes make neurons activate together\",\r\n    \"inhibitory_connections\": \"Haters (Inhibitory)\",\r\n    \"inhibitory_desc\": \"Negative vibes shut that down\",\r\n    \"very_strong\": \"Ride or Die\",\r\n    \"strong\": \"Solid\",\r\n    \"moderate\": \"Casual\",\r\n    \"weak\": \"Meh\",\r\n    \"very_weak\": \"Barely There\",\r\n    \"inhibited\": \"Blocked\",\r\n\r\n    # ===== MEMORY TAB =====\r\n    \"memory\": \"Core Memories\",\r\n    \"memories\": \"The Vault\",\r\n    \"short_term_memory\": \"Recent Tea\",\r\n    \"long_term_memory\": \"Core Memories\",\r\n    \"no_memories\": \"Clean slate.\",\r\n    \"overview\": \"TL;DR\",\r\n    \"memory_stats\": \"Brain Stats\",\r\n    \"categories\": \"Folders\",\r\n    \"time_label\": \"Timestamp:\",\r\n    \"important_label\": \"Core Memory\",\r\n    \"unknown\": \"???\",\r\n    \"category_label\": \"Tag:\",\r\n    \"key_label\": \"Key:\",\r\n    \"access_count\": \"Views:\",\r\n    \"full_content\": \"Full Deets:\",\r\n    \"effects_label\": \"Impact:\",\r\n    \"positive\": \"W\",\r\n    \"negative\": \"L\",\r\n    \"neutral\": \"Mid\",\r\n\r\n    # ===== NETWORK TAB (ORIGINAL) =====\r\n    \"brain_network\": \"The Wiring\",\r\n    \"neurons\": \"Brain Cells\",\r\n    \"connections\": \"Links\",\r\n    \"activity\": \"Noise\",\r\n\r\n    # ===== STATISTICS WINDOW =====\r\n    \"status\": \"Vibe Check\",\r\n    \"health\": \"HP\",\r\n\r\n    # ===== NEURON NAMES (NEW) =====\r\n    \"hunger\": \"Hangry\",\r\n    \"happiness\": \"Serotonin\",\r\n    \"cleanliness\": \"Clean\",\r\n    \"sleepiness\": \"Tired\",\r\n    \"satisfaction\": \"Satisfied\",\r\n    \"curiosity\": \"Nosy\",\r\n    \"anxiety\": \"Panic\",\r\n    \"can_see_food\": \"Food Spotted\",\r\n    \"is_eating\": \"Munching\",\r\n    \"is_sleeping\": \"Snoozing\",\r\n    \"is_sick\": \"Unwell\",\r\n    \"is_fleeing\": \"Running Away\",\r\n    \"is_startled\": \"Spooked\",\r\n    \"pursuing_food\": \"Hunting\",\r\n    \"external_stimulus\": \"Stimulus\",\r\n    \"plant_proximity\": \"Near Plant\",\r\n    \"stress\": \"Stress\",\r\n    \"novelty\": \"New Stuff\",\r\n    \"reward\": \"Treat\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS (NEW) =====\r\n    \"layer_name\": \"Layer\",\r\n    \"layer_input\": \"Input\",\r\n    \"layer_output\": \"Output\",\r\n    \"layer_hidden\": \"Hidden\",\r\n\r\n    # ===== NEUROGENESIS LOGS (NEW) =====\r\n    \"log_created\": \"{time} - spawned a {type} neuron ({name}) cuz {type} hit {value:.2f}\",\r\n    \"log_pruned\": \"{time} - deleted neuron ({name}) cuz {reason}\",\r\n    \"log_stress_detail\": \"Made a hater connection to ANXIETY. Max panic permanently nerfed by 10.\",\r\n\r\n    # State Pills\r\n    \"fleeing\": \"Runing Away!\",\r\n    \"startled\": \"Spooked!\",\r\n    \"eating\": \"Munching\",\r\n    \"sleeping\": \"Zzz\",\r\n    \"playing\": \"Gaming\",\r\n    \"hiding\": \"Lurking\",\r\n    \"anxious\": \"Panicking\",\r\n    \"curious\": \"Investigating\",\r\n\r\n    # ===== COMMON ACTIONS =====\r\n    \"eat\": \"Consume\",\r\n    \"sleep\": \"Power Nap\",\r\n    \"play\": \"Mess Around\",\r\n    \"explore\": \"Wander\",\r\n    \"rest\": \"Chill\",\r\n    \"hide\": \"Hide\",\r\n    \"wander\": \"Roam\",\r\n    \"idle\": \"AFK\",\r\n    \"seek_food\": \"Find Snacks\",\r\n    \"seek_shelter\": \"Find Safe Spot\",\r\n\r\n    # ===== OBJECTS =====\r\n    \"food\": \"Snack\",\r\n    \"rock\": \"Rock\",\r\n    \"poop\": \"Trash\",\r\n    \"plant\": \"Fern\",\r\n    \"sushi\": \"Sushi\",\r\n    \"decoration\": \"Decor\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"A squid just dropped! You gotta take care of it.\",\r\n    \"tutorial_feed\": \"Feed it when it's hangry (Actions Menu)\",\r\n    \"tutorial_clean\": \"Clean the tank when it's gross\",\r\n    \"tutorial_watch\": \"Watch it to figure out its zodiac sign (personality)\",\r\n    \"tutorial_neural\": \"THE BIG BRAIN\",\r\n    \"tutorial_neural_desc\": \"This is the neural net. It drives behavior based on needs.\\nIt learns from your parenting fails.\",\r\n    \"tutorial_satisfaction\": \"Keep dopamine high and panic low.\",\r\n    \"tutorial_traits\": \"Your squid will get weird traits based on how you raise it.\",\r\n\r\n    # ===== BRAIN DESIGNER (Templates) =====\r\n    \"designer_title\": \"Brain Architect\",\r\n    \"required_only\": \"Bare Minimum\",\r\n    \"dosidicus_default\": \"Factory Settings\",\r\n    \"full_sensors\": \"All The Sensors\",\r\n    \"the_insomniac\": \"Team No Sleep\",\r\n    \"the_hyperactive\": \"Squirrel Mode\",\r\n    \"the_hangry\": \"Perma-Hangry\",\r\n    \"the_depressive\": \"Sad Boi Hour\",\r\n    \"the_obsessive\": \"The Stan\",\r\n    \"balanced\": \"Zen\",\r\n    \"minimal\": \"Potato Mode\",\r\n    \"dense\": \"Big Brain\",\r\n    \"chaotic\": \"Chaos Emerald\",\r\n    \"calm\": \"Lo-fi Beats\",\r\n\r\n    # ===== SPLASH SCREEN =====\r\n    \"squid_hatched\": \"IT'S ALIVE!\",\r\n    \"look_after\": \"DON'T MESS THIS UP..\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"Learning\",\r\n    \"tab_decisions\": \"Choices\",\r\n    \"tab_personality\": \"Vibe\",\r\n    \"tab_about\": \"Creds\",\r\n\r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"Cell Inspector\",\r\n    \"lbl_name\": \"ID:\",\r\n    \"lbl_value\": \"Current Lvl:\",\r\n    \"lbl_position\": \"Coords:\",\r\n    \"lbl_type\": \"Class:\",\r\n    \"grp_neurogenesis\": \"Origin Story\",\r\n    \"lbl_created\": \"Spawned:\",\r\n    \"lbl_trigger\": \"Trigger:\",\r\n    \"lbl_trigger_val\": \"Trigger Lvl:\",\r\n    \"lbl_state\": \"Linked State:\",\r\n    \"col_connected\": \"Linked To\",\r\n    \"col_weight\": \"Strength\",\r\n    \"col_direction\": \"Flow\",\r\n    \"btn_refresh_data\": \"Refresh\",\r\n    \"type_core\": \"OG\",\r\n    \"type_neuro\": \"Gen Z\",\r\n    \"type_system\": \"System\",\r\n    \"direction_incoming\": \"In\",\r\n    \"direction_outgoing\": \"Out\",\r\n\r\n    # ===== TUTORIAL STEPS =====\r\n    \"next\": \"Next ->\",\r\n    \"tutorial_step1_text\": \"New squid just dropped!\\n• Feed it when hangry\\n• Clean the nasty tank\\n• Stalk its behavior\",\r\n    \"tutorial_step2_text\": \"This is the brain. Round things are neurons.\\nThe network evolves based on vibes.\",\r\n    \"tutorial_step3_text\": \"The squid grows new brain cells when it gets traumatized or super happy.\\nEvolution, baby.\",\r\n    \"tutorial_step4_text\": \"Neurons that fire together, wire together. That's how it learns associations.\",\r\n    \"tutorial_step5_text\": \"It decides what to do based on needs and memories.\\nEvery decision shapes its future.\",\r\n    \"tutorial_step6_text\": \"Press D for Decor. Drag and drop stuff.\\nSee how it reacts. Scroll to resize, DEL to yeet.\",\r\n    \"tutorial_step7_text\": \"Keep dopamine up, panic down.\\nGood luck, don't kill it.\",\r\n\r\n    # ===== NETWORK & LEARNING TABS (NEW) =====\r\n    \"stats_neurons\": \"Cells\",\r\n    \"stats_connections\": \"Links\",\r\n    \"stats_health\": \"Integrity\",\r\n    \"emergency_alert\": \"🚨 RED ALERT: {name}\",\r\n    \"global_cooldown\": \"Cooldown\",\r\n    \"style_label\": \"Aesthetic:\",\r\n    \"chk_links\": \"Show links\",\r\n    \"chk_weights\": \"Show weights\",\r\n    \"chk_pruning\": \"Auto-prune\",\r\n    \"tooltip_brain_designer\": \"Open Designer\",\r\n    \"msg_already_open\": \"Already Open\",\r\n    \"msg_designer_running\": \"Designer is already running fam!\",\r\n    \"msg_launch_failed\": \"Launch Failed\",\r\n    \"msg_designer_fail\": \"Could not start Designer:\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"No Brain Found\",\r\n    \"msg_cannot_open_lab\": \"Can't open Lab: Brain missing.\",\r\n    \"msg_cannot_open_buffer\": \"Can't open Buffer: Brain missing.\",\r\n    \"msg_no_neurogenesis\": \"Evolution Failed\",\r\n    \"msg_neurogenesis_not_init\": \"Neurogenesis system broke.\",\r\n    \"msg_decorations_unavailable\": \"Shop Closed\",\r\n    \"msg_decorations_fail\": \"Decorations window MIA.\",\r\n    \"func_neurons_title\": \"Working Neurons\",\r\n    \"count_label\": \"Count\",\r\n    \"avg_utility_label\": \"Usefulness\",\r\n    \"total_activations_label\": \"Total Pings\",\r\n    \"specialisations_label\": \"Specs\",\r\n    \"buffer_title\": \"XP Buffer\",\r\n    \"buffer_header\": \"Recent History\",\r\n    \"col_type\": \"Type\",\r\n    \"col_pattern\": \"Pattern\",\r\n    \"col_outcome\": \"Result\",\r\n    \"col_time\": \"Time\",\r\n    \"btn_refresh\": \"F5\",\r\n    \"buffer_size\": \"Buffer size\",\r\n    \"top_patterns\": \"Meta Patterns\",\r\n    \"no_patterns\": \"Nothing yet\",\r\n\r\n    # Learning Tab Educational Content\r\n    \"learning_pairs_tab\": \"Collabs\",\r\n    \"mechanics_tab\": \"How It Works\",\r\n    \"hebbian_quote\": \"\\\"Neurons that vibe together, wire together\\\"\",\r\n    \"in_practice_title\": \"IRL\",\r\n    \"in_practice_text\": \"Hebbian learning links states like 'Hangry' and 'Dopamine' when you feed it. It's creating core memories.\",\r\n    \"mechanics_title\": \"The Mechanics\",\r\n    \"mechanics_intro\": \"Connection strength (weight) changes based on activity. Active together = BFFs. Active apart = Unfollowed.\",\r\n    \"learning_rule_title\": \"The Math (Ew)\",\r\n    \"where_label\": \"Key:\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = Change in vibe\",\r\n    \"eta_desc\": \"<b>η</b> = Learning speed\",\r\n    \"activation_desc\": \"<b>x, y</b> = Active or nah (1 or 0)\",\r\n    \"example_calc_title\": \"Example\",\r\n    \"scenario_label\": \"<b>Scenario:</b> 'Hangry' and 'Dopamine' both active\",\r\n    \"calc_result\": \"Connection gets stronger (+0.1).\",\r\n    \"over_time_title\": \"Eventually...\",\r\n    \"over_time_text\": \"Small changes add up. Frequent patterns become habits. This is how it learns!\",\r\n    \"str_excitatory\": \"Super Hype\",\r\n    \"weak_excitatory\": \"Kinda Hype\",\r\n    \"weak_inhibitory\": \"Kinda Hater\",\r\n    \"str_inhibitory\": \"Super Hater\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS =====\r\n    \"distance_rollover\": \"🌊 Distance counter looped! {multiplier}x prestige\",\r\n    \"time_min\": \"min\",\r\n    \"time_mins\": \"mins\",\r\n    \"time_hr\": \"hr\",\r\n    \"time_hrs\": \"hrs\",\r\n    \"time_fmt_hm\": \"{hours}h {minutes}m\",\r\n    \"stat_squid_age\": \"Age\",\r\n    \"stat_distance\": \"Steps Taken\",\r\n    \"stat_cheese\": \"Cheese Consumed\",\r\n    \"stat_sushi\": \"Sushi Consumed\",\r\n    \"stat_poops\": \"Messes Made\",\r\n    \"stat_max_poops\": \"Peak Messiness\",\r\n    \"stat_startles\": \"Times Spooked\",\r\n    \"stat_ink\": \"Ink Dumps\",\r\n    \"stat_colour_change\": \"Mood Swings\",\r\n    \"stat_rocks\": \"Rocks Yeeted\",\r\n    \"stat_plants\": \"Plant Interactions\",\r\n    \"stat_sleep\": \"Total Nap Time\",\r\n    \"stat_sickness\": \"Times Sick\",\r\n    \"stat_novelty_neurons\": \"Novelty Cells\",\r\n    \"stat_stress_neurons\": \"Trauma Cells\",\r\n    \"stat_reward_neurons\": \"Treat Cells\",\r\n    \"stat_current_neurons\": \"Total Cells\",\r\n\r\n    \"reset_stats_title\": \"Wipe Stats\",\r\n    \"reset_stats_msg\": \"You sure you want to delete all history?\",\r\n    \"export_stats_title\": \"Export Data\",\r\n    \"export_file_type\": \"Text Files (*.txt)\",\r\n    \"export_header\": \"Squid Data Dump\",\r\n    \"export_time\": \"Time\",\r\n    \"export_activity_section\": \"Activity Log\",\r\n    \"export_end\": \"End of File\",\r\n    \"export_success_title\": \"W\",\r\n    \"export_success_msg\": \"Dumped stats to {file_name}\",\r\n    \"export_error_title\": \"L\",\r\n    \"export_error_msg\": \"Export failed: {error}\",\r\n\r\n    # ===== ACHIEVEMENTS (NEW) =====\r\n    # Categories\r\n    \"cat_feeding\": \"Noms\",\r\n    \"cat_neurogenesis\": \"Big Brain\",\r\n    \"cat_sleep\": \"Naps\",\r\n    \"cat_milestones\": \"Flexes\",\r\n    \"cat_exploration\": \"Adventure\",\r\n    \"cat_cleaning\": \"Chores\",\r\n    \"cat_health\": \"Wellness\",\r\n    \"cat_interaction\": \"Social\",\r\n    \"cat_ink\": \"Ink\",\r\n    \"cat_memory\": \"Nostalgia\",\r\n    \"cat_emotional\": \"Emo\",\r\n    \"cat_secret\": \"Easter Eggs\",\r\n    \"cat_meta\": \"Meta\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"Points\",\r\n    \"ui_unlocked\": \"Unlocked\",\r\n    \"ui_achievement_unlocked\": \"Trophy Unlocked!\",\r\n    \"ui_hidden\": \"Hidden trophy\",\r\n    \"ui_all\": \"All\",\r\n    \"ui_points_gained\": \"pts\",\r\n\r\n    # --- Achievements ---\r\n\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"First Snack\",\r\n    \"ach_first_feeding_desc\": \"Feed the boi for the first time\",\r\n    \"ach_fed_10_times_name\": \"Snack Regular\",\r\n    \"ach_fed_10_times_desc\": \"Feed 10 times\",\r\n    \"ach_fed_50_times_name\": \"Simp for Squid\",\r\n    \"ach_fed_50_times_desc\": \"Feed 50 times\",\r\n    \"ach_fed_100_times_name\": \"Certified Chef\",\r\n    \"ach_fed_100_times_desc\": \"Feed 100 times\",\r\n    \"ach_fed_500_times_name\": \"Gordon Ramsay\",\r\n    \"ach_fed_500_times_desc\": \"Feed 500 times\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"Brain Blast\",\r\n    \"ach_first_neuron_desc\": \"Grow your first new neuron\",\r\n    \"ach_neurons_10_name\": \"Double Digits\",\r\n    \"ach_neurons_10_desc\": \"Grow 10 neurons\",\r\n    \"ach_neurons_50_name\": \"Galaxy Brain\",\r\n    \"ach_neurons_50_desc\": \"Grow 50 neurons\",\r\n    \"ach_neurons_100_name\": \"Megamind\",\r\n    \"ach_neurons_100_desc\": \"Grow 100 neurons\",\r\n    \"ach_first_neuron_levelup_name\": \"Level Up\",\r\n    \"ach_first_neuron_levelup_desc\": \"Level up a neuron once\",\r\n    \"ach_neuron_max_level_name\": \"Over 9000\",\r\n    \"ach_neuron_max_level_desc\": \"Max out a neuron\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"Nap Time\",\r\n    \"ach_first_sleep_desc\": \"Wake up from first nap\",\r\n    \"ach_slept_10_times_name\": \"Sleepyhead\",\r\n    \"ach_slept_10_times_desc\": \"Sleep 10 times\",\r\n    \"ach_dream_state_name\": \"Inception\",\r\n    \"ach_dream_state_desc\": \"Enter REM sleep\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"Hour One\",\r\n    \"ach_age_1_hour_desc\": \"Survive 1 hour\",\r\n    \"ach_age_10_hours_name\": \"Growing Up\",\r\n    \"ach_age_10_hours_desc\": \"Survive 10 hours\",\r\n    \"ach_age_24_hours_name\": \"Daily Grind\",\r\n    \"ach_age_24_hours_desc\": \"Survive 24 hours\",\r\n    \"ach_age_1_week_name\": \"Survivor\",\r\n    \"ach_age_1_week_desc\": \"Live for a week\",\r\n    \"ach_age_1_month_name\": \"Boomer\",\r\n    \"ach_age_1_month_desc\": \"Live for a month\",\r\n    \"ach_happiness_100_name\": \"Living Best Life\",\r\n    \"ach_happiness_100_desc\": \"Reach 100% Happiness\",\r\n    \"ach_all_stats_high_name\": \"Thriving\",\r\n    \"ach_all_stats_high_desc\": \"All stats > 80%\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"Maid Service\",\r\n    \"ach_first_clean_desc\": \"Clean for the first time\",\r\n    \"ach_cleaned_25_times_name\": \"Sparkling\",\r\n    \"ach_cleaned_25_times_desc\": \"Clean 25 times\",\r\n    \"ach_germaphobe_name\": \"Clean Freak\",\r\n    \"ach_germaphobe_desc\": \"Keep clean > 90% for 1 hour\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"The Cure\",\r\n    \"ach_first_medicine_desc\": \"Give meds once\",\r\n    \"ach_medicine_10_times_name\": \"Dr. Squid\",\r\n    \"ach_medicine_10_times_desc\": \"Give meds 10 times\",\r\n    \"ach_comeback_kid_name\": \"Clutch\",\r\n    \"ach_comeback_kid_desc\": \"Recover from < 20% health\",\r\n\r\n    # Interaction (Rocks)\r\n    \"ach_first_rock_pickup_name\": \"Cool Rock\",\r\n    \"ach_first_rock_pickup_desc\": \"Pick up a rock\",\r\n    \"ach_rocks_picked_10_name\": \"Geologist\",\r\n    \"ach_rocks_picked_10_desc\": \"Pick up 10 rocks\",\r\n    \"ach_rocks_picked_50_name\": \"Hoarder\",\r\n    \"ach_rocks_picked_50_desc\": \"Pick up 50 rocks\",\r\n    \"ach_first_rock_throw_name\": \"Yeet\",\r\n    \"ach_first_rock_throw_desc\": \"Throw a rock\",\r\n    \"ach_rocks_thrown_25_name\": \"Catapult\",\r\n    \"ach_rocks_thrown_25_desc\": \"Throw 25 rocks\",\r\n    \"ach_rocks_thrown_100_name\": \"Sniper\",\r\n    \"ach_rocks_thrown_100_desc\": \"Throw 100 rocks\",\r\n\r\n    # Interaction (Decor)\r\n    \"ach_first_decoration_push_name\": \"Feng Shui\",\r\n    \"ach_first_decoration_push_desc\": \"Push decor once\",\r\n    \"ach_decorations_pushed_10_name\": \"Mover\",\r\n    \"ach_decorations_pushed_10_desc\": \"Push decor 10 times\",\r\n    \"ach_decorations_pushed_50_name\": \"Pivot!\",\r\n    \"ach_decorations_pushed_50_desc\": \"Push decor 50 times\",\r\n    \"ach_first_plant_interact_name\": \"Plant Parent\",\r\n    \"ach_first_plant_interact_desc\": \"Touch a plant\",\r\n    \"ach_plants_interacted_10_name\": \"Gardener\",\r\n    \"ach_plants_interacted_10_desc\": \"Touch plants 10 times\",\r\n    \"ach_plants_interacted_50_name\": \"Druid\",\r\n    \"ach_plants_interacted_50_desc\": \"Touch plants 50 times\",\r\n    \"ach_objects_investigated_25_name\": \"Inspector\",\r\n    \"ach_objects_investigated_25_desc\": \"Investigate 25 things\",\r\n    \"ach_objects_investigated_100_name\": \"Sherlock\",\r\n    \"ach_objects_investigated_100_desc\": \"Investigate 100 things\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"Ew David\",\r\n    \"ach_first_poop_throw_desc\": \"Threw poop. Disgusting.\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"Vape Naysh\",\r\n    \"ach_first_ink_cloud_desc\": \"Ink cloud released\",\r\n    \"ach_ink_clouds_20_name\": \"Octopus Mode\",\r\n    \"ach_ink_clouds_20_desc\": \"Release 20 ink clouds\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"First Memory\",\r\n    \"ach_first_memory_desc\": \"Form a memory\",\r\n    \"ach_memory_long_term_name\": \"Core Memory\",\r\n    \"ach_memory_long_term_desc\": \"Create a long-term memory\",\r\n    \"ach_memories_50_name\": \"Elephant\",\r\n    \"ach_memories_50_desc\": \"Store 50 memories\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"Curious George\",\r\n    \"ach_curiosity_100_desc\": \"100% Curiosity\",\r\n    \"ach_zen_master_name\": \"Zen Master\",\r\n    \"ach_zen_master_desc\": \"Anxiety < 10% for 30 mins\",\r\n    \"ach_first_startle_name\": \"Jump Scare\",\r\n    \"ach_first_startle_desc\": \"Startle the squid\",\r\n    \"ach_nervous_wreck_name\": \"Panic Attack\",\r\n    \"ach_nervous_wreck_desc\": \"Reach 100% Anxiety\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"Night Owl\",\r\n    \"ach_night_owl_desc\": \"Play 12AM - 4AM\",\r\n    \"ach_early_bird_name\": \"Early Bird\",\r\n    \"ach_early_bird_desc\": \"Play 5AM - 7AM\",\r\n    \"ach_weekend_warrior_name\": \"No Life\",\r\n    \"ach_weekend_warrior_desc\": \"Play Sat & Sun\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"Brain Surgeon\",\r\n    \"ach_brain_surgeon_desc\": \"Open brain tool\",\r\n    \"ach_speed_demon_name\": \"Speedrun\",\r\n    \"ach_speed_demon_desc\": \"Max speed for 10 mins\",\r\n    \"ach_completionist_name\": \"Completionist\",\r\n    \"ach_completionist_desc\": \"Unlock 30 achievements\",\r\n\r\n    # ===== NEURON LABORATORY =====\r\n    \"lab_title\": \"🧠 The Lab\",\r\n    \"lab_live_refresh\": \"Live feed\",\r\n    \"lab_unlock_editing\": \"🔓 God Mode\",\r\n    \"lab_tab_overview\": \"📊 Vibes\",\r\n    \"lab_tab_inspector\": \"🔍 Deep Dive\",\r\n    \"lab_tab_edit\": \"🔧 Sandbox\",\r\n    \"lab_status_ready\": \"Ready\",\r\n    \"lab_status_locked\": \"🔒 {name} locked @ {value}\",\r\n    \"lab_status_unlocked\": \"🔓 {name} unlocked\",\r\n    \r\n    # Overview Tab\r\n    \"lab_ov_counters\": \"Counters\",\r\n    \"lab_ov_newest\": \"Fresh Spawns\",\r\n    \"lab_ov_limits\": \"Limits & Pruning\",\r\n    \"lab_ov_actions\": \"Quick Actions\",\r\n    \"lab_force_hebbian\": \"Force Hebbian\",\r\n    \"lab_pruning_enabled\": \"Pruning on:\",\r\n    \"lab_none_yet\": \"Empty\",\r\n    \"lab_ago\": \"{seconds}s ago\",\r\n    \r\n    # Inspector Tab\r\n    \"lab_pick_neuron\": \"Pick a cell to stalk:\",\r\n    \"lab_connections_title\": \"The Network\",\r\n    \"lab_header_partner\": \"Connected To\",\r\n    \"lab_header_weight\": \"Weight\",\r\n    \"lab_header_type\": \"Type\",\r\n    \"lab_header_inf\": \"Clout\",\r\n    \"lab_impact_title\": \"Simulation\",\r\n    \"lab_header_neuron\": \"Neuron\",\r\n    \"lab_header_delta\": \"Δ Value\",\r\n    \"lab_no_connections\": \"Forever alone (no connections)\",\r\n    \"lab_did_you_know\": \"Fun Fact:\",\r\n    \"lab_type_excitatory\": \"Hype Man\",\r\n    \"lab_type_inhibitory\": \"Hater\",\r\n    \r\n    # Edit Tab\r\n    \"lab_edit_locked_msg\": \"⚠️ Read-only. Click 'God Mode' to edit.\",\r\n    \"lab_edit_header\": \"Drag values to mess with its brain\",\r\n    \"lab_unlock_title\": \"Unlock God Mode?\",\r\n    \"lab_unlock_msg\": \"You can now edit brain values directly. Don't break it.\",\r\n    \r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"smol\",\r\n    \"lab_inf_mild\": \"meh\",\r\n    \"lab_inf_mod\": \"mid\",\r\n    \"lab_inf_strong\": \"CHAD\",\r\n    \r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"Hunger is basic. High hunger = anxiety and zero chill.\",\r\n    \"lab_tip_happiness\": \"Happiness is driven by treats. Kills anxiety.\",\r\n    \"lab_tip_anxiety\": \"Anxiety is the enemy. It stops curiosity.\",\r\n    \"lab_tip_curiosity\": \"Curiosity requires novelty. Encourages exploring.\",\r\n    \"lab_tip_core\": \"OG Neuron - Can't delete this.\",\r\n    \"lab_tip_neuro_default\": \"Generated Neuron - Born from trauma or joy.\",\r\n    \"lab_tip_neuro_fmt\": \"Spawned by <b>{trigger}</b> – Job: <b>{spec}</b>.\",\r\n\r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"Squid POV\",\r\n    \"vis_logic_unavailable\": \"Brain.exe not found.\",\r\n    \"vis_nothing_in_view\": \"Looking at nothing.\",\r\n    \"vis_distance\": \"range\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"Class\",\r\n    \"tooltip_type\": \"Type\",\r\n    \"tooltip_current\": \"Val\",\r\n    \"tooltip_utility\": \"Utility\",\r\n    \"tooltip_activations\": \"Pings\",\r\n    \"tooltip_last_active\": \"Last Seen\",\r\n    \"tooltip_age\": \"Age\",\r\n    \"tooltip_core\": \"Core\",\r\n    \"tooltip_generated\": \"New\",\r\n    \"tooltip_functional\": \"Func\",\r\n    \"tooltip_connections_header\": \"Links\",\r\n    \"tooltip_connections_stats\": \"{incoming} in, {outgoing} out\",\r\n    \"tooltip_top_incoming\": \"Top Influencers\",\r\n    \"tooltip_top_outgoing\": \"Influencing\",\r\n    \"tooltip_hint\": \"Double-click to stalk • Right-click for menu\",\r\n\r\n    # State values\r\n    \"state_on\": \"ON\",\r\n    \"state_off\": \"OFF\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"{val}s ago\",\r\n    \"fmt_m_ago\": \"{val}m ago\",\r\n    \"fmt_h_ago\": \"{val}h ago\",\r\n    \"fmt_s_short\": \"{val}s\",\r\n    \"fmt_m_short\": \"{val}m\",\r\n    \"fmt_h_short\": \"{val}h\",\r\n    \"fmt_d_short\": \"{val}d\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW UI =====\r\n    \"designer_window_title\": \"Brain Architect - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"Brain Architect [Live Feed]\",\r\n    \r\n    # Tabs\r\n    \"designer_tab_layers\": \"Layers\",\r\n    \"designer_tab_sensors\": \"Inputs\",\r\n    \"designer_tab_props\": \"Props\",\r\n    \"designer_tab_connections\": \"Wiring\",\r\n    \"designer_tab_outputs\": \"Outputs\",\r\n    \r\n    # Toolbar\r\n    \"designer_btn_generate\": \"🎲 Randomize\",\r\n    \"designer_tooltip_generate\": \"YOLO generate connections\",\r\n    \"designer_btn_neuron\": \"➕ Cell\",\r\n    \"designer_tooltip_neuron\": \"Add neuron (Shift+N)\",\r\n    \"designer_btn_fix\": \"🔧 Auto-Fix\",\r\n    \"designer_tooltip_fix\": \"Fix broken stuff\",\r\n    \"designer_btn_validate\": \"✓ Vibe Check\",\r\n    \"designer_tooltip_validate\": \"Validate Design\",\r\n    \"designer_btn_sync\": \"🔄 Sync Live\",\r\n    \"designer_tooltip_sync\": \"Steal brain from running game\",\r\n    \"designer_btn_clear_conn\": \"🗑 Cut Wires\",\r\n    \"designer_tooltip_clear_conn\": \"Delete all connections\",\r\n    \"designer_tooltip_dice\": \"Roll the dice\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 <b>Left-Drag</b> to connect\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+Drag</b> to move\",\r\n    \"designer_help_pan\": \"<b>Right-Drag</b> to pan\",\r\n    \"designer_help_zoom\": \"<b>Scroll</b> to zoom\",\r\n    \"designer_help_edit_weight\": \"<b>Dbl-Click</b> connection to edit\",\r\n    \"designer_help_select\": \"<b>Click</b> to select\",\r\n    \"designer_help_delete\": \"<b>Del</b> to yeet\",\r\n    \"designer_help_reverse\": \"<b>Space</b> to reverse\",\r\n    \"designer_help_keys_weight\": \"<b>+/-</b> to adjust weight\",\r\n    \"designer_help_page_weight\": \"<b>PgUp/Dn</b> for big adjustments\",\r\n    \"designer_help_add_neuron\": \"<b>Shift+N</b> new neuron\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b> save\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b> open\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b> export\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b> new\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b> generate\",\r\n    \"designer_help_dice\": \"🎲 <b>Dice</b> for chaos\",\r\n    \"designer_help_outputs\": \"<b>Outputs tab</b> binds behavior\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"File\",\r\n    \"designer_menu_edit\": \"Edit\",\r\n    \"designer_menu_templates\": \"Presets\",\r\n    \"designer_menu_generate\": \"Generate\",\r\n    \r\n    # Actions\r\n    \"designer_action_new\": \"New Canvas\",\r\n    \"designer_action_save\": \"Save...\",\r\n    \"designer_action_export\": \"Export to Game...\",\r\n    \"designer_action_open\": \"Open...\",\r\n    \"designer_action_gen_sparse\": \"Generate Network...\",\r\n    \"designer_action_autofix\": \"Fix My Mess\",\r\n    \"designer_action_validate\": \"Validate\",\r\n    \"designer_action_clear_conn\": \"Nuke Connections\",\r\n    \"designer_action_clear_outputs\": \"Nuke Outputs\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"Cells: {count}\",\r\n    \"designer_status_connections\": \"Links: {count}\",\r\n    \"designer_status_required\": \"Valid: {ok}\",\r\n    \"designer_status_outputs\": \"Outs: {count}\",\r\n    \"designer_status_selected\": \"Selected: {source} → {target} (w: {weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"Updated: {source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"Deleted: {source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"Nuked {count} links\",\r\n    \"designer_status_cleared_out\": \"Nuked {count} outputs\",\r\n    \"designer_status_generated\": \"Generated {count} links ({style})\",\r\n    \"designer_status_random_gen\": \"🎲 Rolled {count} links ({style})\",\r\n    \"designer_status_synced\": \"✨ Synced: {neurons} cells, {connections} links\",\r\n    \"designer_status_imported\": \"✨ Imported live brain\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"Game Dead\",\r\n    \"designer_msg_game_not_running\": \"Game isn't running.\\n\\nRestart it to sync.\",\r\n    \"designer_msg_sync_confirm_title\": \"Sync Live Brain\",\r\n    \"designer_msg_sync_confirm\": \"Overwrite everything with the live game brain?\",\r\n    \"designer_msg_sync_failed_title\": \"Sync Failed\",\r\n    \"designer_msg_sync_failed\": \"Import failed.\",\r\n    \r\n    \"designer_msg_live_import_title\": \"Live Import\",\r\n    \"designer_msg_live_import_header\": \"🧠 Live Brain Imported\",\r\n    \"designer_msg_live_import_body\": \"Showing the live network.\\n\\n• {neurons} cells\\n• {connections} links\\n\\nChanges here won't affect the live game (sandbox mode).\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"Clear Connections\",\r\n    \"designer_msg_clear_conn_confirm\": \"Delete ALL {count} connections?\\n\\nCells stay.\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"Clear Outputs\",\r\n    \"designer_msg_clear_out_empty\": \"Nothing to clear.\",\r\n    \"designer_msg_clear_out_confirm\": \"Delete ALL {count} output bindings?\",\r\n    \r\n    \"designer_msg_new_design_title\": \"New Design\",\r\n    \"designer_msg_new_design_confirm\": \"Start fresh? Unsaved work goes bye-bye.\",\r\n    \r\n    \"designer_msg_autofix_title\": \"Auto-Fix\",\r\n    \"designer_msg_autofix_result\": \"Fixed {count} things:\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"Looks clean.\",\r\n    \r\n    \"designer_msg_save_title\": \"Save\",\r\n    \"designer_msg_saved_title\": \"Saved\",\r\n    \"designer_msg_save_success\": \"Saved to: {msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n({count} outputs included)\",\r\n    \"designer_msg_error_title\": \"Error\",\r\n    \"designer_msg_save_fail\": \"Save failed:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"Export\",\r\n    \"designer_msg_exported_title\": \"Exported\",\r\n    \"designer_msg_export_success\": \"Export success\",\r\n    \"designer_msg_export_fail\": \"Export failed:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"Open\",\r\n    \"designer_msg_open_fail\": \"Load failed:\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"Load Preset\",\r\n    \"designer_msg_select_template\": \"Pick a vibe:\",\r\n    \"designer_msg_replace_design\": \"Overwrite current?\",\r\n    \r\n    \"designer_msg_status_title\": \"Status\",\r\n    \"designer_msg_status_ok\": \"\\n✅ Vibe Check: PASSED\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ ISSUES:\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"Weight\",\r\n    \"designer_input_weight_label\": \"Set weight {source} → {target}:\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"Nothing selected\",\r\n    \"designer_prop_no_selection_disabled\": \"Empty\",\r\n    \"designer_prop_lbl_name\": \"Name:\",\r\n    \"designer_prop_lbl_type\": \"Type:\",\r\n    \"designer_prop_lbl_x\": \"X:\",\r\n    \"designer_prop_lbl_y\": \"Y:\",\r\n    \"designer_prop_btn_delete\": \"Delete Cell\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"Add Cell\",\r\n    \"designer_add_grp_type\": \"Cell Type\",\r\n    \"designer_add_btn_custom\": \"✨ Custom / Plugin\",\r\n    \"designer_add_btn_sensor\": \"📡 Input Sensor\",\r\n    \"designer_add_tooltip_custom\": \"Named neuron for plugins\",\r\n    \"designer_add_grp_sensor\": \"Pick Sensor\",\r\n    \"designer_add_grp_custom\": \"Custom Def\",\r\n    \"designer_add_info_custom\": \"<i>Name must match plugin ID to work.<br>Ex: <b>'turbo_mode'</b>.</i>\",\r\n    \"designer_add_lbl_id\": \"ID / Name:\",\r\n    \"designer_add_ph_id\": \"e.g. turbo_mode\",\r\n    \"designer_add_btn_create\": \"Create\",\r\n    \"designer_add_all_added\": \"All inputs added\",\r\n    \"designer_add_err_title\": \"Error\",\r\n    \"designer_add_err_exists\": \"Exists\",\r\n    \"designer_add_msg_created\": \"Created {name}\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"+ Layer\",\r\n    \"designer_layer_dlg_title\": \"New Layer\",\r\n    \"designer_layer_dlg_label\": \"Name:\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"Inputs:\",\r\n    \"designer_sensor_tooltip_refresh\": \"Refresh List\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"Src\",\r\n    \"designer_conn_header_target\": \"Dst\",\r\n    \"designer_conn_header_weight\": \"W\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>Outputs</b><br><small>Link brain to actions.</small>\",\r\n    \"designer_output_btn_add\": \"➕ Link\",\r\n    \"designer_output_btn_edit\": \"✏️ Edit\",\r\n    \"designer_output_btn_remove\": \"🗑️ Del\",\r\n    \"designer_output_col_neuron\": \"Cell\",\r\n    \"designer_output_col_behavior\": \"→ Action\",\r\n    \"designer_output_col_threshold\": \"Thresh\",\r\n    \"designer_output_col_mode\": \"Mode\",\r\n    \"designer_output_col_enabled\": \"On\",\r\n    \"designer_output_info\": \"{count} links, {enabled} active\",\r\n    \"designer_output_err_missing\": \"⚠️ Missing cell\",\r\n    \"designer_output_dlg_remove_title\": \"Remove Link\",\r\n    \"designer_output_dlg_remove_msg\": \"Unlink {neuron} → {hook}?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"Add Link\",\r\n    \"designer_binding_title_edit\": \"Edit Link\",\r\n    \"designer_binding_grp_neuron\": \"Source\",\r\n    \"designer_binding_lbl_neuron\": \"Cell:\",\r\n    \"designer_binding_lbl_current\": \"Curr: --\",\r\n    \"designer_binding_grp_hook\": \"Action\",\r\n    \"designer_binding_lbl_trigger\": \"Trigger:\",\r\n    \"designer_binding_grp_settings\": \"Settings\",\r\n    \"designer_binding_lbl_thresh\": \"Threshold:\",\r\n    \"designer_binding_lbl_mode\": \"Mode:\",\r\n    \"designer_binding_lbl_cool\": \"Cooldown:\",\r\n    \"designer_binding_chk_enabled\": \"Active\",\r\n    \"designer_binding_err_neuron\": \"Pick a cell\",\r\n    \"designer_binding_err_hook\": \"Pick an action\",\r\n    \"designer_binding_err_duplicate\": \"Link already exists\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"Rising Edge (Goes Up)\",\r\n    \"designer_mode_falling\": \"Falling Edge (Goes Down)\",\r\n    \"designer_mode_above\": \"While Above (Hold)\",\r\n    \"designer_mode_below\": \"While Below (Hold)\",\r\n    \"designer_mode_change\": \"On Change\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"Stock sensor: {name}\",\r\n    \"desc_vision_food\": \"Sees food\",\r\n    \"desc_custom_sensor\": \"Custom: {plugin}\",\r\n    \"desc_builtin\": \"stock\",\r\n    \"desc_plugin\": \"plugin\",\r\n    \"desc_other\": \"misc\",\r\n    \"desc_vision\": \"sight\",\r\n    \r\n    # ===== DESIGNER TEMPLATES (Extra Keys) =====\r\n    \"tmpl_core_name\": \"🟡 Minimum Viable\",\r\n    \"tmpl_core_desc\": \"8 core cells\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Default\",\r\n    \"tmpl_dosidicus_desc\": \"Stock layout\",\r\n    \"tmpl_full_sensors_name\": \"🟡 All Inputs\",\r\n    \"tmpl_full_sensors_desc\": \"Everything plugged in\",\r\n    \"tmpl_insomniac_name\": \"🔴 The Insomniac\",\r\n    \"tmpl_insomniac_desc\": \"Can't sleep won't sleep\",\r\n    \"tmpl_hyperactive_name\": \"🔴 The Zoomer\",\r\n    \"tmpl_hyperactive_desc\": \"Pure noise\",\r\n    \"tmpl_hangry_name\": \"🔴 The Hangry\",\r\n    \"tmpl_hangry_desc\": \"Rage monster\",\r\n    \"tmpl_depressive_name\": \"🔴 The Doomer\",\r\n    \"tmpl_depressive_desc\": \"Sad vibes only\",\r\n    \"tmpl_obsessive_name\": \"🔴 The Stan\",\r\n    \"tmpl_obsessive_desc\": \"Obsessive loop\",\r\n    \"layer_sensors\": \"Sensors\",\r\n    \"layer_core\": \"Core\",\r\n    \"layer_input\": \"Input\",\r\n    \"layer_out\": \"Output\",\r\n    \"layer_racing_mind\": \"Racing Mind\",\r\n    \"layer_state\": \"State\",\r\n    \"layer_vision\": \"Sight\",\r\n    \"layer_noise\": \"Noise\",\r\n    \"layer_output\": \"Out\",\r\n    \"layer_gut_brain\": \"Gut\",\r\n    \"layer_gray\": \"Gray Matter\",\r\n    \"layer_loop\": \"Loop\",\r\n    \"layer_stats\": \"Stats\",\r\n    \"layer_emotions\": \"Feels\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"Delete Link\",\r\n    \"designer_cnv_del_conn_msg\": \"Yeet this connection?\\n{source} → {target}\",\r\n    \"designer_cnv_chk_dont_ask\": \"Don't ask again\",\r\n    \"designer_cnv_btn_del\": \"Yeet\",\r\n    \"designer_cnv_btn_cancel\": \"Cancel\",\r\n    \"designer_cnv_dlg_edit_title\": \"Edit Link\",\r\n    \"designer_cnv_lbl_conn\": \"Link: {source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"Weight:\",\r\n    \"designer_cnv_info_weight\": \"Green = Hype (Excitatory), Red = Hater (Inhibitory)\",\r\n    \"designer_cnv_btn_del_conn\": \"Delete\",\r\n    \"designer_cnv_btn_ok\": \"Kk\",\r\n    \"designer_cnv_tooltip_invalid\": \"Broken link\",\r\n}"
  },
  {
    "path": "translations/zh.py",
    "content": "LANGUAGE_HEADER = \"zh - 中文\"\r\ntranslations = {\r\n    # Core continuous neurons\r\n    \"hunger\": \"饥饿感\",\r\n    \"happiness\": \"快乐值\",\r\n    \"cleanliness\": \"清洁度\",\r\n    \"sleepiness\": \"困倦感\",\r\n    \"satisfaction\": \"满足感\",\r\n    \"anxiety\": \"焦虑感\",\r\n    \"curiosity\": \"好奇心\",\r\n\r\n    # Binary/sensor neurons\r\n    \"can_see_food\": \"看见食物\",\r\n    \"is_eating\": \"进食中\",\r\n    \"is_sleeping\": \"睡眠中\",\r\n    \"is_sick\": \"生病中\",\r\n    \"pursuing_food\": \"寻找食物\",\r\n    \"is_startled\": \"受惊\",\r\n    \"is_fleeing\": \"逃跑中\",\r\n\r\n    # Base keys for neurogenesis patterns\r\n    \"novelty\": \"新奇\",\r\n    \"stress\": \"压力\",\r\n    \"reward\": \"奖励\",\r\n\r\n    # ===== MAIN MENU =====\r\n    \"file\": \"文件\",\r\n    \"new_game\": \"新游戏\",\r\n    \"load_game\": \"读取游戏\",\r\n    \"save_game\": \"保存游戏\",\r\n    \"view\": \"视图\",\r\n    \"speed\": \"速度\",\r\n    \"pause\": \"暂停\",\r\n    \"actions\": \"动作\",\r\n    \"debug\": \"调试\",\r\n    \"plugins\": \"插件\",\r\n\r\n    # ===== VIEW MENU =====\r\n    \"brain_designer\": \"大脑设计器\",\r\n    \"decorations\": \"装饰品\",\r\n    \"statistics\": \"统计数据\",\r\n    \"brain_tool\": \"大脑工具\",\r\n    \"neuron_lab\": \"神经元实验室\",\r\n    \"task_manager\": \"任务管理器\",\r\n\r\n    # ===== SPEED MENU =====\r\n    \"normal_speed\": \"正常 (1x)\",\r\n    \"fast_speed\": \"快速 (2x)\",\r\n    \"very_fast\": \"极快 (3x)\",\r\n\r\n    # ===== DEBUG MENU =====\r\n    \"toggle_debug\": \"切换调试模式\",\r\n    \"toggle_cone\": \"切换视锥显示\",\r\n    \"squid_vision\": \"鱿鱼视觉\",\r\n\r\n    # ===== ACTION BUTTONS =====\r\n    \"feed\": \"喂食\",\r\n    \"clean\": \"清洁\",\r\n    \"medicine\": \"喂药\",\r\n    \"feed_btn\": \"喂食\",\r\n    \"clean_btn\": \"清洁\",\r\n    \"medicine_btn\": \"药物\",\r\n\r\n    # ===== MESSAGES =====\r\n    \"feed_msg\": \"鱿鱼需要喂食\",\r\n    \"points\": \"得分\",\r\n    \"dirty\": \"脏乱\",\r\n    \"paused_msg\": \"模拟已暂停\",\r\n    \"paused_sub\": \"使用速度菜单恢复\",\r\n\r\n    # ===== DIALOGS =====\r\n    \"yes\": \"是\",\r\n    \"no\": \"否\",\r\n    \"ok\": \"确定\",\r\n    \"cancel\": \"取消\",\r\n    \"close\": \"关闭\",\r\n    \"save\": \"保存\",\r\n    \"load\": \"读取\",\r\n    \"reset\": \"重置\",\r\n    \"apply_changes\": \"应用更改\",\r\n    \"got_it\": \"知道了！\",\r\n    \"finish\": \"完成\",\r\n    \"confirm_new_game\": \"开始新游戏？当前进度将会丢失。\",\r\n    \"confirm_exit\": \"确定要退出吗？\",\r\n    \"save_successful\": \"游戏保存成功！\",\r\n    \"load_successful\": \"游戏读取成功！\",\r\n    \"error_saving\": \"保存游戏时出错。\",\r\n    \"error_loading\": \"读取游戏时出错。\",\r\n    \"no_save_found\": \"未找到存档文件。\",\r\n    \"startup\": \"启动\",\r\n    \"show_tutorial_q\": \"显示教程？\",\r\n    \"auto_decline\": \"({seconds}秒后自动拒绝)\",\r\n    \"tutorial_title\": \"教程\",\r\n    \"tutorial_query\": \"你想查看教程吗？\",\r\n\r\n    # ===== ABOUT TAB =====\r\n    \"hello\": \"你好\",\r\n    \"my_name_is\": \"我的名字是\",\r\n    \"change_name\": \"更改名字\",\r\n    \"enter_new_name\": \"为你的鱿鱼输入新名字：\",\r\n    \"change_colour\": \"更改颜色\",\r\n    \"view_certificate\": \"查看证书\",\r\n    \"care_tips\": \"饲养建议\",\r\n    \"care_tips_for\": \"{personality} 鱿鱼的饲养建议\",\r\n    \"dosidicus_title\": \"电子美洲大赤鱿 (Dosidicus electronicae)\",\r\n    \"dosidicus_desc\": \"一种拥有简单神经网络的电子宠物\",\r\n    \"string_acronym\": \"模拟电子宠物反应与推理及神经发生系统 (STRINg)\",\r\n    \"research_project\": \"这是一个研究项目。请建议新功能。\",\r\n    \"version_dosidicus\": \"Dosidicus 版本：\",\r\n    \"version_brain_tool\": \"大脑工具版本：\",\r\n    \"version_decision\": \"决策引擎版本：\",\r\n    \"version_neuro\": \"神经发生版本：\",\r\n    \"created_by\": \"作者\",\r\n\r\n    # ===== PERSONALITY =====\r\n    \"squid_personality\": \"鱿鱼个性\",\r\n    \"personality_modifier\": \"个性修正\",\r\n    \"description\": \"描述：\",\r\n    \"personality_modifiers\": \"个性修正值：\",\r\n    \"care_tips_label\": \"饲养建议：\",\r\n    \"personality_note\": \"注：个性是在新游戏开始时随机生成的\",\r\n\r\n    # Personality Types\r\n    \"personality_timid\": \"胆小\",\r\n    \"personality_adventurous\": \"爱冒险\",\r\n    \"personality_lazy\": \"懒惰\",\r\n    \"personality_energetic\": \"精力充沛\",\r\n    \"personality_introvert\": \"内向\",\r\n    \"personality_greedy\": \"贪吃\",\r\n    \"personality_stubborn\": \"固执\",\r\n\r\n    # Personality Descriptions\r\n    \"desc_timid\": \"你的鱿鱼很胆小。它容易受惊和焦虑，尤其是在新环境中。它可能更喜欢安静、平稳的环境，不太愿意独自探索。不过，当它感到安全时，能建立牢固的依赖关系。\",\r\n    \"desc_adventurous\": \"你的鱿鱼很爱冒险。它喜欢探索和尝试新事物。通常是它第一个去调查环境中的新物体或区域。这只鱿鱼喜欢新鲜感，在以此不变的环境中容易感到无聊。\",\r\n    \"desc_lazy\": \"你的鱿鱼很懒惰。它喜欢轻松的生活方式，活动量比其他鱿鱼少。它可能需要额外的鼓励才会参与活动，但只要躺着就会很满足。这只鱿鱼非常擅长保存能量！\",\r\n    \"desc_energetic\": \"你的鱿鱼精力充沛。它总是动个不停，充满活力。这只鱿鱼需要大量的刺激和活动来保持快乐。如果没有足够的机会消耗多余的精力，它可能会变得焦躁不安。\",\r\n    \"desc_introvert\": \"你的鱿鱼是个内向者。它享受孤独，可能更喜欢安静、不拥挤的空间。虽然它可以与其他生物互动，但它需要独处的时间来“充电”。这只鱿鱼在行动上可能更加敏锐和深思熟虑。\",\r\n    \"desc_greedy\": \"你的鱿鱼很贪吃。它非常关注食物和资源。与其他鱿鱼相比，它更容易被零食和奖励所驱动。虽然它可能要求更多，但也往往足智多谋，善于寻找隐藏的食物！\",\r\n    \"desc_stubborn\": \"你的鱿鱼很固执。它意志坚定，有明确的喜好。这只鱿鱼可能更抗拒改变，需要更长的时间来适应新常规。然而，它的决心也使它在解决问题时非常执着。\",\r\n\r\n    # Personality Short Modifiers\r\n    \"mod_timid\": \"变得焦虑的几率更高\",\r\n    \"mod_adventurous\": \"好奇心和探索欲增加\",\r\n    \"mod_lazy\": \"移动较慢，能量消耗较低\",\r\n    \"mod_energetic\": \"移动较快，活动水平较高\",\r\n    \"mod_introvert\": \"更喜欢独处和安静的环境\",\r\n    \"mod_greedy\": \"更专注于食物和资源\",\r\n    \"mod_stubborn\": \"只吃最爱的食物（寿司），可能拒绝睡觉\",\r\n\r\n    # Personality Modifier Details\r\n    \"modifiers_timid\": \"- 焦虑感增加速度快 50%\\n- 好奇心增加速度慢 50%\\n- 靠近植物时焦虑感减少 50%\",\r\n    \"modifiers_adventurous\": \"- 好奇心增加速度快 50%\",\r\n    \"modifiers_lazy\": \"- 移动速度更慢\\n- 能量消耗更低\",\r\n    \"modifiers_energetic\": \"- 移动速度更快\\n- 能量消耗更高\",\r\n    \"modifiers_introvert\": \"- 更喜欢安静、不拥挤的空间\\n- 可能需要更多独处时间来“充电”\",\r\n    \"modifiers_greedy\": \"- 饥饿时焦虑感增加 50%\\n- 进食时满足感增加更多\",\r\n    \"modifiers_stubborn\": \"- 更喜欢最爱的食物（寿司）\\n- 即使累了也可能拒绝睡觉\",\r\n\r\n    # Care Tips\r\n    \"tips_timid\": \"- 在环境中放置植物以减少焦虑\\n- 保持环境清洁和平静\\n- 缓慢接近，避免突然的动作\\n- 保持规律的作息\\n- 避免频繁调整窗口大小，这可能会吓到它\",\r\n    \"tips_adventurous\": \"- 定期引入新物体或装饰品\\n- 提供多样化的食物选择\\n- 通过策略性地放置食物鼓励探索\\n- 提供充足的探索空间\\n- 用有趣的物品满足它的好奇心\",\r\n    \"tips_lazy\": \"- 将食物放在离鱿鱼休息点更近的地方\\n- 更频繁地清洁环境\\n- 使用诱人的食物鼓励移动\\n- 不要期望太多活动——它们喜欢放松\\n- 确保它们最喜欢的休息点清洁舒适\",\r\n    \"tips_energetic\": \"- 提供大而开阔的移动空间\\n- 提供频繁的喂食机会\\n- 引入互动元素或游戏\\n- 用各种装饰品保持环境的刺激性\\n- 由于能量消耗高，它们需要更多食物\",\r\n    \"tips_introvert\": \"- 用装饰品创造安静、隐蔽的区域\\n- 避免环境过度拥挤\\n- 尊重鱿鱼独处的需求\\n- 利用植物创造庇护空间\\n- 温柔接近，必要时给它空间\",\r\n    \"tips_greedy\": \"- 提供多种食物，包括寿司\\n- 将食物作为良好行为的奖励\\n- 注意不要过度喂食\\n- 与其他类型相比，饥饿时会更焦虑\\n- 提供收集和排列物品的机会\",\r\n    \"tips_stubborn\": \"- 始终备有寿司，因为这是它们的最爱\\n- 引入改变时要有耐心\\n- 对期望的行为使用正向强化\\n- 饥饿时可能会拒绝非寿司类食物\\n- 即使累了也可能抗拒睡眠——需创造平静的环境\",\r\n\r\n    # ===== DECISIONS TAB =====\r\n    \"thought_process\": \"鱿鱼的思维过程\",\r\n    \"step\": \"步骤\",\r\n    \"step1_title\": \"感知世界\",\r\n    \"step2_title\": \"计算基本冲动\",\r\n    \"step3_title\": \"应用个性与记忆\",\r\n    \"step4_title\": \"做出最终决定\",\r\n    \"final_action\": \"最终动作：\",\r\n    \"awaiting_thought\": \"等待鱿鱼的下一个想法...\",\r\n    \"awaiting_decision\": \"等待决定...\",\r\n    \"sensing_condition\": \"鱿鱼评估当前状况和可见物体：\",\r\n    \"visible_objects\": \"可见物体\",\r\n    \"no_sensory_data\": \"无感官数据可用。\",\r\n    \"none\": \"无\",\r\n    \"no_urges\": \"未计算出冲动。\",\r\n    \"strongest_urge\": \"基于需求，最强烈的冲动是\",\r\n    \"initial_scores\": \"初始得分：\",\r\n    \"personality_memory_adjust\": \"个性特征和近期记忆随后调整这些冲动：\",\r\n    \"no_adjustments\": \"此次没有来自个性或记忆的显著调整。\",\r\n    \"final_scores_text\": \"经过所有计算后，统计最终得分。最高分决定动作。\",\r\n    \"no_final_scores\": \"无最终得分可用。\",\r\n    \"squid_decided\": \"鱿鱼决定\",\r\n    \"with_confidence\": \"置信度为\",\r\n    \"score_increased\": \"增加\",\r\n    \"score_decreased\": \"减少\",\r\n    \"score_for\": \"得分项\",\r\n    \"by_amount\": \"幅度\",\r\n\r\n    # ===== LEARNING TAB (ORIGINAL) =====\r\n    \"active_learning_pairs\": \"活跃学习对\",\r\n    \"hebbian_cycle\": \"赫布循环 (Hebbian Cycle)\",\r\n    \"hebbian_paused\": \"已暂停\",\r\n    \"learning_ready\": \"学习系统就绪\",\r\n    \"learning_ready_desc\": \"赫布学习将在同时激活的神经元之间建立关联。\",\r\n    \"log_cleared\": \"日志已清除\",\r\n    \"log_cleared_desc\": \"当你的鱿鱼神经元形成新连接时，学习对将显示在这里。\",\r\n    \"hebbian_overview\": \"赫布学习概览\",\r\n    \"neurons_fire_together\": \"一同激发的神经元连在一起 (Neurons that fire together, wire together)\",\r\n    \"hebbian_principle\": \"这一基本原则描述了神经网络如何通过经验进行学习。\",\r\n    \"hebbian_explanation\": \"赫布学习是人工神经网络中使用的一条简单而强大的规则。当两个神经元同时激活时，它们之间的连接（权重）会增强。如果它们分开激活，连接就会减弱。这使得网络能够自然地在相关概念之间形成关联。\",\r\n    \"excitatory_connections\": \"兴奋性连接\",\r\n    \"excitatory_desc\": \"正权重 (0.0-1.0) 使神经元更可能一起激活\",\r\n    \"inhibitory_connections\": \"抑制性连接\",\r\n    \"inhibitory_desc\": \"负权重 (-1.0-0.0) 使神经元不太可能一起激活\",\r\n    \"very_strong\": \"极强\",\r\n    \"strong\": \"强\",\r\n    \"moderate\": \"中等\",\r\n    \"weak\": \"弱\",\r\n    \"very_weak\": \"极弱\",\r\n    \"inhibited\": \"受抑制\",\r\n\r\n    # ===== MEMORY TAB =====\r\n    \"memory\": \"记忆\",\r\n    \"memories\": \"记忆列表\",\r\n    \"short_term_memory\": \"短期记忆\",\r\n    \"long_term_memory\": \"长期记忆\",\r\n    \"no_memories\": \"尚未存储记忆。\",\r\n    \"overview\": \"概览\",\r\n    \"memory_stats\": \"记忆统计\",\r\n    \"categories\": \"分类\",\r\n    \"time_label\": \"时间：\",\r\n    \"important_label\": \"重要\",\r\n    \"unknown\": \"未知\",\r\n    \"category_label\": \"分类：\",\r\n    \"key_label\": \"键值：\",\r\n    \"access_count\": \"访问次数：\",\r\n    \"full_content\": \"完整内容：\",\r\n    \"effects_label\": \"效果：\",\r\n    \"positive\": \"正面\",\r\n    \"negative\": \"负面\",\r\n    \"neutral\": \"中性\",\r\n\r\n    # ===== NETWORK TAB (ORIGINAL) =====\r\n    \"brain_network\": \"大脑网络\",\r\n    \"neurons\": \"神经元\",\r\n    \"connections\": \"连接\",\r\n    \"activity\": \"活动\",\r\n\r\n    # ===== STATISTICS WINDOW =====\r\n    \"status\": \"状态\",\r\n    \"health\": \"健康\",\r\n\r\n    # ===== NEURON NAMES (NEW) =====\r\n    \"hunger\": \"饥饿感\",\r\n    \"happiness\": \"快乐值\",\r\n    \"cleanliness\": \"清洁度\",\r\n    \"sleepiness\": \"困倦感\",\r\n    \"satisfaction\": \"满足感\",\r\n    \"curiosity\": \"好奇心\",\r\n    \"anxiety\": \"焦虑感\",\r\n    \"can_see_food\": \"看见食物\",\r\n    \"is_eating\": \"进食中\",\r\n    \"is_sleeping\": \"睡眠中\",\r\n    \"is_sick\": \"生病中\",\r\n    \"is_fleeing\": \"逃跑中\",\r\n    \"is_startled\": \"受惊\",\r\n    \"pursuing_food\": \"追逐食物\",\r\n    \"external_stimulus\": \"外部刺激\",\r\n    \"plant_proximity\": \"靠近植物\",\r\n    \"stress\": \"压力\",\r\n    \"novelty\": \"新奇\",\r\n    \"reward\": \"奖励\",\r\n\r\n    # ===== BRAIN WIDGET LAYERS (NEW) =====\r\n    \"layer_name\": \"层级\",\r\n    \"layer_input\": \"输入层\",\r\n    \"layer_output\": \"输出层\",\r\n    \"layer_hidden\": \"隐藏层\",\r\n\r\n    # ===== NEUROGENESIS LOGS (NEW) =====\r\n    \"log_created\": \"{time} - 创建了一个 {type} 神经元 ({name})，因为 {type} 计数器达到了 {value:.2f}\",\r\n    \"log_pruned\": \"{time} - 一个神经元 ({name}) 因 {reason} 被修剪\",\r\n    \"log_stress_detail\": \"建立了一个到 焦虑感 的抑制性连接\\n最大焦虑值已永久降低 10\",\r\n\r\n    # State Pills\r\n    \"fleeing\": \"逃跑中！\",\r\n    \"startled\": \"受惊！\",\r\n    \"eating\": \"进食\",\r\n    \"sleeping\": \"睡眠\",\r\n    \"playing\": \"玩耍\",\r\n    \"hiding\": \"躲藏\",\r\n    \"anxious\": \"焦虑\",\r\n    \"curious\": \"好奇\",\r\n\r\n    # ===== COMMON ACTIONS =====\r\n    \"eat\": \"吃\",\r\n    \"sleep\": \"睡\",\r\n    \"play\": \"玩\",\r\n    \"explore\": \"探索\",\r\n    \"rest\": \"休息\",\r\n    \"hide\": \"躲藏\",\r\n    \"wander\": \"闲逛\",\r\n    \"idle\": \"空闲\",\r\n    \"seek_food\": \"寻找食物\",\r\n    \"seek_shelter\": \"寻找庇护\",\r\n\r\n    # ===== OBJECTS =====\r\n    \"food\": \"食物\",\r\n    \"rock\": \"石头\",\r\n    \"poop\": \"便便\",\r\n    \"plant\": \"植物\",\r\n    \"sushi\": \"寿司\",\r\n    \"decoration\": \"装饰品\",\r\n\r\n    # ===== TUTORIAL =====\r\n    \"tutorial_hatched\": \"一只鱿鱼孵化了，你必须照顾它！\",\r\n    \"tutorial_feed\": \"当它饿的时候喂它（动作菜单）\",\r\n    \"tutorial_clean\": \"当水箱变脏时进行清洁\",\r\n    \"tutorial_watch\": \"观察它的行为以了解它的个性\",\r\n    \"tutorial_neural\": \"神经网络\",\r\n    \"tutorial_neural_desc\": \"这是鱿鱼的神经网络。它的行为由需求（圆形神经元）驱动。\\n网络会随着鱿鱼与环境的互动而适应和学习。\",\r\n    \"tutorial_satisfaction\": \"保持高满足感和低焦虑感。\",\r\n    \"tutorial_traits\": \"你的鱿鱼会根据你的抚养方式发展出独特的特征和行为。\",\r\n\r\n    # ===== BRAIN DESIGNER (Templates) =====\r\n    \"designer_title\": \"大脑设计器\",\r\n    \"required_only\": \"仅必要项\",\r\n    \"dosidicus_default\": \"Dosidicus 默认\",\r\n    \"full_sensors\": \"全传感器套件\",\r\n    \"the_insomniac\": \"失眠患者\",\r\n    \"the_hyperactive\": \"多动症\",\r\n    \"the_hangry\": \"饿怒症\",\r\n    \"the_depressive\": \"抑郁症\",\r\n    \"the_obsessive\": \"强迫症\",\r\n    \"balanced\": \"平衡型\",\r\n    \"minimal\": \"极简型\",\r\n    \"dense\": \"密集型\",\r\n    \"chaotic\": \"混乱型\",\r\n    \"calm\": \"平静型\",\r\n\r\n    # ===== SPLASH SCREEN =====\r\n    \"squid_hatched\": \"一只鱿鱼孵化了！\",\r\n    \"look_after\": \"你需要照顾它..\",\r\n\r\n    # ===== BRAIN TOOL TABS =====\r\n    \"tab_learning\": \"学习\",\r\n    \"tab_decisions\": \"决策\",\r\n    \"tab_personality\": \"个性\",\r\n    \"tab_about\": \"关于\",\r\n\r\n    # ===== NEURON INSPECTOR =====\r\n    \"inspector_title\": \"神经元检查器\",\r\n    \"lbl_name\": \"名称：\",\r\n    \"lbl_value\": \"当前值：\",\r\n    \"lbl_position\": \"位置：\",\r\n    \"lbl_type\": \"类型：\",\r\n    \"grp_neurogenesis\": \"神经发生详情\",\r\n    \"lbl_created\": \"创建于：\",\r\n    \"lbl_trigger\": \"触发类型：\",\r\n    \"lbl_trigger_val\": \"触发值：\",\r\n    \"lbl_state\": \"关联状态：\",\r\n    \"col_connected\": \"连接到\",\r\n    \"col_weight\": \"权重\",\r\n    \"col_direction\": \"方向\",\r\n    \"btn_refresh_data\": \"刷新数据\",\r\n    \"type_core\": \"核心\",\r\n    \"type_neuro\": \"神经发生\",\r\n    \"type_system\": \"系统状态\",\r\n    \"direction_incoming\": \"传入\",\r\n    \"direction_outgoing\": \"传出\",\r\n\r\n    # ===== TUTORIAL STEPS =====\r\n    \"next\": \"下一步\",\r\n    \"tutorial_step1_text\": \"一只鱿鱼孵化了，你必须照顾它！\\n• 当它饿的时候喂它（动作菜单）\\n• 当水箱变脏时进行清洁\\n• 观察它的行为以了解它的个性\",\r\n    \"tutorial_step2_text\": \"这是鱿鱼的神经网络。它的行为由需求（神经元）驱动。\\n网络会随着鱿鱼与环境的互动而适应和学习。\",\r\n    \"tutorial_step3_text\": \"鱿鱼可以通过产生新的神经元来响应极端的环境刺激。\\n这些新神经元帮助鱿鱼适应具有挑战性的情况。\",\r\n    \"tutorial_step4_text\": \"当一对神经元同时激发时，它们的连接会增强。这使得鱿鱼能够学习不同刺激和反应之间的关联。\",\r\n    \"tutorial_step5_text\": \"神经网络根据当前需求和过去的记忆做出决定。\\n每一个决定都会影响鱿鱼的状态并塑造未来的行为。\",\r\n    \"tutorial_step6_text\": \"随时按 D 键打开装饰品窗口\\n将装饰品拖放到环境中，看看鱿鱼对不同事物的反应。每种装饰类型都会以独特的方式影响鱿鱼的心理状态。点击并使用鼠标滚轮调整大小/按 DEL 删除\",\r\n    \"tutorial_step7_text\": \"保持高满足感和低焦虑感。\\n你的鱿鱼会根据你的抚养方式发展出独特的特征和行为。\",\r\n\r\n    # ===== NETWORK & LEARNING TABS (NEW) =====\r\n    \"stats_neurons\": \"神经元\",\r\n    \"stats_connections\": \"连接数\",\r\n    \"stats_health\": \"网络健康\",\r\n    \"emergency_alert\": \"🚨 紧急情况：{name}\",\r\n    \"global_cooldown\": \"冷却时间\",\r\n    \"style_label\": \"风格：\",\r\n    \"chk_links\": \"显示连线\",\r\n    \"chk_weights\": \"显示权重\",\r\n    \"chk_pruning\": \"启用修剪\",\r\n    \"tooltip_brain_designer\": \"显示大脑设计器\",\r\n    \"msg_already_open\": \"已打开\",\r\n    \"msg_designer_running\": \"大脑设计器已在运行！\",\r\n    \"msg_launch_failed\": \"启动失败\",\r\n    \"msg_designer_fail\": \"无法启动大脑设计器：\\n\\n{e}\",\r\n    \"msg_missing_brain\": \"缺少大脑\",\r\n    \"msg_cannot_open_lab\": \"无法打开神经元实验室：大脑组件不可用。\",\r\n    \"msg_cannot_open_buffer\": \"无法打开经验缓冲区：大脑组件不可用。\",\r\n    \"msg_no_neurogenesis\": \"无神经发生\",\r\n    \"msg_neurogenesis_not_init\": \"无法打开经验缓冲区：神经发生系统未初始化。\",\r\n    \"msg_decorations_unavailable\": \"装饰品不可用\",\r\n    \"msg_decorations_fail\": \"无法打开装饰品：窗口不可用。\",\r\n    \"func_neurons_title\": \"功能性神经元\",\r\n    \"count_label\": \"计数\",\r\n    \"avg_utility_label\": \"平均效用\",\r\n    \"total_activations_label\": \"总激活数\",\r\n    \"specialisations_label\": \"专精\",\r\n    \"buffer_title\": \"神经发生经验缓冲区\",\r\n    \"buffer_header\": \"近期经历\",\r\n    \"col_type\": \"类型\",\r\n    \"col_pattern\": \"模式\",\r\n    \"col_outcome\": \"结果\",\r\n    \"col_time\": \"时间\",\r\n    \"btn_refresh\": \"刷新\",\r\n    \"buffer_size\": \"缓冲区大小\",\r\n    \"top_patterns\": \"热门模式\",\r\n    \"no_patterns\": \"尚无模式\",\r\n\r\n    # Learning Tab Educational Content\r\n    \"learning_pairs_tab\": \"学习对\",\r\n    \"mechanics_tab\": \"机制\",\r\n    \"hebbian_quote\": \"“一同激发的神经元连在一起”\",\r\n    \"in_practice_title\": \"实践中\",\r\n    \"in_practice_text\": \"在你的鱿鱼大脑中，赫布学习有助于关联相关状态，例如进食时的“饥饿”与“满足”，或探索时的“好奇”与“焦虑”。这些习得的关联会影响未来的行为。\",\r\n    \"mechanics_title\": \"学习机制\",\r\n    \"mechanics_intro\": \"赫布学习根据神经元的活动模式更新它们之间的连接强度（权重）。当神经元一起激活时，它们的连接增强；当它们分开激活时，连接减弱。\",\r\n    \"learning_rule_title\": \"学习规则\",\r\n    \"where_label\": \"其中：\",\r\n    \"delta_w_desc\": \"<b>Δw</b> = 两个神经元之间权重的变化\",\r\n    \"eta_desc\": \"<b>η</b> (eta) = 学习率（控制变化速度）\",\r\n    \"activation_desc\": \"<b>x, y</b> = 神经元的激活值（1 为激活，0 为未激活）\",\r\n    \"example_calc_title\": \"计算示例\",\r\n    \"scenario_label\": \"<b>场景：</b> “饥饿”和“满足”同时激活\",\r\n    \"calc_result\": \"权重增加 0.1，增强了这些神经元之间的连接。\",\r\n    \"over_time_title\": \"随着时间推移\",\r\n    \"over_time_text\": \"通过重复激活，这些微小的权重变化会累积。频繁共同出现的模式会发展出强连接，而罕见出现的模式则形成弱连接或负连接。这就是你的鱿鱼从经验中学习的方式！\",\r\n    \"str_excitatory\": \"强兴奋性\",\r\n    \"weak_excitatory\": \"弱兴奋性\",\r\n    \"weak_inhibitory\": \"弱抑制性\",\r\n    \"str_inhibitory\": \"强抑制性\",\r\n\r\n    # ===== SQUID & BRAIN STATISTICS =====\r\n    \"distance_rollover\": \"🌊 距离计数器翻转！现在是 {multiplier} 倍\",\r\n    \"time_min\": \"分\",\r\n    \"time_mins\": \"分\",\r\n    \"time_hr\": \"小时\",\r\n    \"time_hrs\": \"小时\",\r\n    \"time_fmt_hm\": \"{hours}小时 {minutes}分\",\r\n    \"stat_squid_age\": \"鱿鱼年龄\",\r\n    \"stat_distance\": \"游动距离 (像素)\",\r\n    \"stat_cheese\": \"吃掉的奶酪\",\r\n    \"stat_sushi\": \"吃掉的寿司\",\r\n    \"stat_poops\": \"产生的便便\",\r\n    \"stat_max_poops\": \"水箱中最大便便数\",\r\n    \"stat_startles\": \"受惊次数\",\r\n    \"stat_ink\": \"喷墨次数\",\r\n    \"stat_colour_change\": \"变色次数\",\r\n    \"stat_rocks\": \"扔出的石头\",\r\n    \"stat_plants\": \"植物互动\",\r\n    \"stat_sleep\": \"总睡眠时间 (秒)\",\r\n    \"stat_sickness\": \"生病次数\",\r\n    \"stat_novelty_neurons\": \"创建的新奇神经元\",\r\n    \"stat_stress_neurons\": \"创建的压力神经元\",\r\n    \"stat_reward_neurons\": \"创建的奖励神经元\",\r\n    \"stat_current_neurons\": \"当前神经元\",\r\n\r\n    \"reset_stats_title\": \"重置统计\",\r\n    \"reset_stats_msg\": \"确定要重置所有统计数据吗？\",\r\n    \"export_stats_title\": \"导出统计\",\r\n    \"export_file_type\": \"文本文件 (*.txt)\",\r\n    \"export_header\": \"鱿鱼统计导出\",\r\n    \"export_time\": \"导出时间\",\r\n    \"export_activity_section\": \"活动统计\",\r\n    \"export_end\": \"统计结束\",\r\n    \"export_success_title\": \"导出成功\",\r\n    \"export_success_msg\": \"统计数据已导出至 {file_name}\",\r\n    \"export_error_title\": \"导出错误\",\r\n    \"export_error_msg\": \"导出统计数据时出错：{error}\",\r\n\r\n    # ===== ACHIEVEMENTS (NEW) =====\r\n    # Categories\r\n    \"cat_feeding\": \"喂食\",\r\n    \"cat_neurogenesis\": \"神经发生\",\r\n    \"cat_sleep\": \"睡眠\",\r\n    \"cat_milestones\": \"里程碑\",\r\n    \"cat_exploration\": \"探索\",\r\n    \"cat_cleaning\": \"清洁\",\r\n    \"cat_health\": \"健康\",\r\n    \"cat_interaction\": \"互动\",\r\n    \"cat_ink\": \"墨汁\",\r\n    \"cat_memory\": \"记忆\",\r\n    \"cat_emotional\": \"情绪\",\r\n    \"cat_secret\": \"秘密\",\r\n    \"cat_meta\": \"元成就\",\r\n\r\n    # UI Elements\r\n    \"ui_points\": \"点数\",\r\n    \"ui_unlocked\": \"已解锁\",\r\n    \"ui_achievement_unlocked\": \"成就解锁！\",\r\n    \"ui_hidden\": \"隐藏成就\",\r\n    \"ui_all\": \"全部\",\r\n    \"ui_points_gained\": \"点数\",\r\n\r\n    # --- Achievements ---\r\n\r\n    # Feeding\r\n    \"ach_first_feeding_name\": \"第一口\",\r\n    \"ach_first_feeding_desc\": \"第一次喂食鱿鱼\",\r\n    \"ach_fed_10_times_name\": \"按时吃饭\",\r\n    \"ach_fed_10_times_desc\": \"喂食鱿鱼 10 次\",\r\n    \"ach_fed_50_times_name\": \"尽职饲养员\",\r\n    \"ach_fed_50_times_desc\": \"喂食鱿鱼 50 次\",\r\n    \"ach_fed_100_times_name\": \"特级大厨\",\r\n    \"ach_fed_100_times_desc\": \"喂食鱿鱼 100 次\",\r\n    \"ach_fed_500_times_name\": \"烹饪传奇\",\r\n    \"ach_fed_500_times_desc\": \"喂食鱿鱼 500 次\",\r\n\r\n    # Neurogenesis\r\n    \"ach_first_neuron_name\": \"大脑火花\",\r\n    \"ach_first_neuron_desc\": \"创建第一个神经发生神经元\",\r\n    \"ach_neurons_10_name\": \"神经网络\",\r\n    \"ach_neurons_10_desc\": \"通过神经发生创建 10 个神经元\",\r\n    \"ach_neurons_50_name\": \"心智扩张\",\r\n    \"ach_neurons_50_desc\": \"通过神经发生创建 50 个神经元\",\r\n    \"ach_neurons_100_name\": \"脑力发电站\",\r\n    \"ach_neurons_100_desc\": \"通过神经发生创建 100 个神经元\",\r\n    \"ach_first_neuron_levelup_name\": \"突触强化\",\r\n    \"ach_first_neuron_levelup_desc\": \"首次升级一个神经元\",\r\n    \"ach_neuron_max_level_name\": \"巅峰表现\",\r\n    \"ach_neuron_max_level_desc\": \"将一个神经元升级到最大强度\",\r\n\r\n    # Sleep\r\n    \"ach_first_sleep_name\": \"美梦\",\r\n    \"ach_first_sleep_desc\": \"鱿鱼从第一次睡眠中醒来\",\r\n    \"ach_slept_10_times_name\": \"休息充足\",\r\n    \"ach_slept_10_times_desc\": \"鱿鱼睡了 10 次\",\r\n    \"ach_dream_state_name\": \"深度梦想家\",\r\n    \"ach_dream_state_desc\": \"鱿鱼进入了快速眼动(REM)睡眠\",\r\n\r\n    # Milestones\r\n    \"ach_age_1_hour_name\": \"一小时大\",\r\n    \"ach_age_1_hour_desc\": \"鱿鱼活到了 1 小时大\",\r\n    \"ach_age_10_hours_name\": \"茁壮成长\",\r\n    \"ach_age_10_hours_desc\": \"鱿鱼活到了 10 小时大\",\r\n    \"ach_age_24_hours_name\": \"一日奇迹\",\r\n    \"ach_age_24_hours_desc\": \"鱿鱼存活了 24 小时\",\r\n    \"ach_age_1_week_name\": \"每周老兵\",\r\n    \"ach_age_1_week_desc\": \"鱿鱼存活了一周\",\r\n    \"ach_age_1_month_name\": \"每月老兵\",\r\n    \"ach_age_1_month_desc\": \"鱿鱼存活了一个月\",\r\n    \"ach_happiness_100_name\": \"纯粹的极乐\",\r\n    \"ach_happiness_100_desc\": \"快乐值达到 100%\",\r\n    \"ach_all_stats_high_name\": \"完美平衡\",\r\n    \"ach_all_stats_high_desc\": \"所有统计数据同时保持在 80% 以上\",\r\n\r\n    # Cleaning\r\n    \"ach_first_clean_name\": \"第一次擦洗\",\r\n    \"ach_first_clean_desc\": \"第一次清洁水箱\",\r\n    \"ach_cleaned_25_times_name\": \"一尘不染\",\r\n    \"ach_cleaned_25_times_desc\": \"清洁水箱 25 次\",\r\n    \"ach_germaphobe_name\": \"洁癖\",\r\n    \"ach_germaphobe_desc\": \"保持清洁度在 90% 以上持续 1 小时\",\r\n\r\n    # Health\r\n    \"ach_first_medicine_name\": \"急救\",\r\n    \"ach_first_medicine_desc\": \"第一次喂药\",\r\n    \"ach_medicine_10_times_name\": \"鱿鱼医生\",\r\n    \"ach_medicine_10_times_desc\": \"喂药 10 次\",\r\n    \"ach_comeback_kid_name\": \"东山再起\",\r\n    \"ach_comeback_kid_desc\": \"从极低健康值 (<20%) 恢复到满血\",\r\n\r\n    # Interaction (Rocks)\r\n    \"ach_first_rock_pickup_name\": \"岩石收藏家\",\r\n    \"ach_first_rock_pickup_desc\": \"第一次捡起石头\",\r\n    \"ach_rocks_picked_10_name\": \"石头采集者\",\r\n    \"ach_rocks_picked_10_desc\": \"捡起 10 块石头\",\r\n    \"ach_rocks_picked_50_name\": \"巨石囤积者\",\r\n    \"ach_rocks_picked_50_desc\": \"捡起 50 块石头\",\r\n    \"ach_first_rock_throw_name\": \"打水漂\",\r\n    \"ach_first_rock_throw_desc\": \"第一次扔石头\",\r\n    \"ach_rocks_thrown_25_name\": \"岩石发射器\",\r\n    \"ach_rocks_thrown_25_desc\": \"扔出 25 块石头\",\r\n    \"ach_rocks_thrown_100_name\": \"投石机大师\",\r\n    \"ach_rocks_thrown_100_desc\": \"扔出 100 块石头\",\r\n\r\n    # Interaction (Decor)\r\n    \"ach_first_decoration_push_name\": \"室内设计师\",\r\n    \"ach_first_decoration_push_desc\": \"第一次推动装饰品\",\r\n    \"ach_decorations_pushed_10_name\": \"搬家工人\",\r\n    \"ach_decorations_pushed_10_desc\": \"推动装饰品 10 次\",\r\n    \"ach_decorations_pushed_50_name\": \"风水大师\",\r\n    \"ach_decorations_pushed_50_desc\": \"推动装饰品 50 次\",\r\n    \"ach_first_plant_interact_name\": \"园艺新手\",\r\n    \"ach_first_plant_interact_desc\": \"第一次与植物互动\",\r\n    \"ach_plants_interacted_10_name\": \"花园探险家\",\r\n    \"ach_plants_interacted_10_desc\": \"与植物互动 10 次\",\r\n    \"ach_plants_interacted_50_name\": \"植物学家\",\r\n    \"ach_plants_interacted_50_desc\": \"与植物互动 50 次\",\r\n    \"ach_objects_investigated_25_name\": \"好奇的检查员\",\r\n    \"ach_objects_investigated_25_desc\": \"调查 25 种不同的物体\",\r\n    \"ach_objects_investigated_100_name\": \"大侦探\",\r\n    \"ach_objects_investigated_100_desc\": \"调查 100 种不同的物体\",\r\n\r\n    # Exploration (Poop)\r\n    \"ach_first_poop_throw_name\": \"捣蛋鬼\",\r\n    \"ach_first_poop_throw_desc\": \"鱿鱼第一次扔便便\",\r\n\r\n    # Ink\r\n    \"ach_first_ink_cloud_name\": \"烟幕弹\",\r\n    \"ach_first_ink_cloud_desc\": \"鱿鱼第一次释放墨汁云\",\r\n    \"ach_ink_clouds_20_name\": \"墨汁大师\",\r\n    \"ach_ink_clouds_20_desc\": \"释放 20 次墨汁云\",\r\n\r\n    # Memory\r\n    \"ach_first_memory_name\": \"第一份记忆\",\r\n    \"ach_first_memory_desc\": \"形成第一份记忆\",\r\n    \"ach_memory_long_term_name\": \"长远思考\",\r\n    \"ach_memory_long_term_desc\": \"将一份记忆提升到长期存储\",\r\n    \"ach_memories_50_name\": \"过目不忘\",\r\n    \"ach_memories_50_desc\": \"存储 50 份记忆\",\r\n\r\n    # Emotional\r\n    \"ach_curiosity_100_name\": \"好奇宝宝\",\r\n    \"ach_curiosity_100_desc\": \"好奇心达到 100%\",\r\n    \"ach_zen_master_name\": \"禅宗大师\",\r\n    \"ach_zen_master_desc\": \"保持焦虑感低于 10% 持续 30 分钟\",\r\n    \"ach_first_startle_name\": \"吓一跳！\",\r\n    \"ach_first_startle_desc\": \"第一次吓到鱿鱼\",\r\n    \"ach_nervous_wreck_name\": \"神经衰弱\",\r\n    \"ach_nervous_wreck_desc\": \"焦虑感达到 100%\",\r\n\r\n    # Secret\r\n    \"ach_night_owl_name\": \"夜猫子\",\r\n    \"ach_night_owl_desc\": \"在午夜到凌晨 4 点之间游玩\",\r\n    \"ach_early_bird_name\": \"早起的鸟儿\",\r\n    \"ach_early_bird_desc\": \"在凌晨 5 点到 7 点之间游玩\",\r\n    \"ach_weekend_warrior_name\": \"周末战士\",\r\n    \"ach_weekend_warrior_desc\": \"在周六和周日都进行了游玩\",\r\n\r\n    # Meta\r\n    \"ach_brain_surgeon_name\": \"脑外科医生\",\r\n    \"ach_brain_surgeon_desc\": \"打开大脑可视化工具\",\r\n    \"ach_speed_demon_name\": \"速度恶魔\",\r\n    \"ach_speed_demon_desc\": \"以最快速度运行模拟 10 分钟\",\r\n    \"ach_completionist_name\": \"完美主义者\",\r\n    \"ach_completionist_desc\": \"解锁其他 30 个成就\",\r\n\r\n    # Additional Log/Debug Messages\r\n    \"Hebbian learning chosen pairs:\": \"赫布学习选择的对：\",\r\n    \"Main thread: Created neuron\": \"主线程：已创建神经元\",\r\n    \"BrainWidget received external BrainWorker\": \"BrainWidget 接收到外部 BrainWorker\",\r\n    \"Neurogenesis monitoring timer started\": \"神经发生监控计时器已启动\",\r\n    \"Brain state export enabled for designer sync\": \"大脑状态导出已启用以进行设计器同步\",\r\n    \"Animation palette built for style:\": \"动画调色板已构建，样式：\",\r\n    \"Animation style changed:\": \"动画样式已更改：\",\r\n    \"Unknown animation style:\": \"未知动画样式：\",\r\n    \"Available:\": \"可用：\",\r\n\r\n    # ===== NEURON LABORATORY =====\r\n    \"lab_title\": \"🧠 神经元实验室\",\r\n    \"lab_live_refresh\": \"实时刷新\",\r\n    \"lab_unlock_editing\": \"🔓 解锁编辑\",\r\n    \"lab_tab_overview\": \"📊 实时概览\",\r\n    \"lab_tab_inspector\": \"🔍 深度检查器\",\r\n    \"lab_tab_edit\": \"🔧 编辑沙箱\",\r\n    \"lab_status_ready\": \"就绪\",\r\n    \"lab_status_locked\": \"🔒 {name} 锁定于 {value}\",\r\n    \"lab_status_unlocked\": \"🔓 {name} 已解锁\",\r\n    \r\n    # Overview Tab\r\n    \"lab_ov_counters\": \"计数器进度\",\r\n    \"lab_ov_newest\": \"最新神经发生神经元\",\r\n    \"lab_ov_limits\": \"限制与修剪\",\r\n    \"lab_ov_actions\": \"快速操作\",\r\n    \"lab_force_hebbian\": \"强制赫布循环\",\r\n    \"lab_pruning_enabled\": \"启用修剪：\",\r\n    \"lab_none_yet\": \"暂无\",\r\n    \"lab_ago\": \"{seconds}秒前\",\r\n    \r\n    # Inspector Tab\r\n    \"lab_pick_neuron\": \"选择要检查的神经元：\",\r\n    \"lab_connections_title\": \"连接（兴奋性 vs 抑制性）\",\r\n    \"lab_header_partner\": \"伙伴\",\r\n    \"lab_header_weight\": \"权重\",\r\n    \"lab_header_type\": \"类型\",\r\n    \"lab_header_inf\": \"影响力\",\r\n    \"lab_impact_title\": \"功能影响模拟\",\r\n    \"lab_header_neuron\": \"神经元\",\r\n    \"lab_header_delta\": \"Δ 值\",\r\n    \"lab_no_connections\": \"目前没有活跃连接\",\r\n    \"lab_did_you_know\": \"你知道吗？\",\r\n    \"lab_type_excitatory\": \"兴奋性\",\r\n    \"lab_type_inhibitory\": \"抑制性\",\r\n    \r\n    # Edit Tab\r\n    \"lab_edit_locked_msg\": \"⚠️ 编辑已锁定 – 请勾选工具栏中的“解锁编辑”。\",\r\n    \"lab_edit_header\": \"神经元值（拖动以更改）– 点击 🔒 锁定\",\r\n    \"lab_unlock_title\": \"解锁编辑？\",\r\n    \"lab_unlock_msg\": \"你现在可以更改神经元值并强制创建事件。请谨慎操作！\",\r\n    \r\n    # Badges/Influence\r\n    \"lab_inf_tiny\": \"微小\",\r\n    \"lab_inf_mild\": \"轻微\",\r\n    \"lab_inf_mod\": \"中等\",\r\n    \"lab_inf_strong\": \"强劲\",\r\n    \r\n    # Educational Tips\r\n    \"lab_tip_hunger\": \"饥饿是一种稳态驱动力。高饥饿感会抑制满足感并加剧焦虑。\",\r\n    \"lab_tip_happiness\": \"快乐感由奖励神经元强化。它抑制焦虑并促进好奇心。\",\r\n    \"lab_tip_anxiety\": \"焦虑感通过压力神经元（抑制性）减少。高焦虑会抑制好奇心。\",\r\n    \"lab_tip_curiosity\": \"当新奇度高时，好奇心会激增。它鼓励探索并减少焦虑。\",\r\n    \"lab_tip_core\": \"核心神经元 – 对生存至关重要。\",\r\n    \"lab_tip_neuro_default\": \"神经发生神经元 – 目的由出生环境推断。\",\r\n    \"lab_tip_neuro_fmt\": \"由 <b>{trigger}</b> 创建 – 专精于 <b>{spec}</b>。它的工作是将经历转化为长期行为。\",\r\n\r\n    # ===== VISION WINDOW =====\r\n    \"vision_window_title\": \"鱿鱼视觉\",\r\n    \"vis_logic_unavailable\": \"鱿鱼逻辑不可用。\",\r\n    \"vis_nothing_in_view\": \"视野中目前没有任何东西。\",\r\n    \"vis_distance\": \"距离\",\r\n\r\n    # --- Brain Tooltips ---\r\n    \"tooltip_specialization\": \"专精\",\r\n    \"tooltip_type\": \"类型\",\r\n    \"tooltip_current\": \"当前\",\r\n    \"tooltip_utility\": \"效用\",\r\n    \"tooltip_activations\": \"激活次数\",\r\n    \"tooltip_last_active\": \"上次活跃\",\r\n    \"tooltip_age\": \"年龄\",\r\n    \"tooltip_core\": \"核心\",\r\n    \"tooltip_generated\": \"已生成\",\r\n    \"tooltip_functional\": \"功能性\",\r\n    \"tooltip_connections_header\": \"连接\",\r\n    \"tooltip_connections_stats\": \"{incoming} 传入, {outgoing} 传出\",\r\n    \"tooltip_top_incoming\": \"热门传入\",\r\n    \"tooltip_top_outgoing\": \"热门传出\",\r\n    \"tooltip_hint\": \"双击检查 • 右键选项\",\r\n\r\n    # State values\r\n    \"state_on\": \"开启\",\r\n    \"state_off\": \"关闭\",\r\n\r\n    # Time formatting\r\n    \"fmt_s_ago\": \"{val}秒前\",\r\n    \"fmt_m_ago\": \"{val}分前\",\r\n    \"fmt_h_ago\": \"{val}小时前\",\r\n    \"fmt_s_short\": \"{val}秒\",\r\n    \"fmt_m_short\": \"{val}分\",\r\n    \"fmt_h_short\": \"{val}小时\",\r\n    \"fmt_d_short\": \"{val}天\",\r\n\r\n    # ===== BRAIN DESIGNER WINDOW UI =====\r\n    \"designer_window_title\": \"大脑设计器 - Dosidicus-2\",\r\n    \"designer_window_title_imported\": \"大脑设计器 - Dosidicus-2 [从游戏导入]\",\r\n    \r\n    # Tabs\r\n    \"designer_tab_layers\": \"层级\",\r\n    \"designer_tab_sensors\": \"传感器\",\r\n    \"designer_tab_props\": \"属性\",\r\n    \"designer_tab_connections\": \"连接\",\r\n    \"designer_tab_outputs\": \"输出\",\r\n    \r\n    # Toolbar\r\n    \"designer_btn_generate\": \"🎲 生成稀疏网络\",\r\n    \"designer_tooltip_generate\": \"在核心神经元之间生成随机连接\",\r\n    \"designer_btn_neuron\": \"➕ 神经元\",\r\n    \"designer_tooltip_neuron\": \"添加新神经元 (Shift+N)\",\r\n    \"designer_btn_fix\": \"🔧 自动修复\",\r\n    \"designer_tooltip_fix\": \"自动修复孤立神经元和连接问题\",\r\n    \"designer_btn_validate\": \"✓ 验证\",\r\n    \"designer_tooltip_validate\": \"检查设计是否存在问题\",\r\n    \"designer_btn_sync\": \"🔄 从游戏同步\",\r\n    \"designer_tooltip_sync\": \"从运行中的 Dosidicus 游戏刷新大脑状态\",\r\n    \"designer_btn_clear_conn\": \"🗑 清除连接\",\r\n    \"designer_tooltip_clear_conn\": \"移除所有连接（保留神经元）\",\r\n    \"designer_tooltip_dice\": \"立即生成随机网络（无对话框）\",\r\n    \r\n    # Ticker / Help Bar\r\n    \"designer_help_drag_connect\": \"💡 <b>左键拖动</b> 从神经元创建连接\",\r\n    \"designer_help_ctrl_move\": \"<b>Ctrl+拖动</b> 移动神经元\",\r\n    \"designer_help_pan\": \"<b>右键拖动</b> 平移画布\",\r\n    \"designer_help_zoom\": \"<b>鼠标滚轮</b> 缩放（或调整连接权重）\",\r\n    \"designer_help_edit_weight\": \"<b>双击</b> 连接以编辑权重\",\r\n    \"designer_help_select\": \"<b>点击</b> 神经元/连接以选择\",\r\n    \"designer_help_delete\": \"<b>Del</b> 删除选中项\",\r\n    \"designer_help_reverse\": \"<b>空格键</b> 反转连接方向\",\r\n    \"designer_help_keys_weight\": \"<b>+/-</b> 键调整权重（Shift 加大幅度）\",\r\n    \"designer_help_page_weight\": \"<b>Page Up/Down</b> 调整权重（大幅度）\",\r\n    \"designer_help_add_neuron\": \"<b>Shift+N</b> 添加神经元\",\r\n    \"designer_help_save\": \"<b>Ctrl+S</b> 保存\",\r\n    \"designer_help_open\": \"<b>Ctrl+O</b> 打开\",\r\n    \"designer_help_export\": \"<b>Ctrl+E</b> 导出\",\r\n    \"designer_help_new\": \"<b>Ctrl+N</b> 新建设计\",\r\n    \"designer_help_gen\": \"<b>Ctrl+G</b> 生成网络\",\r\n    \"designer_help_dice\": \"🎲 <b>骰子按钮</b> 立即随机生成\",\r\n    \"designer_help_outputs\": \"<b>输出标签页</b> 将神经元绑定到鱿鱼行为\",\r\n    \r\n    # Menus\r\n    \"designer_menu_file\": \"文件\",\r\n    \"designer_menu_edit\": \"编辑\",\r\n    \"designer_menu_templates\": \"模板\",\r\n    \"designer_menu_generate\": \"生成\",\r\n    \r\n    # Actions\r\n    \"designer_action_new\": \"新建设计\",\r\n    \"designer_action_save\": \"保存...\",\r\n    \"designer_action_export\": \"导出为 Dosidicus...\",\r\n    \"designer_action_open\": \"打开...\",\r\n    \"designer_action_gen_sparse\": \"生成稀疏网络...\",\r\n    \"designer_action_autofix\": \"自动修复连接\",\r\n    \"designer_action_validate\": \"验证设计\",\r\n    \"designer_action_clear_conn\": \"清除所有连接\",\r\n    \"designer_action_clear_outputs\": \"清除所有输出绑定\",\r\n    \r\n    # Status Bar\r\n    \"designer_status_neurons\": \"神经元：{count}\",\r\n    \"designer_status_connections\": \"连接：{count}\",\r\n    \"designer_status_required\": \"必需项：{ok}\",\r\n    \"designer_status_outputs\": \"输出：{count}\",\r\n    \"designer_status_selected\": \"已选：{source} → {target} (权重：{weight:+.3f})\",\r\n    \"designer_status_weight_updated\": \"权重已更新：{source} → {target} = {weight:+.3f}\",\r\n    \"designer_status_deleted\": \"已删除连接：{source} → {target}\",\r\n    \"designer_status_cleared_conn\": \"已清除 {count} 个连接\",\r\n    \"designer_status_cleared_out\": \"已清除 {count} 个输出绑定\",\r\n    \"designer_status_generated\": \"使用 '{style}' 预设生成了 {count} 个连接\",\r\n    \"designer_status_random_gen\": \"🎲 生成了 {count} 个随机连接 (风格：{style})\",\r\n    \"designer_status_synced\": \"✨ 已同步：{neurons} 个神经元，{connections} 个连接\",\r\n    \"designer_status_imported\": \"✨ 已从运行中的游戏导入活跃大脑\",\r\n    \r\n    # Dialogs & Messages\r\n    \"designer_msg_game_not_running_title\": \"游戏未运行\",\r\n    \"designer_msg_game_not_running\": \"Dosidicus 游戏不再运行。\\n\\n请重新开始游戏以进行同步。\",\r\n    \"designer_msg_sync_confirm_title\": \"从游戏同步\",\r\n    \"designer_msg_sync_confirm\": \"用游戏中最新的大脑状态替换当前设计？\",\r\n    \"designer_msg_sync_failed_title\": \"同步失败\",\r\n    \"designer_msg_sync_failed\": \"无法从游戏导入大脑状态。\",\r\n    \r\n    \"designer_msg_live_import_title\": \"实时大脑导入\",\r\n    \"designer_msg_live_import_header\": \"🧠 已从运行中的游戏导入活跃大脑\",\r\n    \"designer_msg_live_import_body\": \"设计器现在显示的是你运行中的 Dosidicus 游戏的精确神经网络。\\n\\n• {neurons} 个神经元\\n• {connections} 个连接\\n\\n在此处所做的更改**不会**影响运行中的游戏。\",\r\n    \r\n    \"designer_msg_clear_conn_title\": \"清除连接\",\r\n    \"designer_msg_clear_conn_confirm\": \"移除所有 {count} 个连接？\\n\\n神经元将被保留。\",\r\n    \r\n    \"designer_msg_clear_out_title\": \"清除输出绑定\",\r\n    \"designer_msg_clear_out_empty\": \"没有可清除的输出绑定。\",\r\n    \"designer_msg_clear_out_confirm\": \"移除所有 {count} 个输出绑定？\",\r\n    \r\n    \"designer_msg_new_design_title\": \"新建设计\",\r\n    \"designer_msg_new_design_confirm\": \"开始新设计？未保存的更改将会丢失。\",\r\n    \r\n    \"designer_msg_autofix_title\": \"自动修复\",\r\n    \"designer_msg_autofix_result\": \"创建了 {count} 个连接：\\n\\n{details}\",\r\n    \"designer_msg_autofix_none\": \"未发现问题。\",\r\n    \r\n    \"designer_msg_save_title\": \"保存设计\",\r\n    \"designer_msg_saved_title\": \"已保存\",\r\n    \"designer_msg_save_success\": \"设计保存成功：{msg}\",\r\n    \"designer_msg_save_bindings\": \"\\n(包含 {count} 个输出绑定)\",\r\n    \"designer_msg_error_title\": \"错误\",\r\n    \"designer_msg_save_fail\": \"保存设计失败：\\n\\n{error}\",\r\n    \r\n    \"designer_msg_export_title\": \"导出\",\r\n    \"designer_msg_exported_title\": \"已导出\",\r\n    \"designer_msg_export_success\": \"设计导出成功\",\r\n    \"designer_msg_export_fail\": \"导出设计失败：\\n\\n{error}\",\r\n    \r\n    \"designer_msg_open_title\": \"打开设计\",\r\n    \"designer_msg_open_fail\": \"无法加载设计：\\n\\n{error}\",\r\n    \r\n    \"designer_msg_load_template_title\": \"加载模板\",\r\n    \"designer_msg_select_template\": \"选择一个模板：\",\r\n    \"designer_msg_replace_design\": \"替换当前设计？\",\r\n    \r\n    \"designer_msg_status_title\": \"设计状态\",\r\n    \"designer_msg_status_ok\": \"\\n✅ 状态：正常\",\r\n    \"designer_msg_status_issues\": \"\\n⚠️ 问题：\\n\",\r\n    \r\n    \"designer_input_weight_title\": \"连接权重\",\r\n    \"designer_input_weight_label\": \"设置权重 {source} → {target}:\",\r\n    \r\n    # ===== DESIGNER PANELS =====\r\n    # Properties Panel\r\n    \"designer_prop_no_selection\": \"未选择神经元\",\r\n    \"designer_prop_no_selection_disabled\": \"无选择\",\r\n    \"designer_prop_lbl_name\": \"名称：\",\r\n    \"designer_prop_lbl_type\": \"类型：\",\r\n    \"designer_prop_lbl_x\": \"X:\",\r\n    \"designer_prop_lbl_y\": \"Y:\",\r\n    \"designer_prop_btn_delete\": \"删除神经元\",\r\n    \r\n    # Add Neuron Dialog\r\n    \"designer_add_title\": \"添加神经元\",\r\n    \"designer_add_grp_type\": \"选择神经元类型\",\r\n    \"designer_add_btn_custom\": \"✨ 自定义 / 插件神经元\",\r\n    \"designer_add_btn_sensor\": \"📡 输入传感器\",\r\n    \"designer_add_tooltip_custom\": \"创建一个具有特定名称的神经元以链接游戏插件\",\r\n    \"designer_add_grp_sensor\": \"选择传感器\",\r\n    \"designer_add_grp_custom\": \"定义自定义神经元\",\r\n    \"designer_add_info_custom\": \"<i>为了影响鱿鱼，<b>名称</b>必须与插件 ID 匹配。<br>例如：命名为 <b>'jet_boost'</b> 以激活喷气背包插件。</i>\",\r\n    \"designer_add_lbl_id\": \"插件 ID / 名称：\",\r\n    \"designer_add_ph_id\": \"例如 turbo_mode\",\r\n    \"designer_add_btn_create\": \"创建链接\",\r\n    \"designer_add_all_added\": \"已添加所有传感器\",\r\n    \"designer_add_err_title\": \"错误\",\r\n    \"designer_add_err_exists\": \"已存在\",\r\n    \"designer_add_msg_created\": \"已创建 {name}\",\r\n    \r\n    # Layers Panel\r\n    \"designer_layer_btn_add\": \"添加层级\",\r\n    \"designer_layer_dlg_title\": \"新层级\",\r\n    \"designer_layer_dlg_label\": \"名称：\",\r\n    \r\n    # Sensors Panel\r\n    \"designer_sensor_header\": \"输入传感器：\",\r\n    \"designer_sensor_tooltip_refresh\": \"刷新传感器列表（包括插件注册的传感器）\",\r\n    \"designer_sensor_cat_label\": \"── {name} ──\",\r\n    \r\n    # Connections Table\r\n    \"designer_conn_header_source\": \"源\",\r\n    \"designer_conn_header_target\": \"目标\",\r\n    \"designer_conn_header_weight\": \"权重\",\r\n    \r\n    # ===== OUTPUTS PANEL =====\r\n    \"designer_output_header\": \"<b>输出绑定</b><br><small>将神经元连接到鱿鱼行为。当神经元的激活超过阈值时，它会触发绑定的动作。</small>\",\r\n    \"designer_output_btn_add\": \"➕ 添加绑定\",\r\n    \"designer_output_btn_edit\": \"✏️ 编辑\",\r\n    \"designer_output_btn_remove\": \"🗑️ 移除\",\r\n    \"designer_output_col_neuron\": \"神经元\",\r\n    \"designer_output_col_behavior\": \"→ 行为\",\r\n    \"designer_output_col_threshold\": \"阈值\",\r\n    \"designer_output_col_mode\": \"模式\",\r\n    \"designer_output_col_enabled\": \"启用\",\r\n    \"designer_output_info\": \"{count} 个绑定, {enabled} 个已启用\",\r\n    \"designer_output_err_missing\": \"⚠️ 设计中未找到神经元\",\r\n    \"designer_output_dlg_remove_title\": \"移除绑定\",\r\n    \"designer_output_dlg_remove_msg\": \"移除绑定：{neuron} → {hook}?\",\r\n    \r\n    # Output Binding Dialog\r\n    \"designer_binding_title_add\": \"添加输出绑定\",\r\n    \"designer_binding_title_edit\": \"配置输出绑定\",\r\n    \"designer_binding_grp_neuron\": \"源神经元\",\r\n    \"designer_binding_lbl_neuron\": \"神经元：\",\r\n    \"designer_binding_lbl_current\": \"当前：--\",\r\n    \"designer_binding_grp_hook\": \"输出行为\",\r\n    \"designer_binding_lbl_trigger\": \"触发器：\",\r\n    \"designer_binding_grp_settings\": \"触发设置\",\r\n    \"designer_binding_lbl_thresh\": \"阈值：\",\r\n    \"designer_binding_lbl_mode\": \"模式：\",\r\n    \"designer_binding_lbl_cool\": \"冷却：\",\r\n    \"designer_binding_chk_enabled\": \"启用\",\r\n    \"designer_binding_err_neuron\": \"请选择一个神经元\",\r\n    \"designer_binding_err_hook\": \"请选择一个输出行为\",\r\n    \"designer_binding_err_duplicate\": \"{neuron} → {hook} 的绑定已存在\",\r\n    \r\n    # Trigger Modes\r\n    \"designer_mode_rising\": \"上升沿 (穿过阈值向上)\",\r\n    \"designer_mode_falling\": \"下降沿 (穿过阈值向下)\",\r\n    \"designer_mode_above\": \"高于时 (持续 > 阈值)\",\r\n    \"designer_mode_below\": \"低于时 (持续 < 阈值)\",\r\n    \"designer_mode_change\": \"改变时 (任何显著变化)\",\r\n    \r\n    # ===== DESIGNER SENSOR DISCOVERY =====\r\n    \"desc_builtin_sensor\": \"内置传感器：{name}\",\r\n    \"desc_vision_food\": \"检测视锥内的食物\",\r\n    \"desc_custom_sensor\": \"来自 {plugin} 的自定义传感器\",\r\n    \"desc_builtin\": \"内置\",\r\n    \"desc_plugin\": \"插件\",\r\n    \"desc_other\": \"其他\",\r\n    \"desc_vision\": \"视觉\",\r\n    \r\n    # ===== DESIGNER TEMPLATES (Extra Keys) =====\r\n    \"tmpl_core_name\": \"🟡 仅必要项\",\r\n    \"tmpl_core_desc\": \"8 个必需神经元\",\r\n    \"tmpl_dosidicus_name\": \"🟡 Dosidicus 默认\",\r\n    \"tmpl_dosidicus_desc\": \"标准布局\",\r\n    \"tmpl_full_sensors_name\": \"🟡 全传感器套件\",\r\n    \"tmpl_full_sensors_desc\": \"所有传感器\",\r\n    \"tmpl_insomniac_name\": \"🔴 失眠患者\",\r\n    \"tmpl_insomniac_desc\": \"焦虑和好奇心阻止睡眠\",\r\n    \"tmpl_hyperactive_name\": \"🔴 多动症\",\r\n    \"tmpl_hyperactive_desc\": \"噪音神经元压倒困倦\",\r\n    \"tmpl_hangry_name\": \"🔴 饿怒症\",\r\n    \"tmpl_hangry_desc\": \"饥饿导致极端愤怒\",\r\n    \"tmpl_depressive_name\": \"🔴 抑郁症\",\r\n    \"tmpl_depressive_desc\": \"对快乐有抵抗力\",\r\n    \"tmpl_obsessive_name\": \"🔴 强迫症\",\r\n    \"tmpl_obsessive_desc\": \"焦虑/好奇心反馈循环\",\r\n    \"layer_sensors\": \"传感器\",\r\n    \"layer_core\": \"核心\",\r\n    \"layer_input\": \"输入\",\r\n    \"layer_out\": \"输出\",\r\n    \"layer_racing_mind\": \"思维奔逸\",\r\n    \"layer_state\": \"状态\",\r\n    \"layer_vision\": \"视觉\",\r\n    \"layer_noise\": \"噪音\",\r\n    \"layer_output\": \"输出\",\r\n    \"layer_gut_brain\": \"肠脑\",\r\n    \"layer_gray\": \"灰质\",\r\n    \"layer_loop\": \"循环\",\r\n    \"layer_stats\": \"统计\",\r\n    \"layer_emotions\": \"情绪\",\r\n    \r\n    # ===== CANVAS CONTEXT MENU / DIALOGS =====\r\n    \"designer_cnv_del_conn_title\": \"删除连接\",\r\n    \"designer_cnv_del_conn_msg\": \"确定要删除连接：\\n{source} → {target} 吗？\",\r\n    \"designer_cnv_chk_dont_ask\": \"不再询问\",\r\n    \"designer_cnv_btn_del\": \"是，删除\",\r\n    \"designer_cnv_btn_cancel\": \"取消\",\r\n    \"designer_cnv_dlg_edit_title\": \"编辑连接\",\r\n    \"designer_cnv_lbl_conn\": \"连接：{source} → {target}\",\r\n    \"designer_cnv_lbl_weight\": \"权重：\",\r\n    \"designer_cnv_info_weight\": \"正数 = 兴奋性 (绿色)，负数 = 抑制性 (红色)\",\r\n    \"designer_cnv_btn_del_conn\": \"删除连接\",\r\n    \"designer_cnv_btn_ok\": \"确定\",\r\n    \"designer_cnv_tooltip_invalid\": \"无效连接\",\r\n}"
  },
  {
    "path": "version",
    "content": "dosidicus: 2.6.2.0_STRINg2\r\nbrain_tool: 23.02.26\r\ndecision_engine: 4.0 Dec25\r\nneurogenesis:  3.3_stress_cap\r\n"
  }
]