├── Docs ├── Brain Window.md ├── Creating Plugins.md ├── DecisionEngine.md ├── Goal Neurons.md ├── Memory System.md ├── Neural Learning System.html ├── Overview.md ├── Overview2.md ├── Personalities.md ├── Simulating Awareness.md ├── Squid Class.md ├── Tamagotchi Class.md └── multiplayer │ └── squid_multiplayer_autopilot.md ├── LICENSE ├── README.md ├── _memory ├── LongTerm.json └── ShortTerm.json ├── config.ini ├── images ├── cheese.png ├── curious.png ├── decoration │ ├── bigr02.png │ ├── plant01.png │ ├── plant02.png │ ├── plant03.png │ ├── plant04.png │ ├── plant05.png │ ├── plant06.png │ ├── plant07.png │ ├── plant08.png │ ├── plant09.png │ ├── plant10.png │ ├── rock01.png │ ├── rock02.png │ ├── sml_plant01.png │ ├── st_bigr01.png │ ├── st_castle.png │ ├── st_urchin1.png │ └── urchin2.png ├── egg │ ├── anim01.jpg │ ├── anim02.jpg │ ├── anim03.jpg │ ├── anim04.jpg │ ├── anim05.jpg │ └── anim06.jpg ├── food.png ├── inkcloud.png ├── left1.png ├── left2.png ├── medicine.png ├── poop1.png ├── poop2.png ├── right1.png ├── right2.png ├── sick.png ├── sleep1.png ├── sleep2.png ├── squid_rps_frame.png ├── startled.png ├── sushi.png ├── think.png ├── up1.png └── up2.png ├── main.py ├── plugins └── multiplayer │ ├── __init__.py │ ├── main.py │ ├── multiplayer_config_dialog.py │ ├── multiplayer_events.py │ ├── multiplayer_status_widget.py │ ├── network_utilities.py │ ├── packet_validator.py │ ├── plugin.txt │ ├── remote_entity_manager.py │ ├── squid_multiplayer_autopilot.py │ └── status_bar_component.py ├── src ├── __init__.py ├── brain_about_tab.py ├── brain_base_tab.py ├── brain_decisions_tab.py ├── brain_dialogs.py ├── brain_learning_tab.py ├── brain_memory_tab.py ├── brain_network_tab.py ├── brain_tool.py ├── brain_ui_utils.py ├── brain_utils.py ├── brain_widget.py ├── certificate.py ├── config_manager.py ├── decision_engine.py ├── decoration_stats.json ├── display_scaling.py ├── image_cache.py ├── interactions.py ├── interactions2.py ├── learning.py ├── memory_manager.py ├── mental_states.py ├── neural_network_visualizer_tab.py ├── personality.py ├── personality_traits.py ├── plugin_manager.py ├── plugin_manager_dialog.py ├── rps_game.py ├── save_manager.py ├── splash_screen.py ├── squid.py ├── statistics_window.py ├── tamagotchi_logic.py ├── tutorial.py └── ui.py └── version /Docs/Brain Window.md: -------------------------------------------------------------------------------- 1 | # Technical Overview: Brain Window 2 | 3 | This module implements a neural network visualization and interaction system for a digital pet squid using PyQt5. It provides a window into the squid's "brain" with visual representations of neurons, their connections, and learning processes. 4 | 5 | ## Core Components 6 | 7 | ### 1\. BrainWidget Class 8 | 9 | The main visualization widget that displays the neural network and handles interactions. 10 | 11 | ### ` __init__(self) ` 12 | 13 | Initializes the brain widget with: 14 | 15 | * Neuron positions and connections 16 | 17 | * State variables (hunger, happiness, etc.) 18 | 19 | * Neurogenesis tracking system 20 | 21 | * Visualization settings 22 | 23 | ### `update_state(self, new_state)` 24 | 25 | Updates the brain's state with new values and triggers visualization updates. 26 | 27 | Parameters: new_state (dict) - Dictionary containing new state values 28 | 29 | ###`check_neurogenesis(self, state)` 30 | 31 | Checks conditions for creating new neurons based on novelty, stress, and reward thresholds. 32 | 33 | Parameters: state (dict) - Current brain state 34 | 35 | Returns: bool - True if any neurons were created 36 | 37 | ### `paintEvent(self, event)` 38 | 39 | Handles all drawing operations for the widget, including: 40 | 41 | * Neurons (circles, squares, triangles) 42 | * Connections between neurons 43 | * Weight values 44 | * Highlights for new neurons 45 | 46 | ### `update_weights(self)` 47 | 48 | Randomly adjusts connection weights between neurons when not frozen. 49 | 50 | ### 2\. SquidBrainWindow Class 51 | 52 | The main application window that contains the brain visualization and various control tabs. 53 | 54 | ### ` __init\__(self, tamagotchi\_logic, debug\_mode=False)` 55 | 56 | Initializes the main window with: 57 | 58 | * Reference to the tamagotchi logic 59 | * Debug mode flag 60 | * Various timers for updates 61 | * Tab-based interface 62 | 63 | ### `update\_brain(self, state)` 64 | 65 | Main method for updating the brain visualization with new state data. 66 | 67 | Parameters: state (dict) - Complete brain state dictionary 68 | 69 | ### `perform_hebbian_learning(self)` 70 | 71 | Implements Hebbian learning ("neurons that fire together wire together") by: 72 | 73 | 1. Identifying active neurons 74 | 2. Selecting random pairs of active neurons 75 | 3. Updating their connection weights 76 | 4. Logging the changes 77 | 78 | ### `update_connection(self, neuron1, neuron2, value1, value2)` 79 | 80 | Updates the weight between two neurons based on their activation levels. 81 | 82 | Parameters: neuron1, neuron2 (str) - Names of the neurons 83 | 84 | Parameters: value1, value2 (float) - Activation levels (0-100) 85 | 86 | ## Key Features 87 | 88 | ### Neurogenesis System 89 | 90 | The brain can grow new neurons under certain conditions: 91 | 92 | * **Novelty:** When exposed to new experiences 93 | * **Stress:** During prolonged stressful situations 94 | * **Reward:** After receiving positive reinforcement 95 | 96 | New neurons are visually distinct (triangular) and have default connections. 97 | 98 | ### Visual Representation 99 | 100 | The brain visualization includes: 101 | 102 | * **Original neurons:** Circles (basic needs) and squares (complex states) 103 | * **New neurons:** Triangles (color-coded by type) 104 | * **Connections:** Colored lines (green=positive, red=negative) 105 | * **Weights:** Displayed at connection midpoints 106 | 107 | ### Interactive Features 108 | 109 | * **Neuron dragging:** Rposition neurons with mouse 110 | * **Stimulation:** Manual input of state values via dialog 111 | * **Training:** Capture and apply Hebbian learning 112 | * **Freezing:** Temporarily stop weight changes 113 | 114 | ### Information Tabs 115 | 116 | The interface provides multiple tabs for different aspects: 117 | 118 | | Tab | Purpose | 119 | | --- | --- | 120 | | Network | Main brain visualization and controls | 121 | | Memory | Short-term and long-term memory display | 122 | | Personality | Personality traits and care tips | 123 | | Learning | Weight change history and analysis | 124 | | Thinking | Decision-making process visualization | 125 | 126 | ## Important Data Structures 127 | 128 | ### Brain State 129 | 130 | The core state dictionary contains: 131 | 132 | * `hunger`, `happiness`, `cleanliness`, `sleepiness` (0-100) 133 | * `satisfaction`, `anxiety`, `curiosity` (0-100) 134 | * Boolean flags: `is_sick`, `is_eating`, `is_sleeping` 135 | * Movement: `direction`, `position` 136 | 137 | ### Neurogenesis Data 138 | 139 | Tracks neuron creation: 140 | 141 | * `novelty_counter`: Count of novel experiences 142 | * `new_neurons`: List of created neurons 143 | * `last_neuron_time`: Timestamp of last creation 144 | 145 | ### Connection Weights 146 | 147 | Stored as a dictionary with tuple keys (neuron pairs) and float values (-1 to 1): 148 | 149 | `{ 150 | ("hunger", "satisfaction"): 0.75, 151 | ("happiness", "cleanliness"): 0.32, 152 | ... 153 | }` 154 | 155 | ### Implementation Notes 156 | 157 | * The system uses PyQt5 for visualization and Qt's signal/slot mechanism for updates 158 | * Neuron positions are stored as (x,y) tuples in `neuron_positions` 159 | * The Hebbian learning timer runs every 2 seconds by default 160 | * Debug mode provides additional console output 161 | 162 | ## Integration Points 163 | 164 | The module integrates with the main tamagotchi system through: 165 | 166 | * `tamagotchi_logic` reference for state updates 167 | * Memory manager access for memory tab updates 168 | * Personality system for trait-specific behaviors 169 | -------------------------------------------------------------------------------- /Docs/Creating Plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin Template Structure 2 | 3 | 4 | ### Examine the `/plugins/multiplayer` directory for the multiplayer plugin example 5 | 6 | When creating a plugin for Dosidicus, you should follow this structure: 7 | 8 | ``` 9 | plugins/ 10 | └── plugin_name/ 11 | ├── main.py 12 | └── assets/ 13 | └── (any images or other assets) 14 | ``` 15 | 16 | 17 | ## Available Hooks 18 | 19 | The following hooks are available for plugins to use: 20 | 21 | 1. `on_init(tamagotchi_logic)`: Called when the game is initialized 22 | 2. `on_update(tamagotchi_logic)`: Called every game update tick 23 | 3. `on_feed(tamagotchi_logic)`: Called when the squid is fed 24 | 4. `on_clean(tamagotchi_logic)`: Called when the environment is cleaned 25 | 5. `on_medicine(tamagotchi_logic)`: Called when medicine is given 26 | 6. `on_spawn_food(tamagotchi_logic, food_item)`: Called when food is spawned 27 | 7. `on_spawn_poop(tamagotchi_logic, poop_item)`: Called when poop is spawned 28 | 8. `on_save_game(tamagotchi_logic, save_data)`: Called when the game is saved 29 | 9. `on_load_game(tamagotchi_logic, save_data)`: Called when the game is loaded 30 | 10. `on_squid_state_change(squid, attribute, old_value, new_value)`: Called when a squid attribute changes 31 | 11. `on_setup_ui(ui)`: Called when the UI is being set up 32 | 12. `on_key_press(event)`: Called when a key is pressed 33 | 13. `on_scene_click(event)`: Called when the scene is clicked 34 | 14. `on_window_resize(event)`: Called when the window is resized 35 | 15. `on_menu_setup(menu_bar)`: Called when the menu bar is being set up 36 | 37 | ## Using the Plugin API 38 | 39 | The `tamagotchi_logic.mod_api` provides various helper methods for interacting with the game: 40 | 41 | ```python 42 | # UI operations 43 | api.show_message("Hello world!") 44 | api.register_menu("My Menu") 45 | 46 | # Game operations 47 | api.spawn_food(x=100, y=100, is_sushi=True) 48 | api.modify_stat("happiness", 10) # Add 10 to happiness 49 | api.add_memory("mod", "action", "Did something cool", importance=7) 50 | 51 | # Custom graphics 52 | pixmap = QtGui.QPixmap("path/to/image.png") 53 | api.register_custom_graphic(pixmap, x=200, y=200) 54 | 55 | # Timers 56 | api.register_timer(1000, my_timer_callback) # Call every 1 second 57 | 58 | # Keyboard 59 | api.register_keyboard_shortcut("Ctrl+M", my_shortcut_callback) 60 | ``` 61 | -------------------------------------------------------------------------------- /Docs/DecisionEngine.md: -------------------------------------------------------------------------------- 1 | `DecisionEngine` calculates decision weights for potential actions based on neural states, memories, and personality traits. The highest-weighted action is executed 2 | 3 | ### Core Components: 4 | 5 | #### * Neural State Integration: 6 | 7 | Utilizes real-time neural states (e.g., hunger, anxiety) to drive decision-making, reflecting the squid's internal motivations. 8 | 9 | #### * Memory-Driven Influence: 10 | 11 | Incorporates active memories to adjust current states, allowing past experiences to influence present actions dynamically. 12 | 13 | #### * Personality-Based Modifiers: 14 | 15 | Adjusts decision weights based on predefined personality traits, ensuring diverse behavioral patterns across different squid instances. 16 | 17 | #### * Stochastic Elements: 18 | 19 | Introduces randomness to decision weights, enhancing unpredictability and mimicking natural variability in behavior. 20 | 21 | --------- 22 | 23 | Implemented in `decision_engine.py` : 24 | 25 | ```python 26 | # Decision engine version 1.0 April 2025 27 | 28 | import random 29 | from .personality import Personality 30 | 31 | class DecisionEngine: 32 | def __init__(self, squid): 33 | """ 34 | Initialize the DecisionEngine with a squid object. 35 | 36 | Parameters: 37 | - squid: An object representing the squid, containing attributes and methods 38 | related to its state and behaviors. 39 | """ 40 | self.squid = squid 41 | 42 | def make_decision(self): 43 | """ 44 | Decision-making process based on the squid's neural network state and current conditions. 45 | This function aims to simulate decision-making with minimal hardcoding, relying on 46 | the squid's neural state and active memories. 47 | """ 48 | # Gather the current state of the squid 49 | current_state = { 50 | "hunger": self.squid.hunger, 51 | "happiness": self.squid.happiness, 52 | "cleanliness": self.squid.cleanliness, 53 | "sleepiness": self.squid.sleepiness, 54 | "satisfaction": self.squid.satisfaction, 55 | "anxiety": self.squid.anxiety, 56 | "curiosity": self.squid.curiosity, 57 | "is_sick": self.squid.is_sick, 58 | "is_sleeping": self.squid.is_sleeping, 59 | "has_food_visible": bool(self.squid.get_visible_food()), 60 | "carrying_rock": self.squid.carrying_rock, 61 | "rock_throw_cooldown": getattr(self.squid, 'rock_throw_cooldown', 0) 62 | } 63 | 64 | # Retrieve the brain network state, which influences emergent behavior 65 | brain_state = self.squid.tamagotchi_logic.squid_brain_window.brain_widget.state 66 | 67 | # Collect active memories to influence the decision-making process 68 | active_memories = self.squid.memory_manager.get_active_memories_data(3) 69 | memory_influence = {} 70 | 71 | # Process active memories to determine their influence on the current state 72 | for memory in active_memories: 73 | if isinstance(memory.get('raw_value'), dict): 74 | for key, value in memory['raw_value'].items(): 75 | if key in memory_influence: 76 | memory_influence[key] += value * 0.5 # Memory influence is half the weight of the current state 77 | else: 78 | memory_influence[key] = value * 0.5 79 | 80 | # Apply the influence of memories to the current state 81 | for key, value in memory_influence.items(): 82 | if key in current_state and isinstance(current_state[key], (int, float)): 83 | current_state[key] = min(100, max(0, current_state[key] + value)) 84 | 85 | # Check for extreme conditions that should override neural decisions 86 | if self.squid.sleepiness >= 95: 87 | self.squid.go_to_sleep() 88 | return "sleeping" 89 | 90 | if self.squid.is_sleeping: 91 | return "sleeping" 92 | 93 | # Calculate decision weights for each possible action based on the neural state 94 | decision_weights = { 95 | "exploring": brain_state.get("curiosity", 50) * 0.8 * (1 - (brain_state.get("anxiety", 50) / 100)), 96 | "eating": brain_state.get("hunger", 50) * 1.2 if self.squid.get_visible_food() else 0, 97 | "approaching_rock": brain_state.get("curiosity", 50) * 0.7 if not self.squid.carrying_rock else 0, 98 | "throwing_rock": brain_state.get("satisfaction", 50) * 0.7 if self.squid.carrying_rock else 0, 99 | "avoiding_threat": brain_state.get("anxiety", 50) * 0.9, 100 | "organizing": brain_state.get("satisfaction", 50) * 0.5 101 | } 102 | 103 | # Adjust decision weights based on the squid's personality 104 | if self.squid.personality == Personality.TIMID: 105 | decision_weights["avoiding_threat"] *= 1.5 106 | decision_weights["approaching_rock"] *= 0.7 107 | elif self.squid.personality == Personality.ADVENTUROUS: 108 | decision_weights["exploring"] *= 1.3 109 | decision_weights["approaching_rock"] *= 1.2 110 | elif self.squid.personality == Personality.GREEDY: 111 | decision_weights["eating"] *= 1.5 112 | 113 | # Introduce randomness to make the behavior more unpredictable 114 | for key in decision_weights: 115 | decision_weights[key] *= random.uniform(0.85, 1.15) 116 | 117 | # Determine the best decision based on the highest weight 118 | best_decision = max(decision_weights, key=decision_weights.get) 119 | 120 | # Implement the chosen decision 121 | if best_decision == "eating" and self.squid.get_visible_food(): 122 | closest_food = min(self.squid.get_visible_food(), 123 | key=lambda f: self.squid.distance_to(f[0], f[1])) 124 | self.squid.move_towards(closest_food[0], closest_food[1]) 125 | return "moving_to_food" 126 | elif best_decision == "approaching_rock" and not self.squid.carrying_rock: 127 | nearby_rocks = [d for d in self.squid.tamagotchi_logic.get_nearby_decorations( 128 | self.squid.squid_x, self.squid.squid_y, 150) 129 | if getattr(d, 'can_be_picked_up', False)] 130 | if nearby_rocks: 131 | self.squid.current_rock_target = random.choice(nearby_rocks) 132 | return "approaching_rock" 133 | elif best_decision == "throwing_rock" and self.squid.carrying_rock: 134 | direction = random.choice(["left", "right"]) 135 | if self.squid.throw_rock(direction): 136 | return "throwing_rock" 137 | elif best_decision == "organizing" and self.squid.should_organize_decorations(): 138 | return self.squid.organize_decorations() 139 | elif best_decision == "avoiding_threat" and self.squid.anxiety > 70: 140 | # Move away from potential threats 141 | if len(self.squid.tamagotchi_logic.poop_items) > 0: 142 | self.squid.move_erratically() 143 | return "avoiding_threat" 144 | 145 | # Default to exploration with varying patterns 146 | exploration_style = random.choice(["normal", "slow", "erratic"]) 147 | if exploration_style == "slow": 148 | self.squid.move_slowly() 149 | elif exploration_style == "erratic": 150 | self.squid.move_erratically() 151 | else: 152 | self.squid.move_randomly() 153 | 154 | return "exploring" 155 | ``` 156 | 157 | 158 | Explanation of Key Concepts: 159 | ---------------------------- 160 | 161 | * **Neural Network State:** The decision-making process relies heavily on the squid's neural network state, which is influenced by various factors such as hunger, happiness, and curiosity. This state is represented by the `brain_state` dictionary. 162 | * **Memory Influence:** Active memories affect the squid's current state, with each memory having half the weight of the current state. This influence is calculated and applied to the current state. 163 | * **Personality Modifiers:** The squid's personality (e.g., timid, adventurous, greedy) modifies the decision weights, making certain actions more or less likely based on the personality trait. 164 | * **Randomness:** Randomness is introduced to make the squid's behavior more unpredictable, simulating real-life decision-making where actions are not always deterministic. 165 | * **Decision Weights:** Each possible action has a weight calculated based on the neural state and modified by personality and randomness. The action with the highest weight is chosen. 166 | * **Extreme Conditions:** Certain conditions, such as high sleepiness, override the neural decisions to ensure the squid's well-being. 167 | -------------------------------------------------------------------------------- /Docs/Goal Neurons.md: -------------------------------------------------------------------------------- 1 | The squid has 7 interconnected Neurons but only 4 of them can be fully interacted with or edited via the brain tool: 2 | 3 | `Curiosity`, `Anxiety` and `Satisfaction` are so-called **GOAL NEURONS** which their values set and adjusted dynamically by the other neurons. 4 | They're also affected by the squid's personality, and they affect the squid's decision-making processes. 5 | 6 | The challenge is to try to keep `Satisfaction` as high as possible whilst also trying to keep `Anxiety` as low as possible - 7 | caring for the squid and giving him the correct environment in which to thrive are key to accomplishing this. A `satisified` squid is a happy squid. 8 | 9 | `Curiosity` affects how the squid will react to his environment. Squids of all personality types have naturally high curiosity. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Docs/Memory System.md: -------------------------------------------------------------------------------- 1 | # Memory System Technical Overview 2 | 3 | The `MemoryManager` class handles both short-term and long-term memories. This system allows the squid to store, retrieve, and utilize past experiences to influence its decision-making process. 4 | * The `_memory` folder holds `ShortTerm.json` and `LongTerm.json` 5 | 6 | ## Key Components 7 | 8 | 1. `MemoryManager` class 9 | 2. Short-term memory storage 10 | 3. Long-term memory storage 11 | 4. Memory persistence (JSON files) 12 | 5. Memory transfer mechanism 13 | 6. Integration with decision-making 14 | 15 | ## Detailed Breakdown 16 | 17 | ### 1. MemoryManager Class 18 | 19 | The `MemoryManager` class is the core of the memory system. It handles: 20 | 21 | - Initialization of memory storages 22 | - Loading and saving memories to/from files 23 | - Adding new memories 24 | - Retrieving memories 25 | - Transferring memories from short-term to long-term storage 26 | - Periodic memory management 27 | 28 | ```python 29 | class MemoryManager: 30 | def __init__(self): 31 | self.memory_dir = '_memory' 32 | self.short_term_file = os.path.join(self.memory_dir, 'ShortTerm.json') 33 | self.long_term_file = os.path.join(self.memory_dir, 'LongTerm.json') 34 | self.short_term_memory = self.load_memory(self.short_term_file) or [] 35 | self.long_term_memory = self.load_memory(self.long_term_file) or [] 36 | self.short_term_limit = 50 # Maximum number of short-term memories 37 | self.short_term_duration = timedelta(minutes=5) # Duration of short-term memory 38 | ``` 39 | 40 | ### 2. Short-term Memory 41 | 42 | Short-term memories are recent experiences or observations. They have a limited capacity and duration. 43 | 44 | - Stored in `self.short_term_memory` list 45 | - Limited to `self.short_term_limit` items 46 | - Expire after `self.short_term_duration` 47 | 48 | ```python 49 | def add_short_term_memory(self, category, key, value, importance=1): 50 | memory = { 51 | 'category': category, 52 | 'key': key, 53 | 'value': value, 54 | 'timestamp': datetime.now().isoformat(), 55 | 'importance': importance, 56 | 'access_count': 1 57 | } 58 | self.short_term_memory.append(memory) 59 | ``` 60 | 61 | ### 3. Long-term Memory 62 | 63 | Long-term memories are persistent and don't expire. They represent important or frequently accessed information. 64 | 65 | - Stored in `self.long_term_memory` list 66 | - No limit on the number of items 67 | - Do not expire 68 | 69 | ```python 70 | def add_long_term_memory(self, category, key, value): 71 | memory = { 72 | 'category': category, 73 | 'key': key, 74 | 'value': value, 75 | 'timestamp': datetime.now().isoformat() 76 | } 77 | self.long_term_memory.append(memory) 78 | ``` 79 | 80 | ### 4. Memory Persistence 81 | 82 | Memories are saved to and loaded from JSON files for persistence across sessions. 83 | 84 | ```python 85 | def save_memory(self, memory, file_path): 86 | with open(file_path, 'w') as file: 87 | json.dump(memory, file, indent=4, default=str) 88 | 89 | def load_memory(self, file_path): 90 | if os.path.exists(file_path): 91 | with open(file_path, 'r') as file: 92 | memory = json.loads(file.read()) 93 | for item in memory: 94 | if 'timestamp' in item: 95 | item['timestamp'] = datetime.fromisoformat(item['timestamp']) 96 | return memory 97 | return [] 98 | ``` 99 | 100 | ### 5. Memory Transfer Mechanism 101 | 102 | Short-term memories can be transferred to long-term memory based on importance and access frequency. 103 | 104 | ```python 105 | def should_transfer_to_long_term(self, memory): 106 | return ( 107 | memory['importance'] >= 7 or 108 | memory['access_count'] >= 3 or 109 | (memory['importance'] >= 5 and memory['access_count'] >= 2) 110 | ) 111 | 112 | def review_and_transfer_memories(self): 113 | for memory in list(self.short_term_memory): 114 | if self.should_transfer_to_long_term(memory): 115 | self.transfer_to_long_term_memory(memory['category'], memory['key']) 116 | elif (datetime.now() - memory['timestamp']) > self.short_term_duration: 117 | self.short_term_memory.remove(memory) 118 | ``` 119 | 120 | ### 6. Integration with Decision-making 121 | 122 | The memory system is integrated into the squid's decision-making process in the `make_decision` method: 123 | 124 | ```python 125 | def make_decision(self): 126 | # ... (other code) 127 | short_term_memories = self.memory_manager.get_all_short_term_memories('experiences') 128 | long_term_memories = self.memory_manager.get_all_long_term_memories('experiences') 129 | 130 | combined_state = current_state.copy() 131 | combined_state.update(short_term_memories) 132 | combined_state.update(long_term_memories) 133 | 134 | decision = self.tamagotchi_logic.squid_brain_window.make_decision(combined_state) 135 | # ... (rest of the decision-making process) 136 | ``` 137 | 138 | ## Key Features 139 | 140 | 1. **Dual Storage**: Separate short-term and long-term memory systems allow for different treatment of recent vs. important memories. 141 | 142 | 2. **Automatic Transfer**: Important or frequently accessed short-term memories are automatically transferred to long-term storage. 143 | 144 | 3. **Expiration**: Short-term memories expire after a set duration, preventing information overload. 145 | 146 | 4. **Persistence**: Memories are saved to disk, allowing for continuity across sessions. 147 | 148 | 5. **Categorization**: Memories are categorized (e.g., 'experiences', 'decorations'), allowing for targeted retrieval and use. 149 | 150 | 6. **Integration**: The memory system is tightly integrated with the decision-making process, allowing past experiences to influence behavior. 151 | 152 | 7. **Importance and Access Tracking**: Memories have importance levels and access counts, which influence their likelihood of being transferred to long-term storage. 153 | 154 | This memory system provides the squid with a sophisticated ability to learn from and adapt to its experiences, contributing to more complex and realistic behavior over time. 155 | -------------------------------------------------------------------------------- /Docs/Neural Learning System.html: -------------------------------------------------------------------------------- 1 | idicus: Squid Neural Learning System 2 | ======================================= 3 | 4 | The squid uses a sophisticated neural learning system based on Hebbian learning principles, summarized by the famous neuroscience axiom: "Neurons that fire together, wire together." 5 | 6 | 2\. Core Learning Mechanism 7 | --------------------------- 8 | 9 | ### 2.1 Hebbian Learning Principles 10 | 11 | The learning process occurs through the strengthening of connections between neurons based on their simultaneous activation. When two neurons are active at the same time, the connection between them becomes stronger, making future communication easier. 12 | 13 | ### 2.2 Learning Process Components 14 | 15 | * **Neuron Activation:** Neurons become active based on the squid's experiences and internal state 16 | * **Connection Strengthening:** Active neurons form stronger neural pathways 17 | * **Weight Adjustment:** Connections are assigned weights indicating their strength 18 | * **Neurogenesis:** New neurons can be created during significant learning experiences 19 | 20 | 3\. Personality-Driven Learning 21 | ------------------------------- 22 | 23 | ### 3.1 Personality Learning Modifiers 24 | 25 | #### Timid Personality 26 | 27 | * Slower, more cautious learning 28 | * Reduced connection formation rate 29 | * Increased anxiety-related neural connections 30 | * More conservative neural adaptation 31 | 32 | #### Adventurous Personality 33 | 34 | * Rapid neural connection formation 35 | * Increased learning rate 36 | * More dynamic and flexible neural networks 37 | * Quick adaptation to new experiences 38 | 39 | #### Greedy Personality 40 | 41 | * Focus on reward-related connections 42 | * Strong links between experiences and satisfaction 43 | * Prioritizes goal-oriented learning 44 | * Emphasizes immediate benefit pathways 45 | 46 | #### Stubborn Personality 47 | 48 | * Resistance to forming new connections 49 | * Maintains existing neural patterns 50 | * Slower adaptation to new experiences 51 | * Reduced learning rate for novel stimuli 52 | 53 | 4\. Neurogenesis: Creating New Neurons 54 | -------------------------------------- 55 | 56 | The squid can create new neurons through a process called neurogenesis, triggered by specific conditions: 57 | 58 | * **Novelty Exposure:** Encountering new and unusual experiences 59 | * **Sustained Stress:** Prolonged periods of high anxiety 60 | * **Positive Rewards:** Significant positive experiences 61 | 62 | ### Neurogenesis Triggers 63 | 64 | New neurons are preferentially connected to related existing neurons, creating specialized neural pathways that help the squid adapt and learn. 65 | 66 | 5\. Learning Event Tracking 67 | --------------------------- 68 | 69 | Every learning event is comprehensively logged, capturing: 70 | 71 | * Neurons involved 72 | * Connection weight changes 73 | * Personality influences 74 | * Timestamp of learning 75 | * Neurogenesis status 76 | 77 | 6\. Neural Network Evolution 78 | ---------------------------- 79 | 80 | The system continuously tracks and analyzes neural network changes, providing insights into: 81 | 82 | * Total neurons added 83 | * Connection weight modifications 84 | * Personality's impact on learning 85 | * Learning rate trends 86 | 87 | 7\. Debugging and Analysis 88 | -------------------------- 89 | 90 | Advanced logging and export features allow for detailed examination of the learning process: 91 | 92 | * Export learning logs to CSV 93 | * Capture comprehensive network states 94 | * Analyze neural network evolution 95 | 96 | ### Scientific Inspiration 97 | 98 | This learning system is inspired by biological neural networks, incorporating stochasticity and adaptive mechanisms observed in living systems. 99 | 100 | 8\. Performance and Limitations 101 | ------------------------------- 102 | 103 | * Computationally efficient neural updates 104 | * Personality-driven adaptive learning 105 | * Configurable learning parameters 106 | * Limitations in extremely complex or unpredictable environments 107 | 108 | _Dosidicus Neural Learning System - Version 1.0_ 109 | -------------------------------------------------------------------------------- /Docs/Overview.md: -------------------------------------------------------------------------------- 1 | Additional overview here: [https://github.com/ViciousSquid/Dosidicus/blob/main/Docs/Overview2.md] 2 | # Squid Brain Technical Overview 3 | 4 | 5 | 6 | ## 1. Brain Structure and State 7 | 8 | The squid's brain is represented by a set of neurons, each corresponding to a specific attribute or state. The main components are: 9 | 10 | - Basic needs: `hunger`, `happiness`, `cleanliness`, `sleepiness` 11 | - Advanced states: `satisfaction`, `anxiety`, `curiosity` 12 | 13 | These neurons are interconnected, and their states influence each other based on weighted connections. 14 | 15 | ## 2. Neural Network Implementation 16 | 17 | The brain is implemented as a simple neural network (single layer perceptron with 7 inputs) 18 | 19 | - Neurons are represented as nodes with activation values (`0-100` for most states). 20 | - Connections between neurons have weights (`-1 to 1`) that determine how much one neuron's state influences another. 21 | - The network is updated periodically (every second in game time) to recalculate neuron states based on internal and external factors. 22 | 23 | ## 3. Autonomy and Decision Making 24 | 25 | The squid has a moderate level of autonomy: 26 | 27 | - Movement: The squid moves autonomously based on its current state and environmental factors. 28 | - Sleep cycles: The squid will autonomously go to sleep when its sleepiness reaches 100, and wake up when it reaches 0. 29 | - Eating: The squid will pursue and eat food items when they're available and it's hungry. 30 | - Pooping: The squid will autonomously create poop items based on its digestive cycle. 31 | 32 | However, the squid cannot autonomously feed itself or clean its environment, requiring player intervention for these actions. 33 | 34 | ## 4. State Updates and Interactions 35 | 36 | The squid's brain state is updated regularly: 37 | 38 | - Basic needs (`hunger`, `sleepiness`, `happiness`, `cleanliness`) change over time. 39 | - Health decreases if the squid is sick or if happiness and cleanliness are very low. 40 | - Advanced states (`satisfaction`, `anxiety`, `curiosity`) are calculated based on the basic needs and environmental factors. 41 | - Binary states are updated based on specific conditions or player actions. 42 | 43 | ## 5. Player Interaction and Care 44 | 45 | The player needs to care for the squid in several ways: 46 | 47 | 1. Feeding: Player must spawn food items for the squid to eat, managing its hunger. 48 | 2. Cleaning: Player must clean the environment to maintain cleanliness and prevent sickness. 49 | 3. Medicine: If the squid becomes sick, the player must administer medicine. 50 | 4. Entertainment: Player can play Rock Paper Scissors with the squid and can place decorations which affect mood/behaviours 51 | 52 | ## 6. Environmental Factors 53 | 54 | The squid's brain also responds to environmental factors: 55 | 56 | - Presence of food items influences the `pursuing_food` state. 57 | - Cleanliness of the environment affects the squid's `cleanliness` state. 58 | 59 | ## 7. Learning and Adaptation 60 | 61 | The current implementation has limited learning capabilities: 62 | 63 | - The weights between neurons can change slightly over time, potentially allowing for some adaptation. 64 | - A Hebbian learning mechanism is implemented, allowing for strengthening of associations between frequently co-activated neurons. 65 | 66 | ## 8. Visualization and Debugging 67 | 68 | The game includes a brain visualization window: 69 | 70 | - Displays the current state of all neurons. 71 | - Shows connections between neurons and their weights. 72 | - Allows for real-time monitoring of the squid's internal state. 73 | 74 | ## 9. Save and Load Functionality 75 | 76 | The brain state can be saved and loaded: 77 | 78 | - All neuron states and connection weights are stored. 79 | - This allows for persistence of the squid's "personality" across game sessions. 80 | 81 | ## 10. Future Expansion Possibilities 82 | 83 | The current brain implementation allows for potential expansions: 84 | 85 | - More complex decision-making algorithms. 86 | - Introduction of memory or long-term learning. 87 | - More sophisticated environmental interactions. 88 | - Implementation of mood or personality traits that influence behavior. 89 | 90 | In conclusion, the squid's brain provides a balance between autonomy and the need for player care. It simulates a simple but effective AI that responds to both its internal state and external stimuli, creating a dynamic and engaging pet care experience. 91 | -------------------------------------------------------------------------------- /Docs/Overview2.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Neural Network Architecture 4 | =========================== 5 | 6 | Core Structure 7 | -------------- 8 | 9 | * **7 Primary Neurons**: 10 | * Circular (Basic Needs): `hunger`, `happiness`, `cleanliness`, `sleepiness` 11 | * Square (Complex States): `satisfaction`, `anxiety`, `curiosity` 12 | 13 | Connection System 14 | ----------------- 15 | 16 | # Example weight initialization 17 | self.weights = { 18 | ("hunger", "happiness"): random.uniform(-1, 1), 19 | ("cleanliness", "anxiety"): -0.5, # Pre-wired negative correlation 20 | # ...all possible pairwise connections... 21 | } 22 | 23 | Visualization Features 24 | ---------------------- 25 | 26 | ![image](https://github.com/user-attachments/assets/3cc66fc2-6c0d-40dd-aee9-1b40f3a7d52f) 27 | 28 | 29 | Key Methods 30 | ----------- 31 | 32 | def update_state(self, new_state): 33 | # Only allow certain states to be modified 34 | for key in ['hunger', 'happiness', 'cleanliness', 'sleepiness']: 35 | if key in new_state: 36 | self.state[key] = new_state[key] 37 | self.update_weights() 38 | 39 | Weight Dynamics 40 | --------------- 41 | 42 | * **Random Drift**: 43 | 44 | self.weights[conn] += random.uniform(-0.1, 0.1) # Small random changes 45 | 46 | * **Bounded Values**: Hard-limited to \[-1, 1\] range 47 | * **Frozen States**: Can pause learning with `freeze_weights()` 48 | 49 | 50 | 51 | Hebbian Learning System 52 | ======================= 53 | 54 | Core Principle 55 | -------------- 56 | 57 | "Neurons that fire together, wire together" 58 | 59 | Learning Algorithm 60 | ------------------ 61 | 62 | def perform_hebbian_learning(self): 63 | active_neurons = [ 64 | n for n, v in self.state.items() 65 | if (isinstance(v, (int, float)) and v > 50) 66 | or (isinstance(v, bool) and v) 67 | ] 68 | 69 | for i, j in random.sample( 70 | [(a,b) for a in active_neurons for b in active_neurons if a != b], 71 | min(5, len(active_neurons)) 72 | ): 73 | self.update_connection(i, j, self.state[i], self.state[j]) 74 | 75 | Update Rules 76 | ------------ 77 | 78 | 1. **Basic Hebbian**: 79 | 80 | weight_change = 0.01 * (value1/100) * (value2/100) 81 | 82 | 2. **Personality Modifiers**: 83 | 84 | if self.personality == Personality.GREEDY: 85 | weight_change *= 1.5 # Faster learning for food-related connections 86 | 87 | 88 | Special Cases 89 | ------------- 90 | ![image](https://github.com/user-attachments/assets/df917330-413e-4c36-965b-3bf5e6e64e13) 91 | 92 | 93 | Memory System 94 | ============= 95 | 96 | Two-Tiered Architecture 97 | ----------------------- 98 | ![image](https://github.com/user-attachments/assets/492df054-a203-49d2-b2ab-f0ce298b0886) 99 | 100 | 101 | 102 | Memory Format 103 | ------------- 104 | 105 | { 106 | "category": "food", 107 | "value": "Ate sushi: Hunger-20, Happiness+10", 108 | "timestamp": 1625097600, 109 | "weight": 0.85 110 | } 111 | 112 | Key Operations 113 | -------------- 114 | 115 | # Adding memory 116 | memory_manager.add_short_term_memory( 117 | category="interaction", 118 | key="rock_push", 119 | value={"satisfaction": +8, "happiness": +5} 120 | ) 121 | 122 | # Consolidation 123 | if memory['weight'] > 0.7: # Important memory 124 | memory_manager.transfer_to_long_term_memory(memory) 125 | 126 | Emergent Behaviors 127 | ================== 128 | 129 | Personality-Driven Emergence 130 | ---------------------------- 131 | ![image](https://github.com/user-attachments/assets/e490e9f8-886b-4142-8d09-bb43fc88c762) 132 | 133 | 134 | 135 | Observed Emergent Patterns 136 | -------------------------- 137 | 138 | 1. **Decoration Preferences**: 139 | * Squids develop favorite decoration spots through reinforcement 140 | * Example: Timid squids form strong plant-anxiety reduction associations 141 | 2. **Circadian Rhythms**: 142 | 143 | # Emergent sleep-wake cycle 144 | if (sleepiness > 90 and 145 | random.random() < hunger/100): # Hunger affects sleep resistance 146 | self.go_to_sleep() 147 | 148 | 3. **Learned Phobias**: 149 | * Negative events create lasting anxiety connections 150 | * After 3+ startle events: `anxiety-cleanliness` weight <-0.5 151 | 152 | Measurement System 153 | ------------------ 154 | 155 | # Behavior scoring metric 156 | def calculate_emergence_score(): 157 | return sum( 158 | abs(w) for w in self.weights.values() 159 | if w not in [-1, 0, 1] # Exclude default/min/max 160 | ) / len(self.weights) 161 | 162 | Technical Limitations 163 | ===================== 164 | 165 | 1. **Scalability**: 166 | * Current O(n²) connections (49 for 7 neurons) 167 | * Practical limit ~15 neurons (225 connections) 168 | 2. **Memory Bottlenecks**: 169 | * QGraphicsScene performance degrades with >1000 items 170 | * Memory recall takes O(n) time 171 | 3. **Learning Constraints**: 172 | * No backpropagation (pure Hebbian) 173 | * Catastrophic forgetting possible 174 | 175 | -------------------------------------------------------------------------------- /Docs/Personalities.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | There are seven different squid personalities that affect their needs and how they behave: 4 | 5 | * `Timid`: Higher chance of becoming anxious 6 | * `Adventurous`: Increased curiosity and exploration 7 | * `Lazy`: Slower movement and energy consumption 8 | * `Energetic`: Faster movement and higher activity levels 9 | * `Introvert`: Prefers solitude and quiet environments 10 | * `Greedy`: More focused on food and resources 11 | * `Stubborn`: Fussy and difficult 12 | 13 | One of these is randomly chosen at the start of a new game, immediately after the egg hatches. 14 | 15 | A personality type can be forced at launch using the `-p` flag followed by the personality name above (example: `main.py -p lazy`) 16 | 17 | Each 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. 18 | 19 | Here's a description of the different personality types and their corresponding behaviors in the game: 20 | 21 | ### `Timid` Personality: 22 | 23 | * Tendency to become anxious, especially when not near plants. 24 | * Moves slowly and prefers quiet, solitary environments. 25 | * Curiosity level is lower than other personalities. 26 | * Has a higher chance of becoming startled by decorations or other environmental factors. 27 | 28 | 29 | ### `Adventurous` Personality: 30 | 31 | * Curious and exploratory, with a higher chance of entering the "curious" state. 32 | * Moves faster and is more active compared to other personalities. 33 | * Curiosity level is higher, leading to increased exploration and interaction with the environment. 34 | 35 | 36 | ### `Lazy` Personality: 37 | 38 | * Moves and consumes energy at a slower pace. 39 | * Takes more time to fulfill their needs, such as eating and sleeping. 40 | * May be less responsive to environmental changes or stimuli. 41 | 42 | 43 | ### `Energetic` Personality: 44 | 45 | * Moves and acts at a faster pace. 46 | * Tends to have higher activity levels and may expend energy more quickly. 47 | * May be more prone to restlessness or agitation. 48 | 49 | 50 | ### `Introvert` Personality: 51 | 52 | * Prefers solitary environments and is content when alone. 53 | * May become unhappy or anxious when forced to interact with the environment or decorations. 54 | * Curiosity level is balanced, not too high or too low. 55 | 56 | 57 | ### `Greedy` Personality: 58 | 59 | * Highly focused on food and resources, becoming anxious and hungry when those needs are not met. 60 | * Curiosity level may be higher, leading to more exploration, but this is primarily driven by the desire for food. 61 | * May become more aggressive or assertive in obtaining food or resources. 62 | 63 | 64 | ### `Stubborn` Personality: 65 | 66 | * Only eats its favorite food (sushi), often refusing to consume any other type of food (cheese). 67 | * Displays the message "Fussy squid does not like that type of food!" when presented with non-favorite food. 68 | * Has a chance of refusing to sleep when its sleepiness is high, instead moving randomly. 69 | * Moves slowly and stubbornly when not actively searching for its favorite food. 70 | * Prioritizes sushi over cheese in its vision cone when searching for food. 71 | -------------------------------------------------------------------------------- /Docs/Simulating Awareness.md: -------------------------------------------------------------------------------- 1 | ### Simulating Awareness 2 | 3 | The squid has a form of awareness of its environment and surroundings, implemented through a combination of sensory inputs, memory, and neural network processing . Here's how it works: 4 | 5 | ### Environmental Awareness: 6 | **Sensory Inputs:** The squid tracks states like hunger, cleanliness, sleepiness, and happiness which are influenced by its environment. 7 | 8 | **Memory System:** It maintains short-term and long-term memories of interactions with objects (food, decorations). 9 | 10 | **Visual Detection:** It can detect nearby objects (food, poop, decorations) and respond to them. 11 | 12 | **Personality Modifiers:** Different personalities change how the squid perceives and reacts to its environment (e.g., timid squids are more anxious). 13 | 14 | ### Hebbian Learning and Neurogenesis Interaction: 15 | ## Hebbian Learning: 16 | 17 | Strengthens connections between co-active neurons ("neurons that fire together, wire together"). 18 | 19 | Implemented in `strengthen_connection()` where weights between active neurons are increased. 20 | 21 | Goal-oriented learning reinforces specific behaviors (e.g., eating increases hunger-satisfaction connections). 22 | 23 | ## Neurogenesis: 24 | 25 | Creates new neurons in response to triggers (novelty, stress, rewards). 26 | 27 | New neurons are initialized with connections to existing ones (`create_new_neuron()`). 28 | 29 | Temporarily boosts learning rate (`neurogenesis_learning_boost`). 30 | 31 | ## How They Work Together: 32 | 33 | Hebbian learning adjusts the weights of existing connections based on experience. 34 | 35 | Neurogenesis adds new neurons when the squid encounters novel, stressful, or rewarding situations, expanding the network's capacity. 36 | 37 | The new neurons then participate in Hebbian learning, allowing the network to adapt to new patterns. 38 | 39 | 40 | 41 | ## Impact on the Network: 42 | 43 | **Enhancements:** 44 | 45 | New neurons add capacity to learn novel patterns (e.g., a "novelty" neuron helps recognize new objects). 46 | 47 | Stress-induced neurons may improve threat response. 48 | 49 | Reward-based neurons reinforce positive behaviors. 50 | 51 | ## Potential Degradation: 52 | 53 | Poorly integrated neurons could destabilize the network (though weights are bounded). 54 | 55 | The `weight_decay` mechanism prevents unchecked growth. 56 | 57 | ## Key Enhancements: 58 | **Adaptability:** New neurons allow the squid to develop specialized responses (e.g., a "rock_interaction" neuron if rocks are frequently encountered). 59 | 60 | **Memory Integration:** Neurogenesis is tied to memory, so new neurons reflect long-term experiences. 61 | 62 | **Personality Influence:** Traits like "adventurous" increase curiosity, making neurogenesis more likely. 63 | 64 | ## Example Workflow: 65 | The squid encounters a new plant (novelty_exposure increases). 66 | 67 | After 3+ encounters, neurogenesis triggers a "novel_plant" neuron. 68 | 69 | Hebbian learning strengthens connections between this neuron and "curiosity"/"satisfaction". 70 | 71 | Future plant interactions activate this pathway, making the squid more likely to approach plants. 72 | 73 | This creates a dynamic system where the squid's "awareness" evolves based on its experiences, personality, and environment. 74 | -------------------------------------------------------------------------------- /Docs/Squid Class.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Squid Class Technical Overview 4 | 5 | The Squid class represents a squid with complex behaviors and personality traits. 6 | 7 | ## Class Overview 8 | 9 | The Squid class is a PyQt5-based implementation that handles: 10 | 11 | * Visual representation and animation 12 | * Movement and navigation 13 | * Decision making based on needs and personality 14 | * Interaction with environment (food, decorations) 15 | * State management (hunger, happiness, etc.) 16 | 17 | ## Core Attributes 18 | 19 | | Attribute | Type | Description | 20 | | --- | --- | --- | 21 | | ui | UserInterface | Reference to the main UI component | 22 | | tamagotchi\_logic | TamagotchiLogic | Reference to the main game logic | 23 | | memory\_manager | MemoryManager | Handles memory storage and retrieval | 24 | | mental\_state\_manager | MentalStateManager | Manages emotional states | 25 | | personality | Personality (Enum) | The squid's personality type (TIMID, GREEDY, STUBBORN) | 26 | | health, hunger, happiness, etc. | int (0-100) | Various state attributes | 27 | 28 | ## Key Methods 29 | 30 | ### Initialization and Setup 31 | 32 | ``` 33 | def __init__(self, user_interface, tamagotchi_logic=None, personality=None, neuro_cooldown=None): 34 | # Initializes the squid with default values and sets up UI components 35 | ``` 36 | 37 | ### Movement and Navigation 38 | 39 | ``` 40 | def move_squid(self): 41 | # Handles the squid's movement based on current direction and animation speed 42 | ``` 43 | 44 | ``` 45 | def move_towards(self, x, y): 46 | # Moves the squid towards a specific coordinate 47 | ``` 48 | 49 | ``` 50 | def change_direction(self): 51 | # Randomly changes the squid's movement direction 52 | ``` 53 | 54 | ### Food Interactions 55 | 56 | ``` 57 | def eat(self, food_item): 58 | # Handles eating behavior and applies effects 59 | ``` 60 | 61 | ``` 62 | def get_visible_food(self): 63 | # Returns food items within the squid's vision cone 64 | ``` 65 | 66 | ``` 67 | def is_in_vision_cone(self, x, y): 68 | # Determines if a point is within the squid's field of view 69 | ``` 70 | 71 | ### Environment Interaction 72 | 73 | ``` 74 | def push_decoration(self, decoration, direction): 75 | # Pushes a decoration with animation 76 | ``` 77 | 78 | ``` 79 | def organize_decorations(self): 80 | # Hoards nearby decorations (personality-specific behavior) 81 | ``` 82 | 83 | ### State Management 84 | 85 | ``` 86 | def go_to_sleep(self): 87 | # Puts the squid to sleep 88 | ``` 89 | 90 | ``` 91 | def wake_up(self): 92 | # Wakes the squid up 93 | ``` 94 | 95 | ``` 96 | def load_state(self, state): 97 | # Loads a saved state 98 | ``` 99 | 100 | ## Detailed Method Explanations 101 | 102 | 103 | ### `move_squid()` 104 | 105 | Handles the squid's movement with several key features: 106 | 107 | * Respects animation speed settings 108 | * Handles sleeping state differently (moves downward) 109 | * Implements food pursuit behavior when food is visible 110 | * Changes direction at screen boundaries 111 | * Updates visual representation (animation frames) 112 | 113 | ### `eat()` 114 | 115 | Complex food consumption behavior that: 116 | 117 | * Applies different effects based on food type 118 | * Triggers personality-specific reactions 119 | * Starts digestion/poop timer 120 | * Manages memory of eating events 121 | * Updates multiple status attributes 122 | 123 | ## Personality System 124 | 125 | The squid implements distinct personality types that modify behavior: 126 | 127 | * **TIMID**: More cautious, avoids risks 128 | * **GREEDY**: Focused on food, eats more 129 | * **STUBBORN**: Prefers specific foods, resistant to change 130 | 131 | 132 | ## Vision System 133 | 134 | The squid has a limited field of view implemented with: 135 | 136 | * 80-degree view cone (`view_cone_angle`) 137 | * Visualization capability (`toggle_view_cone()`) 138 | * Food prioritization within view (`get_visible_food()`) 139 | 140 | ## Animation System 141 | 142 | The visual representation uses: 143 | 144 | * Frame-based animation (current_frame) 145 | * Direction-specific sprites 146 | * Sleeping state animation 147 | * Adjustable animation speed 148 | 149 | ## State Management 150 | 151 | The squid maintains numerous state variables: 152 | 153 | | Variable | Range | Description | 154 | | --- | --- | --- | 155 | | hunger | 0-100 | Need for food | 156 | | happiness | 0-100 | Overall contentment | 157 | | cleanliness | 0-100 | Clean/dirty state | 158 | | sleepiness | 0-100 | Need for sleep | 159 | | satisfaction | 0-100 | Recent positive experiences | 160 | | anxiety | 0-100 | Stress level | 161 | | curiosity | 0-100 | Desire to explore | 162 | 163 | ## Interaction System 164 | 165 | The squid interacts with its environment through: 166 | 167 | * Food consumption (`eat()`) 168 | * Decoration manipulation (`push_decoration()`) 169 | * Personality-specific behaviors (`organize_decorations()`) 170 | 171 | 172 | **Note:** The Squid class demonstrates a sophisticated virtual pet implementation with complex behavior patterns, personality-driven decision making, and rich environmental interactions. 173 | -------------------------------------------------------------------------------- /Docs/Tamagotchi Class.md: -------------------------------------------------------------------------------- 1 | 2 | # TamagotchiLogic Technical Documentation 3 | 4 | ## Class Overview 5 | 6 | The `TamagotchiLogic` class is the core game logic controller for the Dosidicus digital pet application. It manages: 7 | 8 | * Squid behavior and state management 9 | * Environment interactions (food, decorations, cleanliness) 10 | * Game mechanics and progression 11 | * Brain/neural network integration 12 | * Save/load functionality 13 | 14 | **Key Relationships:** TamagotchiLogic coordinates between the UI (PyQt5), Squid object, and BrainWindow to create a cohesive digital pet experience. 15 | 16 | ## Initialization and Setup 17 | 18 | ### Constructor (`__init__`) 19 | 20 | Initializes the game controller with references to UI, Squid, and BrainWindow components. 21 | 22 | `def __init__(self, user_interface, squid, brain_window):` 23 | 24 | ### Key Initialization Steps: 25 | 26 | 1. Sets up neurogenesis triggers tracking 27 | 2. Initializes Hebbian learning system 28 | 3. Configures mental state cooldowns 29 | 4. Sets up game timers and simulation speed 30 | 5. Connects UI actions to methods 31 | 6. Initializes statistics window 32 | 33 | **Important:** The constructor ensures all required components are properly connected before loading game state. 34 | 35 | ## Core Functions 36 | 37 | ### Simulation Update (`update_simulation`) 38 | 39 | The main game loop that updates all systems: 40 | 41 | `def update\_simulation(self):` 42 | 43 | * Handles object movement (food, poop) 44 | * Updates squid position and state 45 | * Manages mental states (startle, curiosity) 46 | * Tracks neurogenesis triggers 47 | * Updates brain state 48 | 49 | ### State Management (`update_statistics`) 50 | 51 | Updates all squid attributes and game state: 52 | 53 | `def update\_statistics(self):` 54 | 55 | * Adjusts hunger, happiness, cleanliness 56 | * Manages sickness state 57 | * Updates satisfaction, anxiety, curiosity 58 | * Handles sleep transitions 59 | * Calculates points 60 | 61 | ### Brain Integration (`update_squid_brain`) 62 | 63 | Packages squid state for brain visualization: 64 | 65 | `def update\_squid\_brain(self):` 66 | 67 | * Collects all squid attributes 68 | * Includes position and direction 69 | * Adds personality information 70 | * Sends to BrainWindow for visualization 71 | 72 | ## Interaction Functions 73 | 74 | ### Feeding System 75 | 76 | | Function | Description | 77 | | --- | --- | 78 | | `feed_squid` | Triggers food spawning (called from UI) | 79 | | `spawn_food` | Creates food items (sushi or cheese) | 80 | | `move_foods` | Handles food item movement | 81 | 82 | ### Cleaning System 83 | 84 | | Function | Description | 85 | | --- | --- | 86 | | `clean_environment` | Initiates cleaning process | 87 | | `update_cleaning` | Animates cleaning line and removes items | 88 | | `finish_cleaning` | Completes cleaning and updates stats | 89 | 90 | ### Medical System 91 | 92 | `def give\_medicine(self):` 93 | 94 | Handles medicine administration with visual effects: 95 | 96 | * Cures sickness 97 | * Reduces happiness 98 | * Increases sleepiness 99 | * Shows needle animation 100 | * Forces squid to sleep 101 | 102 | ## Mental State Management 103 | 104 | ### Startle System 105 | 106 | | Function | Description | 107 | | --- | --- | 108 | | `check_for_startle` | Determines if squid should be startled | 109 | | `startle_squid` | Triggers startle state and effects | 110 | | `end_startle` | Returns squid to normal state | 111 | 112 | ### Curiosity System 113 | 114 | | Function | Description | 115 | | --- | --- | 116 | | `check_for_curiosity` | Determines if squid becomes curious | 117 | | `make_squid_curious` | Triggers curious state | 118 | | `curious_interaction` | Handles interactions while curious | 119 | 120 | ### Neurogenesis Tracking 121 | 122 | `def track\_neurogenesis\_triggers(self):` 123 | 124 | Manages counters for brain development triggers: 125 | 126 | * Novel object encounters 127 | * High stress cycles 128 | * Positive outcomes 129 | 130 | ## Save/Load System 131 | 132 | ### Save Function 133 | 134 | `def save\_game(self, squid, tamagotchi\_logic, is\_autosave=False):` 135 | 136 | Serializes game state including: 137 | 138 | * Squid attributes 139 | * Game logic state 140 | * Decorations 141 | * Brain state 142 | * Memory systems 143 | 144 | ### Load Function 145 | 146 | `def load\_game(self):` 147 | 148 | Restores game state from save file: 149 | 150 | * Squid attributes 151 | * Brain connections 152 | * Memories 153 | * Decorations 154 | * Game state 155 | 156 | **Autosave:** Configured to save every 5 minutes via `start_autosave` and `autosave` methods. 157 | 158 | ## Utility Functions 159 | 160 | ### Game Management 161 | 162 | | Function | Description | 163 | | --- | --- | 164 | | `game_over` | Handles end-game state | 165 | | `reset_game` | Resets all game state | 166 | 167 | ### UI Integration 168 | 169 | | Function | Description | 170 | | --- | --- | 171 | | `show_message` | Displays messages to user | 172 | | `handle_window_resize` | Adjusts UI elements on resize | 173 | 174 | ### Simulation Control 175 | 176 | | Function | Description | 177 | | --- | --- | 178 | | `set_simulation_speed` | Adjusts game speed (0-4x) | 179 | | `update_timers` | Adjusts timer intervals based on speed | 180 | 181 | ## Conclusion 182 | 183 | The `TamagotchiLogic` class provides a comprehensive framework for managing all aspects of the Dosidicus digital pet simulation. Its key strengths include: 184 | 185 | * Tight integration with neural network visualization 186 | * Sophisticated mental state management 187 | * Robust save/load system 188 | * Flexible simulation speed control 189 | * Extensive interaction systems 190 | 191 | **Performance Note:** The class manages multiple QTimer instances which must be properly stopped/started during speed changes and game state transitions. 192 | -------------------------------------------------------------------------------- /Docs/multiplayer/squid_multiplayer_autopilot.md: -------------------------------------------------------------------------------- 1 | ### Remote Squid Control 2 | 3 | (`squid_multiplayer_autopilot.py`) 4 | 5 | When your squid visits another instance, it's not directly controlled by the other player. Instead, the `RemoteSquidController` class in `squid_multiplayer_autopilot.py` takes over. 6 | This is an AI autopilot that simulates your squid's behavior according to predefined patterns (exploring, seeking food, interacting with objects, etc.). The controller makes decisions based on: 7 | 8 | The initial state of your squid when it crossed the boundary 9 | A random "personality" for the visit 10 | The environment of the other tank (available food, rocks, etc.) 11 | 12 | The flow works like this: 13 | 14 | Your squid hits a boundary and sends a `squid_exit` message 15 | The other instance receives this message and: 16 | 17 | Creates a visual representation of your squid 18 | Creates a RemoteSquidController to manage its behavior 19 | 20 | 21 | The autopilot controls your squid in the other tank 22 | After a random amount of time (or when the controller decides it's met its goals like stealing rocks), it initiates a return 23 | When your squid returns, any experiences or stolen items are synchronized back 24 | 25 | This autopilot system allows for autonomous interaction between instances without requiring direct player control. Your squid effectively has a "life of its own" while visiting other tanks. 26 | 27 | 28 | # RemoteSquidController Autopilot System 29 | 30 | The `RemoteSquidController` class in `squid_multiplayer_autopilot.py` implements an autopilot system that governs the behavior of remote squids in a multiplayer Tamagotchi-like game. These squids are visiting from other players' game instances and operate autonomously in a foreign environment. The autopilot manages their movement, interactions, and decision-making, simulating natural squid behavior while allowing for unique actions like stealing rocks. Below is a detailed explanation of how the autopilot works. 31 | 32 | --- 33 | 34 | ## Overview 35 | 36 | The `RemoteSquidController` is instantiated for each remote squid entering a player's game instance. It uses a state machine to control the squid's behavior, transitioning between states like exploring, feeding, interacting, and returning home. The autopilot relies on the game’s graphics scene to detect objects (e.g., food, rocks) and uses predefined rules to decide actions based on probabilities, timers, and environmental conditions. 37 | 38 | **Key features:** 39 | - **State-based behavior**: The squid operates in one of four states: `exploring`, `feeding`, `interacting`, or `returning`. 40 | - **Autonomous decision-making**: Decisions are made at regular intervals (every 0.5 seconds) with some randomness for natural movement. 41 | - **Object interaction**: Squids can eat food, interact with rocks, or steal rocks to bring back to their home instance. 42 | - **Time-limited visits**: Squids stay for a random duration (1–3 minutes) before returning home. 43 | - **Debug mode**: Provides verbose logging for troubleshooting. 44 | 45 | --- 46 | 47 | ## Key Components and Initialization 48 | 49 | When a remote squid enters a foreign instance, the `RemoteSquidController` is initialized with: 50 | - **`squid_data`**: A dictionary containing the squid’s initial state (e.g., position, direction, color, hunger, happiness). 51 | - **`scene`**: The PyQt5 graphics scene, used to detect and interact with objects like food and rocks. 52 | - **`debug_mode`**: A boolean flag for enabling detailed logs (default: `False`). 53 | 54 | **During initialization:** 55 | - The squid’s data is copied to avoid reference issues. 56 | - Default window dimensions (1280x900) are set if not provided. 57 | - A random visit duration (`max_time_away`, 60–180 seconds) is assigned. 58 | - The squid starts in the `exploring` state, with a random number of rocks it can steal (1–3). 59 | - Movement parameters are set: speed (`move_speed = 5`), direction change probability (`direction_change_prob = 0.02`), and decision interval (`decision_interval = 0.5` seconds). 60 | 61 | **Example debug output at initialization:** 62 | ``` 63 | [AutoPilot] Initialized for squid at (x, y) 64 | [AutoPilot] Will return home after 120 seconds 65 | [AutoPilot] Home direction: left 66 | ``` 67 | 68 | --- 69 | 70 | ## State Machine and Behavior 71 | 72 | The autopilot operates using a state machine, with the `update` method driving behavior updates at regular intervals. The squid makes decisions every 0.5 seconds, but continues moving between decisions to maintain smooth motion. The states and their behaviors are: 73 | 74 | ### 1. Exploring (`exploring`) 75 | 76 | - **Purpose**: The squid moves randomly to explore the environment. 77 | - **Behavior**: 78 | - Moves in the current direction (left, right, up, or down) at a speed of 5 pixels per update. 79 | - Has a 2% chance (`direction_change_prob`) to randomly change direction, creating natural wandering. 80 | - Periodically checks for nearby food (10% chance) or rocks (5% chance) within detection ranges (300 pixels for food, 200 for rocks). 81 | - If food is found, transitions to `feeding` state; if a rock is found, transitions to `interacting` state. 82 | - **Implementation**: The `explore` method handles movement and object detection using `find_nearby_food` and `find_nearby_rock`. 83 | 84 | ### 2. Feeding (`feeding`) 85 | 86 | - **Purpose**: The squid moves toward and consumes food. 87 | - **Behavior**: 88 | - Targets the closest food item found during exploration. 89 | - Moves toward the food’s position using `move_toward`, prioritizing the larger axis (horizontal or vertical) for direction. 90 | - If the squid is within 50 pixels of the food, it “eats” it: 91 | - Removes the food from the scene. 92 | - Increments `food_eaten_count`. 93 | - Reduces hunger (`hunger - 15`) and increases happiness (`happiness + 10`). 94 | - If the food is no longer valid (e.g., eaten by another squid), reverts to `exploring`. 95 | - **Implementation**: The `seek_food` method manages movement and eating, with `eat_food` handling consumption. 96 | 97 | ### 3. Interacting (`interacting`) 98 | 99 | - **Purpose**: The squid interacts with rocks, with a chance to steal them. 100 | - **Behavior**: 101 | - Targets the closest rock found during exploration. 102 | - Moves toward the rock using `move_toward`. 103 | - If within 50 pixels, the squid interacts: 104 | - Increments `rock_interaction_count` and boosts happiness (`happiness + 5`). 105 | - If the rock is locally owned (not remote) and the squid hasn’t reached its stealing limit (`max_rocks_to_steal`), it has a 40% chance to steal the rock: 106 | - Sets `carrying_rock = True` and `stealing_phase = True`. 107 | - Hides the rock in the scene and increments `rocks_stolen`. 108 | - If the stealing quota is met, transitions to `returning`. 109 | - After interaction (or if no steal occurs), reverts to `exploring`. 110 | - **Implementation**: The `interact_with_object` method handles movement, interaction, and stealing logic. 111 | 112 | ### 4. Returning (`returning`) 113 | 114 | - **Purpose**: The squid heads back to its home instance. 115 | - **Behavior**: 116 | - Triggered when the visit duration (`time_away`) exceeds `max_time_away` or the stealing quota is met. 117 | - Determines the home direction using `determine_home_direction` (opposite of entry direction or closest boundary if unknown). 118 | - Moves toward the boundary corresponding to `home_direction` (e.g., left edge for `left`) with slight randomness to avoid straight-line movement. 119 | - Tracks distance traveled (`distance_traveled += move_speed`). 120 | - When within 20 pixels of the boundary (`is_at_boundary`), the squid is considered to have exited, and the controller logs a summary of activities (food eaten, rocks interacted with, rocks stolen, distance traveled). 121 | - **Implementation**: The `return_home` method manages movement to the boundary, with `is_at_boundary` checking for exit conditions. 122 | 123 | --- 124 | 125 | ## Movement Mechanics 126 | 127 | The autopilot uses two primary movement methods: 128 | - **`move_in_direction(direction)`**: Moves the squid in the specified direction (left, right, up, down) by `move_speed` (5 pixels), ensuring it stays within window bounds (10 pixels from edges). 129 | - **`move_toward(target_x, target_y)`**: Moves toward a target position by prioritizing the larger axis (horizontal or vertical) and calling `move_in_direction`. 130 | 131 | Movement is smooth because the squid continues moving between decision intervals, only changing direction or state during decision points. 132 | 133 | --- 134 | 135 | ## Object Detection 136 | 137 | The autopilot interacts with the game’s graphics scene to detect objects: 138 | - **`find_nearby_food`**: 139 | - Scans the scene for food items (identified by filenames containing “food,” “sushi,” or “cheese” or a `category` of “food”). 140 | - Returns the closest food within 300 pixels, if any. 141 | - **`find_nearby_rock`**: 142 | - Scans for rocks (identified by `category` of “rock” or “rock” in filename). 143 | - Returns the closest rock within 200 pixels, if any. 144 | - **Vision Range**: The `is_in_vision_range` method checks if an item is within 800 pixels, though this is less frequently used. 145 | 146 | These methods rely on `get_food_items_from_scene` and `get_rock_items_from_scene`, which filter scene items based on attributes like `filename` or `category`. 147 | 148 | --- 149 | 150 | ## Rock Stealing Mechanic 151 | 152 | A unique feature is the ability to steal rocks: 153 | - The squid is assigned a random stealing limit (`max_rocks_to_steal`, 1–3) at initialization. 154 | - During `interacting`, if a rock is local (not remote) and the stealing limit isn’t reached, there’s a 40% chance to steal: 155 | - The rock is hidden (`setVisible(False)`), and `rocks_stolen` is incremented. 156 | - The squid’s `carrying_rock` flag is set, and its status is updated to “stealing rock.” 157 | - If the stealing quota is met, the squid immediately transitions to `returning`. 158 | - Stolen rocks are later recreated in the squid’s home instance (handled by `main.py`’s `create_stolen_rocks`). 159 | 160 | --- 161 | 162 | ## Time Management and Returning Home 163 | 164 | - The squid tracks `time_away` (seconds spent in the foreign instance) using `delta_time` calculated in `update`. 165 | - When `time_away > max_time_away`, the squid transitions to `returning`. 166 | - The `determine_home_direction` method sets the exit direction: 167 | - Uses the opposite of the entry direction (e.g., entered from left → exit via right). 168 | - If entry direction is unknown, chooses the closest boundary based on the squid’s position. 169 | - Upon reaching the boundary, the controller logs a summary via `get_summary`, which includes: 170 | - Time away 171 | - Food eaten 172 | - Rock interactions 173 | - Rocks stolen 174 | - Distance traveled 175 | - Final state 176 | 177 | --- 178 | 179 | ## Integration with `main.py` 180 | 181 | The autopilot is tightly integrated with the `MultiplayerPlugin` in `main.py`: 182 | - **Instantiation**: When a squid exits another instance (via a `network_squid_exit` message), `main.py` creates a `RemoteSquidController` in `_setup_controller_immediately` or `_process_pending_controller_creations`. 183 | - **Updates**: The `update_remote_controllers` method in `main.py` calls the controller’s `update` method every 50ms (20 FPS), passing `delta_time` for smooth movement. 184 | - **Visuals**: The controller updates the squid’s position in `squid_data`, and `main.py` synchronizes the visual representation (`remote_squids[node_id]['visual']`) with this data. 185 | - **Return Handling**: When a squid reaches a boundary, `main.py`’s `handle_remote_squid_return` triggers a fade-out animation and sends a `squid_return` message with the activity summary. The home instance’s `handle_squid_return` applies the effects (e.g., creates stolen rocks, updates squid stats). 186 | 187 | --- 188 | 189 | ## Debugging and Logging 190 | 191 | When `debug_mode` is enabled, the autopilot provides detailed logs for: 192 | - Initialization details (position, max time away, home direction). 193 | - State transitions (e.g., spotting food, switching to feeding). 194 | - Movement changes (direction changes, boundary reaching). 195 | - Object interactions (eating food, stealing rocks). 196 | - Final summary upon returning. 197 | 198 | The `debug_autopilot_status` method in `main.py` prints the current state of all controllers, including position, direction, time away, and activities. 199 | 200 | --- 201 | 202 | ## Example Flow 203 | 204 | 1. A squid enters from the left (`entry_direction = 'left'`) with `max_time_away = 120` seconds and `max_rocks_to_steal = 2`. 205 | 2. It starts in `exploring`, wandering randomly. 206 | 3. After 10 seconds, it spots food (within 300 pixels), transitions to `feeding`, moves to the food, and eats it (`food_eaten_count += 1`). 207 | 4. Later, it finds a rock, transitions to `interacting`, and steals it (40% chance, `rocks_stolen = 1`). 208 | 5. After 80 seconds, it finds another rock but doesn’t steal it (random chance fails). 209 | 6. At 120 seconds, it transitions to `returning`, determines `home_direction = 'right'` (opposite of entry), and moves to the right boundary. 210 | 7. Upon reaching the boundary, it logs a summary (e.g., 1 food eaten, 2 rocks interacted with, 1 rock stolen) and exits. 211 | 8. `main.py` sends a `squid_return` message, and the home instance creates the stolen rock and updates the squid’s stats. 212 | 213 | --- 214 | 215 | ## Key Methods 216 | 217 | - **`__init__`**: Initializes the controller with squid data, scene, and parameters. 218 | - **`update`**: Main method driving state transitions and behavior updates. 219 | - **`explore`**: Handles random movement and object detection. 220 | - **`seek_food`**: Manages movement toward food and eating. 221 | - **`interact_with_object`**: Handles rock interactions and stealing. 222 | - **`return_home`**: Manages movement to the home boundary. 223 | - **`find_nearby_food`/`find_nearby_rock`**: Detects nearby objects. 224 | - **`move_in_direction`/`move_toward`**: Controls movement mechanics. 225 | - **`get_summary`**: Generates activity summary upon returning. 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What if a Tamagotchi had a neural network and could learn stuff? 2 | # Dosidicus electronicae 3 | ### A digital pet with a simple neural network [research project] 4 | * Includes detailed tools for visualising and understanding how neural networks and Hebbian learning work 5 | 6 | * requires `PyQt5` and `numpy` 7 | 8 | Check releases: https://github.com/ViciousSquid/Dosidicus/releases/ 9 | 10 | 11 | ![image](https://github.com/user-attachments/assets/6102225a-52d6-440c-adfb-a58fd800f1cd) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ### Autonomous Behavior: 20 | 21 | * The squid moves autonomously, making decisions based on his current state (hunger, sleepiness, etc.). 22 | * Implements a vision cone for food detection, simulating realistic foraging behavior. 23 | * Neural network can make decisions and form associations 24 | * Weights are analysed, tweaked and trained by Hebbian learning algorithm 25 | * Experiences from short-term and long-term memory can influence decision-making 26 | * Squid can create new neurons in response to his environment (Neurogenesis) 27 | 28 | I'm trying to document everything! 29 | [https://github.com/ViciousSquid/Dosidicus/tree/main/Docs] 30 | 31 | ### Needs Management System: 32 | 33 | * Tracks various needs like hunger, sleepiness, happiness, and cleanliness. 34 | * Needs change over time and affect the pet's health and behavior. 35 | * The squid can become sick and die if his needs are neglected. 36 | 37 | Be aware the squid hates taking medicine and will become depressed and need sleep if made to do so. 38 | 39 | ### Personality system 40 | 41 | * Seven different [personality types](https://github.com/ViciousSquid/Dosidicus/blob/main/Docs/Personalities.md) which influence behaviour 42 | 43 | ### Decorate and customise! 44 | 45 | * Choose decorations to be placed into the environment which the squid will interact with! 46 | 47 | ### Debug Tools: 48 | 49 | * Directly View and edit the squid's internal states 50 | -------------------------------------------------------------------------------- /_memory/LongTerm.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /_memory/ShortTerm.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [RockInteractions] 2 | ; Probability values (0.0 to 1.0) 3 | pickup_probability = 0.7 4 | throw_probability = 0.6 5 | min_carry_duration = 2.0 6 | max_carry_duration = 8.0 7 | cooldown_after_throw = 20.0 8 | 9 | ; Stat modifiers 10 | happiness_boost = 9 11 | satisfaction_boost = 9 12 | anxiety_reduction = 9 13 | 14 | ; Memory settings 15 | memory_decay_rate = 0.98 16 | max_rock_memories = 6 17 | 18 | [Neurogenesis] 19 | ; General settings 20 | enabled = True 21 | cooldown = 120 22 | max_neurons = 20 23 | initial_neuron_count = 7 24 | 25 | [Neurogenesis.Novelty] 26 | enabled = True 27 | threshold = 0.6 28 | decay_rate = 0.95 29 | max_counter = 10.0 30 | min_curiosity = 0.3 31 | adventurous_modifier = 1.2 32 | timid_modifier = 0.8 33 | 34 | [Neurogenesis.Stress] 35 | enabled = True 36 | threshold = 0.75 37 | decay_rate = 0.9 38 | max_counter = 10.0 39 | min_anxiety = 0.4 40 | timid_modifier = 1.5 41 | energetic_modifier = 0.7 42 | 43 | [Neurogenesis.Reward] 44 | enabled = True 45 | threshold = 0.6 46 | decay_rate = 0.85 47 | max_counter = 10.0 48 | min_satisfaction = 0.5 49 | boost_multiplier = 1.1 50 | 51 | [Neurogenesis.NeuronProperties] 52 | base_activation = 0.5 53 | position_variance = 50 54 | default_connections = True 55 | connection_strength = 0.3 56 | reciprocal_strength = 0.15 57 | 58 | [Neurogenesis.Appearance] 59 | ; Colors as RGB values 60 | novelty_color = 255,255,150 61 | stress_color = 255,150,150 62 | reward_color = 150,255,150 63 | 64 | ; Shapes 65 | novelty_shape = triangle 66 | stress_shape = square 67 | reward_shape = circle 68 | 69 | [Neurogenesis.VisualEffects] 70 | highlight_duration = 5.0 71 | highlight_radius = 40 72 | pulse_effect = True 73 | pulse_speed = 0.5 -------------------------------------------------------------------------------- /images/cheese.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/cheese.png -------------------------------------------------------------------------------- /images/curious.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/curious.png -------------------------------------------------------------------------------- /images/decoration/bigr02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/bigr02.png -------------------------------------------------------------------------------- /images/decoration/plant01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant01.png -------------------------------------------------------------------------------- /images/decoration/plant02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant02.png -------------------------------------------------------------------------------- /images/decoration/plant03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant03.png -------------------------------------------------------------------------------- /images/decoration/plant04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant04.png -------------------------------------------------------------------------------- /images/decoration/plant05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant05.png -------------------------------------------------------------------------------- /images/decoration/plant06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant06.png -------------------------------------------------------------------------------- /images/decoration/plant07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant07.png -------------------------------------------------------------------------------- /images/decoration/plant08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant08.png -------------------------------------------------------------------------------- /images/decoration/plant09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant09.png -------------------------------------------------------------------------------- /images/decoration/plant10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/plant10.png -------------------------------------------------------------------------------- /images/decoration/rock01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/rock01.png -------------------------------------------------------------------------------- /images/decoration/rock02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/rock02.png -------------------------------------------------------------------------------- /images/decoration/sml_plant01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/sml_plant01.png -------------------------------------------------------------------------------- /images/decoration/st_bigr01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/st_bigr01.png -------------------------------------------------------------------------------- /images/decoration/st_castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/st_castle.png -------------------------------------------------------------------------------- /images/decoration/st_urchin1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/st_urchin1.png -------------------------------------------------------------------------------- /images/decoration/urchin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/decoration/urchin2.png -------------------------------------------------------------------------------- /images/egg/anim01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim01.jpg -------------------------------------------------------------------------------- /images/egg/anim02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim02.jpg -------------------------------------------------------------------------------- /images/egg/anim03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim03.jpg -------------------------------------------------------------------------------- /images/egg/anim04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim04.jpg -------------------------------------------------------------------------------- /images/egg/anim05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim05.jpg -------------------------------------------------------------------------------- /images/egg/anim06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/egg/anim06.jpg -------------------------------------------------------------------------------- /images/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/food.png -------------------------------------------------------------------------------- /images/inkcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/inkcloud.png -------------------------------------------------------------------------------- /images/left1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/left1.png -------------------------------------------------------------------------------- /images/left2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/left2.png -------------------------------------------------------------------------------- /images/medicine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/medicine.png -------------------------------------------------------------------------------- /images/poop1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/poop1.png -------------------------------------------------------------------------------- /images/poop2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/poop2.png -------------------------------------------------------------------------------- /images/right1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/right1.png -------------------------------------------------------------------------------- /images/right2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/right2.png -------------------------------------------------------------------------------- /images/sick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/sick.png -------------------------------------------------------------------------------- /images/sleep1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/sleep1.png -------------------------------------------------------------------------------- /images/sleep2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/sleep2.png -------------------------------------------------------------------------------- /images/squid_rps_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/squid_rps_frame.png -------------------------------------------------------------------------------- /images/startled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/startled.png -------------------------------------------------------------------------------- /images/sushi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/sushi.png -------------------------------------------------------------------------------- /images/think.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/think.png -------------------------------------------------------------------------------- /images/up1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/up1.png -------------------------------------------------------------------------------- /images/up2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/images/up2.png -------------------------------------------------------------------------------- /plugins/multiplayer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/plugins/multiplayer/__init__.py -------------------------------------------------------------------------------- /plugins/multiplayer/multiplayer_config_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import time 3 | 4 | class MultiplayerConfigDialog(QtWidgets.QDialog): 5 | def __init__(self, plugin, parent=None, multicast_group=None, port=None, sync_interval=None, remote_opacity=None, show_labels=None, show_connections=None): 6 | super().__init__(parent) 7 | self.plugin = plugin 8 | 9 | # Store settings 10 | self.MULTICAST_GROUP = multicast_group 11 | self.MULTICAST_PORT = port 12 | self.SYNC_INTERVAL = sync_interval 13 | self.REMOTE_SQUID_OPACITY = remote_opacity 14 | self.SHOW_REMOTE_LABELS = show_labels 15 | self.SHOW_CONNECTION_LINES = show_connections 16 | 17 | # Initialize UI 18 | self.setup_ui() 19 | 20 | def setup_ui(self): 21 | """Set up the UI components with larger fonts and better scaling""" 22 | layout = QtWidgets.QVBoxLayout(self) 23 | 24 | # Set a base font size for better readability 25 | base_font = QtGui.QFont() 26 | base_font.setPointSize(10) # Increase default font size 27 | 28 | # Apply the font to the dialog 29 | self.setFont(base_font) 30 | 31 | # Network settings group 32 | network_group = QtWidgets.QGroupBox("Network Settings") 33 | network_group.setFont(base_font) # Apply font to group box title 34 | network_layout = QtWidgets.QFormLayout(network_group) 35 | 36 | # Multicast group address (larger font) 37 | self.multicast_address = QtWidgets.QLineEdit(self.MULTICAST_GROUP) 38 | self.multicast_address.setFont(base_font) # Apply font to text input 39 | network_layout.addRow("Multicast Group:", self.multicast_address) 40 | 41 | # Port (larger font) 42 | self.port = QtWidgets.QSpinBox() 43 | self.port.setFont(base_font) 44 | self.port.setRange(1024, 65535) 45 | self.port.setValue(self.MULTICAST_PORT) 46 | network_layout.addRow("Port:", self.port) 47 | 48 | # Sync interval (larger font) 49 | self.sync_interval = QtWidgets.QDoubleSpinBox() 50 | self.sync_interval.setFont(base_font) 51 | self.sync_interval.setRange(0.1, 5.0) 52 | self.sync_interval.setSingleStep(0.1) 53 | self.sync_interval.setValue(self.SYNC_INTERVAL) 54 | network_layout.addRow("Sync Interval (s):", self.sync_interval) 55 | 56 | layout.addWidget(network_group) 57 | 58 | # Node info group (larger font) 59 | node_group = QtWidgets.QGroupBox("Local Node Information") 60 | node_group.setFont(base_font) 61 | node_layout = QtWidgets.QFormLayout(node_group) 62 | 63 | # Node ID (read-only, larger font) 64 | self.node_id = QtWidgets.QLineEdit() 65 | self.node_id.setFont(base_font) 66 | self.node_id.setReadOnly(True) 67 | if hasattr(self.plugin, 'network_node') and self.plugin.network_node: 68 | self.node_id.setText(self.plugin.network_node.node_id) 69 | node_layout.addRow("Node ID:", self.node_id) 70 | 71 | # Local IP (read-only, larger font) 72 | self.local_ip = QtWidgets.QLineEdit() 73 | self.local_ip.setFont(base_font) 74 | self.local_ip.setReadOnly(True) 75 | if hasattr(self.plugin, 'network_node') and self.plugin.network_node: 76 | self.local_ip.setText(self.plugin.network_node.local_ip) 77 | node_layout.addRow("Local IP:", self.local_ip) 78 | 79 | layout.addWidget(node_group) 80 | 81 | # Visual settings group (larger font) 82 | visual_group = QtWidgets.QGroupBox("Visual Settings") 83 | visual_group.setFont(base_font) 84 | visual_layout = QtWidgets.QFormLayout(visual_group) 85 | 86 | # Remote squid opacity (larger font) 87 | self.remote_opacity = QtWidgets.QSlider(QtCore.Qt.Horizontal) 88 | self.remote_opacity.setFont(base_font) 89 | self.remote_opacity.setRange(10, 100) 90 | self.remote_opacity.setValue(int(self.REMOTE_SQUID_OPACITY * 100)) 91 | self.remote_opacity.setTickPosition(QtWidgets.QSlider.TicksBelow) 92 | self.remote_opacity.setTickInterval(10) 93 | visual_layout.addRow("Remote Squid Opacity:", self.remote_opacity) 94 | 95 | # Show remote labels (larger font) 96 | self.show_labels = QtWidgets.QCheckBox() 97 | self.show_labels.setFont(base_font) 98 | self.show_labels.setChecked(self.SHOW_REMOTE_LABELS) 99 | visual_layout.addRow("Show Remote Labels:", self.show_labels) 100 | 101 | # Show connection lines (larger font) 102 | self.show_connections = QtWidgets.QCheckBox() 103 | self.show_connections.setFont(base_font) 104 | self.show_connections.setChecked(self.SHOW_CONNECTION_LINES) 105 | visual_layout.addRow("Show Connection Lines:", self.show_connections) 106 | 107 | layout.addWidget(visual_group) 108 | 109 | # Add advanced settings group 110 | advanced_group = QtWidgets.QGroupBox("Advanced Settings") 111 | advanced_group.setFont(base_font) 112 | advanced_layout = QtWidgets.QFormLayout(advanced_group) 113 | 114 | # Debug mode checkbox 115 | self.debug_mode = QtWidgets.QCheckBox() 116 | self.debug_mode.setFont(base_font) 117 | self.debug_mode.setChecked(getattr(self.plugin, 'debug_mode', False)) 118 | advanced_layout.addRow("Debug Mode:", self.debug_mode) 119 | 120 | # Reconnect on failure checkbox 121 | self.auto_reconnect = QtWidgets.QCheckBox() 122 | self.auto_reconnect.setFont(base_font) 123 | self.auto_reconnect.setChecked(getattr(self.plugin.network_node, 'auto_reconnect', True) 124 | if hasattr(self.plugin, 'network_node') else True) 125 | advanced_layout.addRow("Auto Reconnect:", self.auto_reconnect) 126 | 127 | # Packet compression checkbox 128 | self.use_compression = QtWidgets.QCheckBox() 129 | self.use_compression.setFont(base_font) 130 | self.use_compression.setChecked(getattr(self.plugin.network_node, 'use_compression', True) 131 | if hasattr(self.plugin, 'network_node') else True) 132 | advanced_layout.addRow("Use Compression:", self.use_compression) 133 | 134 | layout.addWidget(advanced_group) 135 | 136 | # Connected peers list (larger font) 137 | peers_group = QtWidgets.QGroupBox("Connected Peers") 138 | peers_group.setFont(base_font) 139 | peers_layout = QtWidgets.QVBoxLayout(peers_group) 140 | 141 | self.peers_list = QtWidgets.QListWidget() 142 | self.peers_list.setFont(base_font) # Apply font to list items 143 | self.update_peers_list() 144 | peers_layout.addWidget(self.peers_list) 145 | 146 | refresh_button = QtWidgets.QPushButton("Refresh") 147 | refresh_button.setFont(base_font) # Larger button text 148 | refresh_button.clicked.connect(self.update_peers_list) 149 | peers_layout.addWidget(refresh_button) 150 | 151 | layout.addWidget(peers_group) 152 | 153 | # Buttons (larger font) 154 | button_box = QtWidgets.QDialogButtonBox( 155 | QtWidgets.QDialogButtonBox.Save | 156 | QtWidgets.QDialogButtonBox.Cancel 157 | ) 158 | button_box.setFont(base_font) # Larger button text 159 | button_box.accepted.connect(self.save_settings) 160 | button_box.rejected.connect(self.reject) 161 | layout.addWidget(button_box) 162 | 163 | def update_peers_list(self): 164 | """Update the list of connected peers""" 165 | self.peers_list.clear() 166 | 167 | if (hasattr(self.plugin, 'network_node') and 168 | self.plugin.network_node and 169 | hasattr(self.plugin.network_node, 'known_nodes')): 170 | 171 | for node_id, (ip, last_seen, squid_data) in self.plugin.network_node.known_nodes.items(): 172 | status = "Active" if time.time() - last_seen < 10 else "Inactive" 173 | item = QtWidgets.QListWidgetItem(f"{node_id} ({ip}) - {status}") 174 | 175 | # Set color based on status 176 | if status == "Active": 177 | item.setForeground(QtGui.QBrush(QtGui.QColor(0, 128, 0))) 178 | else: 179 | item.setForeground(QtGui.QBrush(QtGui.QColor(128, 128, 128))) 180 | 181 | self.peers_list.addItem(item) 182 | 183 | if self.peers_list.count() == 0: 184 | self.peers_list.addItem("No peers connected") 185 | 186 | def save_settings(self): 187 | """Save settings back to the plugin""" 188 | try: 189 | # Validate multicast address 190 | import socket 191 | socket.inet_aton(self.multicast_address.text()) 192 | 193 | # Save settings that can be changed without restart 194 | self.plugin.REMOTE_SQUID_OPACITY = self.remote_opacity.value() / 100.0 195 | self.plugin.SHOW_REMOTE_LABELS = self.show_labels.isChecked() 196 | self.plugin.SHOW_CONNECTION_LINES = self.show_connections.isChecked() 197 | 198 | # Update remote squid visuals with new opacity 199 | if hasattr(self.plugin, 'remote_squids'): 200 | for squid_data in self.plugin.remote_squids.values(): 201 | if 'visual' in squid_data and squid_data['visual']: 202 | squid_data['visual'].setOpacity(self.plugin.REMOTE_SQUID_OPACITY) 203 | 204 | # Update entity manager if available 205 | if hasattr(self.plugin, 'entity_manager'): 206 | self.plugin.entity_manager.update_settings( 207 | opacity=self.plugin.REMOTE_SQUID_OPACITY, 208 | show_labels=self.plugin.SHOW_REMOTE_LABELS, 209 | show_connections=self.plugin.SHOW_CONNECTION_LINES 210 | ) 211 | 212 | # Settings that require restart 213 | restart_needed = False 214 | if (self.plugin.MULTICAST_GROUP != self.multicast_address.text() or 215 | self.plugin.MULTICAST_PORT != self.port.value() or 216 | abs(self.plugin.SYNC_INTERVAL - self.sync_interval.value()) > 0.01): 217 | 218 | self.plugin.MULTICAST_GROUP = self.multicast_address.text() 219 | self.plugin.MULTICAST_PORT = self.port.value() 220 | self.plugin.SYNC_INTERVAL = self.sync_interval.value() 221 | restart_needed = True 222 | 223 | # Save advanced settings 224 | self.plugin.debug_mode = self.debug_mode.isChecked() 225 | if hasattr(self.plugin, 'network_node'): 226 | self.plugin.network_node.auto_reconnect = self.auto_reconnect.isChecked() 227 | self.plugin.network_node.use_compression = self.use_compression.isChecked() 228 | 229 | # Update debug mode on related components 230 | if hasattr(self.plugin, 'network_node'): 231 | self.plugin.network_node.debug_mode = self.plugin.debug_mode 232 | if hasattr(self.plugin, 'entity_manager'): 233 | self.plugin.entity_manager.debug_mode = self.plugin.debug_mode 234 | if hasattr(self.plugin, 'event_dispatcher'): 235 | self.plugin.event_dispatcher.debug_mode = self.plugin.debug_mode 236 | 237 | # Show message about restart if needed 238 | if restart_needed: 239 | QtWidgets.QMessageBox.information( 240 | self, 241 | "Restart Required", 242 | "Some settings changes require restarting the application to take effect." 243 | ) 244 | 245 | self.accept() 246 | 247 | except Exception as e: 248 | QtWidgets.QMessageBox.warning( 249 | self, 250 | "Invalid Settings", 251 | f"Error in settings: {str(e)}" 252 | ) -------------------------------------------------------------------------------- /plugins/multiplayer/multiplayer_events.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore 2 | from typing import Dict, Any, List, Optional, Callable 3 | 4 | class MultiplayerEventDispatcher(QtCore.QObject): 5 | """Dispatches multiplayer events to registered handlers""" 6 | 7 | # Define signals for various event types 8 | squid_joined = QtCore.pyqtSignal(str, dict) # node_id, squid_data 9 | squid_left = QtCore.pyqtSignal(str, str) # node_id, reason 10 | squid_moved = QtCore.pyqtSignal(str, dict) # node_id, position_data 11 | squid_action = QtCore.pyqtSignal(str, str, dict) # node_id, action_type, action_data 12 | object_synced = QtCore.pyqtSignal(str, list) # node_id, objects_data 13 | rock_thrown = QtCore.pyqtSignal(str, dict) # node_id, rock_data 14 | squid_exited = QtCore.pyqtSignal(str, str, dict) # node_id, direction, exit_data 15 | squid_arrived = QtCore.pyqtSignal(str, dict) # node_id, arrival_data 16 | 17 | def __init__(self, parent=None): 18 | super().__init__(parent) 19 | self.handlers = {} 20 | self.debug_mode = False 21 | 22 | def register_handler(self, event_type: str, handler: Callable): 23 | """Register a handler for a specific event type""" 24 | if event_type not in self.handlers: 25 | self.handlers[event_type] = [] 26 | self.handlers[event_type].append(handler) 27 | 28 | # Connect to corresponding signal if it exists 29 | signal_map = { 30 | 'squid_joined': self.squid_joined, 31 | 'squid_left': self.squid_left, 32 | 'squid_moved': self.squid_moved, 33 | 'squid_action': self.squid_action, 34 | 'object_synced': self.object_synced, 35 | 'rock_thrown': self.rock_thrown, 36 | 'squid_exited': self.squid_exited, 37 | 'squid_arrived': self.squid_arrived 38 | } 39 | 40 | if event_type in signal_map: 41 | signal_map[event_type].connect(handler) 42 | 43 | def dispatch_event(self, event_type: str, *args, **kwargs): 44 | """Dispatch an event to all registered handlers""" 45 | if self.debug_mode: 46 | print(f"Dispatching event: {event_type}") 47 | 48 | # Emit the corresponding signal if it exists 49 | signal_map = { 50 | 'squid_joined': self.squid_joined, 51 | 'squid_left': self.squid_left, 52 | 'squid_moved': self.squid_moved, 53 | 'squid_action': self.squid_action, 54 | 'object_synced': self.object_synced, 55 | 'rock_thrown': self.rock_thrown, 56 | 'squid_exited': self.squid_exited, 57 | 'squid_arrived': self.squid_arrived 58 | } 59 | 60 | if event_type in signal_map: 61 | signal = signal_map[event_type] 62 | signal.emit(*args) 63 | 64 | # Call direct handlers 65 | if event_type in self.handlers: 66 | for handler in self.handlers[event_type]: 67 | try: 68 | handler(*args, **kwargs) 69 | except Exception as e: 70 | print(f"Error in event handler for {event_type}: {e}") -------------------------------------------------------------------------------- /plugins/multiplayer/multiplayer_status_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import time 3 | 4 | class MultiplayerStatusWidget(QtWidgets.QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | self.setObjectName("MultiplayerStatusWidget") 8 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 9 | 10 | # Keep track of peers and connection status 11 | self.connection_active = False 12 | self.node_id = "Unknown" 13 | self.peers = [] 14 | self.last_activity = {} 15 | 16 | # Setup UI 17 | self.setup_ui() 18 | 19 | # Update timer 20 | self.update_timer = QtCore.QTimer(self) 21 | self.update_timer.timeout.connect(self.update_display) 22 | self.update_timer.start(1000) # Update every second 23 | 24 | def setup_ui(self): 25 | layout = QtWidgets.QVBoxLayout(self) 26 | layout.setContentsMargins(5, 5, 5, 5) 27 | 28 | # Create a frame with semi-transparent background 29 | frame = QtWidgets.QFrame(self) 30 | frame.setStyleSheet(""" 31 | QFrame { 32 | background-color: rgba(0, 0, 0, 170); 33 | border-radius: 12px; 34 | color: white; 35 | border: 1px solid rgba(255, 255, 255, 100); 36 | } 37 | """) 38 | frame_layout = QtWidgets.QVBoxLayout(frame) 39 | 40 | # Title with improved styling 41 | title_label = QtWidgets.QLabel("🌐 Multiplayer") 42 | title_label.setStyleSheet("color: #FFFFFF; font-weight: bold; font-size: 14px;") 43 | title_label.setAlignment(QtCore.Qt.AlignCenter) 44 | frame_layout.addWidget(title_label) 45 | 46 | # Status header with icon 47 | status_layout = QtWidgets.QHBoxLayout() 48 | status_icon = QtWidgets.QLabel("⚠️") # Default to warning icon 49 | self.status_icon = status_icon 50 | status_layout.addWidget(status_icon) 51 | 52 | self.status_label = QtWidgets.QLabel("Disconnected") 53 | self.status_label.setStyleSheet("color: #FF6666; font-weight: bold;") 54 | status_layout.addWidget(self.status_label) 55 | status_layout.addStretch() 56 | frame_layout.addLayout(status_layout) 57 | 58 | # Add activity log 59 | self.activity_log = QtWidgets.QListWidget() 60 | self.activity_log.setMaximumHeight(120) 61 | self.activity_log.setStyleSheet(""" 62 | QListWidget { 63 | background-color: rgba(0, 0, 0, 100); 64 | border: 1px solid #444444; 65 | border-radius: 5px; 66 | color: white; 67 | } 68 | QListWidget::item { 69 | padding: 2px; 70 | } 71 | """) 72 | frame_layout.addWidget(self.activity_log) 73 | 74 | # Add minimize/expand button 75 | toggle_button = QtWidgets.QPushButton("▲") 76 | toggle_button.setMaximumWidth(30) 77 | toggle_button.clicked.connect(self.toggle_expanded) 78 | frame_layout.addWidget(toggle_button, alignment=QtCore.Qt.AlignRight) 79 | 80 | # Add to main layout 81 | layout.addWidget(frame) 82 | 83 | # Initialize as expanded 84 | self.is_expanded = True 85 | 86 | def add_activity(self, message): 87 | """Add an entry to the activity log""" 88 | timestamp = QtCore.QTime.currentTime().toString("hh:mm:ss") 89 | item = QtWidgets.QListWidgetItem(f"{timestamp}: {message}") 90 | 91 | # Add to beginning for most recent at top 92 | self.activity_log.insertItem(0, item) 93 | 94 | # Limit size of log 95 | if self.activity_log.count() > 50: 96 | self.activity_log.takeItem(self.activity_log.count() - 1) 97 | 98 | def toggle_expanded(self): 99 | """Toggle between minimized and expanded view""" 100 | self.is_expanded = not self.is_expanded 101 | 102 | # Show/hide elements based on state 103 | self.activity_log.setVisible(self.is_expanded) 104 | 105 | # Adjust button text 106 | sender = self.sender() 107 | if isinstance(sender, QtWidgets.QPushButton): 108 | sender.setText("▲" if not self.is_expanded else "▼") 109 | 110 | # Resize the widget 111 | if self.is_expanded: 112 | self.setMaximumHeight(1000) # Effectively no max height 113 | else: 114 | self.setMaximumHeight(100) # Just enough for status and ID 115 | 116 | def update_connection_status(self, is_connected, node_id=None): 117 | """Update the connection status display""" 118 | self.connection_active = is_connected 119 | 120 | if node_id: 121 | self.node_id = node_id 122 | 123 | if is_connected: 124 | self.status_label.setText("Multiplayer: Connected") 125 | self.status_label.setStyleSheet("color: #66FF66; font-weight: bold;") 126 | self.node_id_label.setText(f"Node ID: {self.node_id}") 127 | else: 128 | self.status_label.setText("Multiplayer: Disconnected") 129 | self.status_label.setStyleSheet("color: #FF6666; font-weight: bold;") 130 | self.node_id_label.setText("Node ID: -") 131 | 132 | def update_peers(self, peers_data): 133 | """Update the list of connected peers""" 134 | self.peers = [] 135 | self.peers_list.clear() 136 | 137 | current_time = time.time() 138 | 139 | for node_id, (ip, last_seen, _) in peers_data.items(): 140 | status = "Active" if current_time - last_seen < 10 else "Inactive" 141 | self.peers.append({ 142 | 'node_id': node_id, 143 | 'ip': ip, 144 | 'last_seen': last_seen, 145 | 'status': status 146 | }) 147 | 148 | # Add to the list widget 149 | item = QtWidgets.QListWidgetItem(f"{node_id[-6:]} ({ip})") 150 | 151 | # Style based on status 152 | if status == "Active": 153 | item.setForeground(QtGui.QBrush(QtGui.QColor(100, 255, 100))) 154 | else: 155 | item.setForeground(QtGui.QBrush(QtGui.QColor(150, 150, 150))) 156 | 157 | self.peers_list.addItem(item) 158 | 159 | # Update peers count 160 | active_count = sum(1 for p in self.peers if p['status'] == "Active") 161 | self.peers_label.setText(f"Connected Peers: {active_count}") 162 | 163 | def update_display(self): 164 | """Update the display with current information""" 165 | # Refresh active/inactive status based on timers 166 | if self.peers: 167 | current_time = time.time() 168 | update_needed = False 169 | 170 | for peer in self.peers: 171 | old_status = peer['status'] 172 | peer['status'] = "Active" if current_time - peer['last_seen'] < 10 else "Inactive" 173 | 174 | if old_status != peer['status']: 175 | update_needed = True 176 | 177 | if update_needed: 178 | self.refresh_peers_list() 179 | 180 | def refresh_peers_list(self): 181 | """Refresh the peers list widget without changing the underlying data""" 182 | self.peers_list.clear() 183 | 184 | for peer in self.peers: 185 | item = QtWidgets.QListWidgetItem(f"{peer['node_id'][-6:]} ({peer['ip']})") 186 | 187 | # Style based on status 188 | if peer['status'] == "Active": 189 | item.setForeground(QtGui.QBrush(QtGui.QColor(100, 255, 100))) 190 | else: 191 | item.setForeground(QtGui.QBrush(QtGui.QColor(150, 150, 150))) 192 | 193 | self.peers_list.addItem(item) 194 | 195 | # Update peers count 196 | active_count = sum(1 for p in self.peers if p['status'] == "Active") 197 | self.peers_label.setText(f"Connected Peers: {active_count}") -------------------------------------------------------------------------------- /plugins/multiplayer/network_utilities.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | import uuid 4 | import json 5 | import zlib 6 | from typing import Dict, Any, Tuple, Optional 7 | 8 | class NetworkUtilities: 9 | @staticmethod 10 | def get_local_ip() -> str: 11 | """Get local IP address with fallback to localhost""" 12 | try: 13 | temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 14 | temp_socket.connect(('8.8.8.8', 80)) 15 | local_ip = temp_socket.getsockname()[0] 16 | temp_socket.close() 17 | return local_ip 18 | except Exception: 19 | return '127.0.0.1' 20 | 21 | @staticmethod 22 | def compress_message(message: Dict[str, Any]) -> bytes: 23 | """Compress a message with efficient error handling""" 24 | try: 25 | serialized_msg = json.dumps(message).encode('utf-8') 26 | try: 27 | return zlib.compress(serialized_msg) 28 | except ImportError: 29 | return serialized_msg 30 | except Exception as e: 31 | print(f"Error compressing message: {e}") 32 | # Return a minimal valid message 33 | return json.dumps({"error": "compression_failure"}).encode('utf-8') 34 | 35 | @staticmethod 36 | def decompress_message(data: bytes) -> Dict[str, Any]: 37 | """Decompress a message with efficient error handling""" 38 | try: 39 | try: 40 | decompressed_data = zlib.decompress(data).decode('utf-8') 41 | except (ImportError, zlib.error): 42 | decompressed_data = data.decode('utf-8') 43 | 44 | return json.loads(decompressed_data) 45 | except Exception as e: 46 | print(f"Error decompressing message: {e}") 47 | return {"error": "decompression_failure"} 48 | 49 | @staticmethod 50 | def generate_node_id(prefix: str = "squid") -> str: 51 | """Generate a unique node identifier""" 52 | return f"{prefix}_{uuid.uuid4().hex[:8]}" 53 | 54 | @staticmethod 55 | def is_node_active(last_seen_time: float, threshold: float = 10.0) -> bool: 56 | """Check if a node is considered active based on last seen time""" 57 | return time.time() - last_seen_time < threshold 58 | 59 | import struct 60 | 61 | class BinaryProtocol: 62 | """Efficient binary protocol for network messages""" 63 | 64 | # Message type constants 65 | MSG_HEARTBEAT = 1 66 | MSG_SQUID_MOVE = 2 67 | MSG_OBJECT_SYNC = 3 68 | MSG_PLAYER_JOIN = 4 69 | 70 | @staticmethod 71 | def encode_squid_move(node_id, x, y, direction, timestamp): 72 | """Encode squid movement into compact binary format""" 73 | # Convert direction to numeric value 74 | dir_map = {'right': 0, 'left': 1, 'up': 2, 'down': 3} 75 | dir_value = dir_map.get(direction, 0) 76 | 77 | # Pack into binary: message type(1) + node_id(16) + x(4) + y(4) + direction(1) + timestamp(8) 78 | return struct.pack('!B16sffBd', 79 | BinaryProtocol.MSG_SQUID_MOVE, 80 | node_id.encode()[:16].ljust(16, b'\0'), 81 | float(x), 82 | float(y), 83 | dir_value, 84 | timestamp) 85 | 86 | @staticmethod 87 | def decode_message(binary_data): 88 | """Decode binary message into appropriate type""" 89 | if not binary_data or len(binary_data) < 2: 90 | return None 91 | 92 | msg_type = struct.unpack('!B', binary_data[0:1])[0] 93 | 94 | if msg_type == BinaryProtocol.MSG_SQUID_MOVE: 95 | # Unpack squid move message 96 | msg_type, node_id, x, y, dir_value, timestamp = struct.unpack('!B16sffBd', binary_data) 97 | 98 | # Convert back to string and direction 99 | node_id = node_id.rstrip(b'\0').decode() 100 | dir_map = {0: 'right', 1: 'left', 2: 'up', 3: 'down'} 101 | direction = dir_map.get(dir_value, 'right') 102 | 103 | return { 104 | 'type': 'squid_move', 105 | 'node_id': node_id, 106 | 'x': x, 107 | 'y': y, 108 | 'direction': direction, 109 | 'timestamp': timestamp 110 | } -------------------------------------------------------------------------------- /plugins/multiplayer/packet_validator.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import time 4 | from typing import Dict, Any, Optional, List, Tuple 5 | 6 | class PacketValidator: 7 | """Utility class to validate network packets for security and integrity""" 8 | 9 | @staticmethod 10 | def validate_message(message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: 11 | """ 12 | Validate a message for required fields and proper structure 13 | 14 | Args: 15 | message: The message to validate 16 | 17 | Returns: 18 | (is_valid, error_message) 19 | """ 20 | # Check for required fields 21 | required_fields = ['node_id', 'timestamp', 'type', 'payload'] 22 | for field in required_fields: 23 | if field not in message: 24 | return False, f"Missing required field: {field}" 25 | 26 | # Validate node_id format (alphanumeric) 27 | if not isinstance(message['node_id'], str) or not re.match(r'^[a-zA-Z0-9_-]+$', message['node_id']): 28 | return False, "Invalid node_id format" 29 | 30 | # Check timestamp (should be within 1 hour of current time to prevent replay attacks) 31 | current_time = time.time() 32 | msg_time = message['timestamp'] 33 | if not isinstance(msg_time, (int, float)) or abs(current_time - msg_time) > 3600: 34 | return False, "Invalid timestamp" 35 | 36 | # Validate message type 37 | valid_types = [ 38 | 'heartbeat', 'squid_move', 'squid_action', 'object_sync', 39 | 'rock_throw', 'player_join', 'player_leave', 'state_update', 40 | 'squid_exit', 'new_squid_arrival' 41 | ] 42 | if message['type'] not in valid_types: 43 | return False, f"Unknown message type: {message['type']}" 44 | 45 | # Validate payload is a dictionary 46 | if not isinstance(message['payload'], dict): 47 | return False, "Payload must be a dictionary" 48 | 49 | # Type-specific validation 50 | if message['type'] == 'squid_exit': 51 | return PacketValidator.validate_squid_exit(message['payload']) 52 | elif message['type'] == 'object_sync': 53 | return PacketValidator.validate_object_sync(message['payload']) 54 | 55 | # Default to valid for types without specific validation 56 | return True, None 57 | 58 | @staticmethod 59 | def validate_squid_exit(payload: Dict[str, Any]) -> Tuple[bool, Optional[str]]: 60 | """Validate squid exit payload""" 61 | # Check for nested payload structure 62 | if 'payload' not in payload: 63 | return False, "Missing nested payload in squid_exit message" 64 | 65 | exit_data = payload['payload'] 66 | 67 | # Check required fields 68 | required_fields = ['node_id', 'direction', 'position', 'color'] 69 | for field in required_fields: 70 | if field not in exit_data: 71 | return False, f"Missing required field in squid_exit: {field}" 72 | 73 | # Validate direction 74 | valid_directions = ['left', 'right', 'up', 'down'] 75 | if exit_data['direction'] not in valid_directions: 76 | return False, f"Invalid exit direction: {exit_data['direction']}" 77 | 78 | # Validate position is a dictionary with x,y 79 | if not isinstance(exit_data['position'], dict) or not all(k in exit_data['position'] for k in ['x', 'y']): 80 | return False, "Invalid position format" 81 | 82 | # Validate color is a tuple or list 83 | color = exit_data['color'] 84 | if not isinstance(color, (list, tuple)) or len(color) < 3 or not all(isinstance(c, int) for c in color[:3]): 85 | return False, "Invalid color format" 86 | 87 | return True, None 88 | 89 | @staticmethod 90 | def validate_object_sync(payload: Dict[str, Any]) -> Tuple[bool, Optional[str]]: 91 | """Validate object sync payload""" 92 | # Check for squid data 93 | if 'squid' not in payload: 94 | return False, "Missing squid data in object_sync" 95 | 96 | # Check for objects array 97 | if 'objects' not in payload: 98 | return False, "Missing objects array in object_sync" 99 | 100 | if not isinstance(payload['objects'], list): 101 | return False, "Objects must be an array" 102 | 103 | # Validate squid data has required fields 104 | squid = payload['squid'] 105 | required_squid_fields = ['x', 'y', 'direction'] 106 | for field in required_squid_fields: 107 | if field not in squid: 108 | return False, f"Missing required squid field: {field}" 109 | 110 | # Validate node_info if present 111 | if 'node_info' in payload: 112 | node_info = payload['node_info'] 113 | if not isinstance(node_info, dict) or 'id' not in node_info: 114 | return False, "Invalid node_info format" 115 | 116 | return True, None 117 | 118 | @staticmethod 119 | def sanitize_object_data(objects: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 120 | """Sanitize object data to ensure no malicious content""" 121 | sanitized = [] 122 | 123 | for obj in objects: 124 | # Check if required fields exist 125 | if not all(k in obj for k in ['id', 'type', 'x', 'y']): 126 | continue 127 | 128 | # Sanitize filename to prevent directory traversal 129 | if 'filename' in obj: 130 | filename = obj['filename'] 131 | # Remove any path navigation 132 | filename = re.sub(r'\.\./', '', filename) 133 | filename = re.sub(r'\.\.\\', '', filename) 134 | # Use only the basename 135 | import os 136 | filename = os.path.basename(filename) 137 | obj['filename'] = filename 138 | 139 | # Ensure numeric values are valid 140 | obj['x'] = float(obj['x']) if isinstance(obj['x'], (int, float)) else 0 141 | obj['y'] = float(obj['y']) if isinstance(obj['y'], (int, float)) else 0 142 | if 'scale' in obj: 143 | obj['scale'] = float(obj['scale']) if isinstance(obj['scale'], (int, float)) else 1.0 144 | 145 | # Limit to valid values 146 | obj['scale'] = max(0.1, min(5.0, obj['scale'])) # Reasonable scale limits 147 | 148 | sanitized.append(obj) 149 | 150 | return sanitized -------------------------------------------------------------------------------- /plugins/multiplayer/plugin.txt: -------------------------------------------------------------------------------- 1 | NAME=Multiplayer 2 | VERSION=1.0 3 | AUTHOR=ViciousSquid 4 | DESCRIPTION=Enables network sync for squids and objects (Experimental) 5 | REQUIRES=network_interface -------------------------------------------------------------------------------- /plugins/multiplayer/status_bar_component.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | 3 | class StatusBarComponent: 4 | def __init__(self, main_window): 5 | self.main_window = main_window 6 | 7 | # Create the status bar if it doesn't exist 8 | if not main_window.statusBar(): 9 | self.status_bar = QtWidgets.QStatusBar(main_window) 10 | main_window.setStatusBar(self.status_bar) 11 | else: 12 | self.status_bar = main_window.statusBar() 13 | 14 | # Create status indicators 15 | self.create_indicators() 16 | 17 | # Message queue for rotating messages 18 | self.message_queue = [] 19 | self.message_timer = QtCore.QTimer() 20 | self.message_timer.timeout.connect(self.rotate_messages) 21 | self.message_timer.start(5000) # Rotate messages every 5 seconds 22 | 23 | def create_indicators(self): 24 | """Create permanent status indicators""" 25 | # Plugins indicator 26 | self.plugins_label = QtWidgets.QLabel("Plugins: None") 27 | self.plugins_label.setStyleSheet("padding: 0 10px;") 28 | self.status_bar.addPermanentWidget(self.plugins_label) 29 | 30 | # Network status indicator 31 | self.network_label = QtWidgets.QLabel("Network: Disconnected") 32 | self.network_label.setStyleSheet("padding: 0 10px;") 33 | self.status_bar.addPermanentWidget(self.network_label) 34 | 35 | # Peers indicator 36 | self.peers_label = QtWidgets.QLabel("Peers: 0") 37 | self.peers_label.setStyleSheet("padding: 0 10px;") 38 | self.status_bar.addPermanentWidget(self.peers_label) 39 | 40 | def update_plugins_status(self, plugin_manager): 41 | """Update the plugins status indicator""" 42 | if not plugin_manager: 43 | self.plugins_label.setText("Plugins: None") 44 | return 45 | 46 | enabled_plugins = plugin_manager.get_enabled_plugins() 47 | if not enabled_plugins: 48 | self.plugins_label.setText("Plugins: None") 49 | self.plugins_label.setStyleSheet("padding: 0 10px; color: gray;") 50 | else: 51 | plugin_count = len(enabled_plugins) 52 | self.plugins_label.setText(f"Plugins: {plugin_count} active") 53 | self.plugins_label.setStyleSheet("padding: 0 10px; color: green;") 54 | 55 | # Create tooltip with plugin names 56 | tooltip = "Active plugins:\n" + "\n".join(f"• {p}" for p in enabled_plugins) 57 | self.plugins_label.setToolTip(tooltip) 58 | 59 | def update_network_status(self, connected, node_id=None): 60 | """Update the network status indicator""" 61 | if connected: 62 | self.network_label.setText(f"Network: Connected") 63 | self.network_label.setStyleSheet("padding: 0 10px; color: green;") 64 | if node_id: 65 | self.network_label.setToolTip(f"Connected as {node_id}") 66 | else: 67 | self.network_label.setText("Network: Disconnected") 68 | self.network_label.setStyleSheet("padding: 0 10px; color: gray;") 69 | self.network_label.setToolTip("Network functionality is disconnected") 70 | 71 | def update_peers_count(self, count): 72 | """Update the peers count indicator""" 73 | self.peers_label.setText(f"Peers: {count}") 74 | if count > 0: 75 | self.peers_label.setStyleSheet("padding: 0 10px; color: green;") 76 | else: 77 | self.peers_label.setStyleSheet("padding: 0 10px; color: gray;") 78 | 79 | def add_message(self, message, duration=5000): 80 | """Add a temporary message to the status bar""" 81 | self.status_bar.showMessage(message, duration) 82 | 83 | def add_to_message_queue(self, message): 84 | """Add a message to the rotation queue""" 85 | if message not in self.message_queue: 86 | self.message_queue.append(message) 87 | 88 | def rotate_messages(self): 89 | """Rotate through queued messages""" 90 | if not self.message_queue: 91 | return 92 | 93 | # Show the next message 94 | message = self.message_queue.pop(0) 95 | self.status_bar.showMessage(message, 4500) # Show for slightly less than rotation time 96 | 97 | # Add the message back to the end of the queue 98 | self.message_queue.append(message) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViciousSquid/Dosidicus/434e5b22e3c3aded793d0749697d6e67374b3f98/src/__init__.py -------------------------------------------------------------------------------- /src/brain_base_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | 3 | class BrainBaseTab(QtWidgets.QWidget): 4 | def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False): 5 | super().__init__(parent) 6 | self.parent = parent 7 | self.tamagotchi_logic = tamagotchi_logic 8 | self.brain_widget = brain_widget 9 | self.config = config 10 | self.debug_mode = debug_mode 11 | self.layout = QtWidgets.QVBoxLayout() 12 | self.setLayout(self.layout) 13 | 14 | def set_tamagotchi_logic(self, tamagotchi_logic): 15 | """Update the tamagotchi_logic reference""" 16 | #print(f"BrainBaseTab.set_tamagotchi_logic: {tamagotchi_logic is not None}") 17 | self.tamagotchi_logic = tamagotchi_logic 18 | 19 | def update_from_brain_state(self, state): 20 | """Update tab based on brain state - override in subclasses""" 21 | pass 22 | 23 | def create_button(self, text, callback, color): 24 | """Common utility for creating consistent buttons""" 25 | button = QtWidgets.QPushButton(text) 26 | button.clicked.connect(callback) 27 | button.setStyleSheet(f"background-color: {color}; border: 1px solid black; padding: 5px;") 28 | button.setFixedSize(200, 50) 29 | return button 30 | 31 | -------------------------------------------------------------------------------- /src/brain_network_tab.py: -------------------------------------------------------------------------------- 1 | # brain_network_tab.py 2 | import json 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | from .brain_base_tab import BrainBaseTab 5 | from .brain_dialogs import StimulateDialog, DiagnosticReportDialog 6 | 7 | class NetworkTab(BrainBaseTab): 8 | def __init__(self, parent=None, tamagotchi_logic=None, brain_widget=None, config=None, debug_mode=False): 9 | super().__init__(parent, tamagotchi_logic, brain_widget, config, debug_mode) 10 | self.initialize_ui() 11 | 12 | def initialize_ui(self): 13 | # Create a widget to hold the brain visualization 14 | main_content_widget = QtWidgets.QWidget() 15 | main_content_layout = QtWidgets.QVBoxLayout() 16 | main_content_widget.setLayout(main_content_layout) 17 | 18 | # Add brain widget to the main content layout 19 | main_content_layout.addWidget(self.brain_widget, 1) # Give it a stretch factor of 1 20 | 21 | # Checkbox controls 22 | checkbox_layout = QtWidgets.QHBoxLayout() 23 | 24 | self.checkbox_links = QtWidgets.QCheckBox("Show links") 25 | self.checkbox_links.setChecked(True) 26 | self.checkbox_links.stateChanged.connect(self.brain_widget.toggle_links) 27 | checkbox_layout.addWidget(self.checkbox_links) 28 | 29 | self.checkbox_weights = QtWidgets.QCheckBox("Show weights") 30 | self.checkbox_weights.setChecked(True) 31 | self.checkbox_weights.stateChanged.connect(self.brain_widget.toggle_weights) 32 | checkbox_layout.addWidget(self.checkbox_weights) 33 | 34 | # Add stretch to push checkboxes to the left 35 | checkbox_layout.addStretch(1) 36 | main_content_layout.addLayout(checkbox_layout) 37 | 38 | # Button controls 39 | button_layout = QtWidgets.QHBoxLayout() 40 | 41 | self.stimulate_button = self.create_button("Stimulate", self.stimulate_brain, "#d3d3d3") 42 | self.stimulate_button.setEnabled(self.debug_mode) 43 | 44 | self.save_button = self.create_button("Save State", self.save_brain_state, "#d3d3d3") 45 | self.load_button = self.create_button("Load State", self.load_brain_state, "#d3d3d3") 46 | self.report_button = self.create_button("Network Report", self.show_diagnostic_report, "#ADD8E6") 47 | 48 | button_layout.addWidget(self.report_button) 49 | button_layout.addWidget(self.stimulate_button) 50 | button_layout.addWidget(self.save_button) 51 | button_layout.addWidget(self.load_button) 52 | 53 | main_content_layout.addLayout(button_layout) 54 | 55 | # Add the content widget to our layout 56 | self.layout.addWidget(main_content_widget) 57 | 58 | def stimulate_brain(self): 59 | dialog = StimulateDialog(self.brain_widget, self) 60 | if dialog.exec_() == QtWidgets.QDialog.Accepted: 61 | stimulation_values = dialog.get_stimulation_values() 62 | if stimulation_values is not None: 63 | self.brain_widget.update_state(stimulation_values) 64 | if self.tamagotchi_logic: 65 | self.tamagotchi_logic.update_from_brain(stimulation_values) 66 | 67 | def save_brain_state(self): 68 | file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Brain State", "", "JSON Files (*.json)") 69 | if file_name: 70 | with open(file_name, 'w') as f: 71 | json.dump(self.brain_widget.state, f) 72 | 73 | def load_brain_state(self): 74 | file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load Brain State", "", "JSON Files (*.json)") 75 | if file_name: 76 | with open(file_name, 'r') as f: 77 | state = json.load(f) 78 | self.brain_widget.update_state(state) 79 | 80 | def show_diagnostic_report(self): 81 | dialog = DiagnosticReportDialog(self.brain_widget, self) 82 | dialog.exec_() 83 | 84 | def create_button(self, text, callback, color): 85 | """Common utility for creating consistent buttons with proper scaling""" 86 | from .display_scaling import DisplayScaling 87 | 88 | button = QtWidgets.QPushButton(text) 89 | button.clicked.connect(callback) 90 | button.setStyleSheet(f"background-color: {color}; border: 1px solid black; padding: {DisplayScaling.scale(5)}px;") 91 | button.setFixedSize(DisplayScaling.scale(200), DisplayScaling.scale(50)) 92 | 93 | font = button.font() 94 | font.setPointSize(DisplayScaling.font_size(10)) 95 | button.setFont(font) 96 | 97 | return button -------------------------------------------------------------------------------- /src/brain_ui_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from PyQt5 import QtCore, QtGui, QtWidgets 3 | 4 | class UiUtils: 5 | @staticmethod 6 | def create_styled_button(text, callback, color, size=(200, 50), font_size=10): 7 | """Create a button with consistent styling""" 8 | button = QtWidgets.QPushButton(text) 9 | button.clicked.connect(callback) 10 | button.setStyleSheet(f""" 11 | QPushButton {{ 12 | background-color: {color}; 13 | border: 1px solid black; 14 | padding: 5px; 15 | font-size: {font_size}px; 16 | border-radius: 5px; 17 | }} 18 | QPushButton:hover {{ 19 | background-color: {darken_color(color, 20)}; 20 | }} 21 | """) 22 | button.setFixedSize(size[0], size[1]) 23 | return button 24 | 25 | @staticmethod 26 | def format_memory_display(memory): 27 | """Format a memory dictionary for display with colored boxes based on valence""" 28 | if not UiUtils.is_displayable_memory(memory): 29 | return "" 30 | 31 | # Get the display text - prefer formatted_value, fall back to value 32 | display_text = memory.get('formatted_value', str(memory.get('value', ''))) 33 | 34 | # Skip if the display text contains just a timestamp 35 | if 'timestamp' in display_text.lower() and len(display_text.split()) < 3: 36 | return "" 37 | 38 | timestamp = memory.get('timestamp', '') 39 | if isinstance(timestamp, str): 40 | try: 41 | timestamp = datetime.fromisoformat(timestamp).strftime("%H:%M:%S") 42 | except: 43 | timestamp = "" 44 | 45 | # Determine valence and color 46 | if memory.get('category') == 'mental_state' and memory.get('key') == 'startled': 47 | interaction_type = "Negative" 48 | background_color = "#FFD1DC" # Pastel red 49 | elif isinstance(memory.get('raw_value'), dict): 50 | total_effect = sum(float(val) for val in memory['raw_value'].values() 51 | if isinstance(val, (int, float))) 52 | if total_effect > 0: 53 | interaction_type = "Positive" 54 | background_color = "#D1FFD1" # Pastel green 55 | elif total_effect < 0: 56 | interaction_type = "Negative" 57 | background_color = "#FFD1DC" # Pastel red 58 | else: 59 | interaction_type = "Neutral" 60 | background_color = "#FFFACD" # Pastel yellow 61 | else: 62 | interaction_type = "Neutral" 63 | background_color = "#FFFACD" # Pastel yellow 64 | 65 | # Create HTML formatted memory box 66 | formatted_memory = f""" 67 |
74 |
{interaction_type}
75 |
{display_text}
76 |
{timestamp}
77 |
78 | """ 79 | 80 | return formatted_memory 81 | 82 | @staticmethod 83 | def _is_displayable_memory(self, memory): 84 | """Check if a memory should be displayed in the UI""" 85 | if not isinstance(memory, dict): 86 | return False 87 | 88 | # Skip timestamp-only memories (they have numeric keys) 89 | if isinstance(memory.get('key'), str) and memory['key'].isdigit(): 90 | return False 91 | 92 | # Skip memories that don't have a proper category or value 93 | if not memory.get('category') or not memory.get('value'): 94 | return False 95 | 96 | # Skip memories where the value is just a timestamp number 97 | if isinstance(memory.get('value'), (int, float)) and 'timestamp' in str(memory['value']).lower(): 98 | return False 99 | 100 | # Must have either formatted_value or a displayable string value 101 | if 'formatted_value' not in memory and not isinstance(memory.get('value'), str): 102 | return False 103 | 104 | return True 105 | 106 | @staticmethod 107 | def create_memory_card(memory): 108 | """Create a styled HTML memory card""" 109 | # Determine card style 110 | bg_color, border_color = UiUtils.get_memory_colors(memory) 111 | 112 | # Format card HTML 113 | card_html = f""" 114 |
122 |
{memory.get('category', 'unknown').capitalize()}
123 |
{memory.get('formatted_value', '')[:60]}
124 |
125 | {memory.get('timestamp', '').split(' ')[-1]} 126 |
127 |
128 | """ 129 | 130 | return card_html 131 | 132 | @staticmethod 133 | def get_memory_colors(memory): 134 | """Determine colors based on memory content""" 135 | if 'positive' in memory.get('tags', []): 136 | return "#E8F5E9", "#C8E6C9" # Green shades 137 | elif 'negative' in memory.get('tags', []): 138 | return "#FFEBEE", "#FFCDD2" # Red shades 139 | elif 'novelty' in memory.get('tags', []): 140 | return "#FFFDE7", "#FFF9C4" # Yellow shades 141 | return "#F5F5F5", "#EEEEEE" # Default gray 142 | 143 | @staticmethod 144 | def create_info_box(title, content, icon_path=None, bg_color="#f8f9fa"): 145 | """Create a styled information box with optional icon""" 146 | box = QtWidgets.QGroupBox(title) 147 | box.setStyleSheet(f""" 148 | QGroupBox {{ 149 | background-color: {bg_color}; 150 | border-radius: 8px; 151 | border: 1px solid #dee2e6; 152 | margin-top: 15px; 153 | padding: 10px; 154 | font-weight: bold; 155 | }} 156 | QGroupBox::title {{ 157 | subcontrol-origin: margin; 158 | left: 10px; 159 | padding: 0 5px; 160 | color: #495057; 161 | }} 162 | """) 163 | 164 | box_layout = QtWidgets.QVBoxLayout(box) 165 | 166 | # Add icon if provided 167 | if icon_path: 168 | icon_label = QtWidgets.QLabel() 169 | icon_label.setPixmap(QtGui.QPixmap(icon_path).scaled(24, 24, QtCore.Qt.KeepAspectRatio)) 170 | box_layout.addWidget(icon_label, alignment=QtCore.Qt.AlignRight) 171 | 172 | # Add content 173 | content_label = QtWidgets.QLabel(content) 174 | content_label.setTextFormat(QtCore.Qt.RichText) 175 | content_label.setWordWrap(True) 176 | content_label.setStyleSheet("font-weight: normal; color: #343a40;") 177 | box_layout.addWidget(content_label) 178 | 179 | return box 180 | 181 | # Utility functions 182 | def darken_color(color, amount=20): 183 | """Darken a hex color by the specified amount""" 184 | # Remove # if present 185 | color = color.lstrip('#') 186 | 187 | # Convert to RGB 188 | r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) 189 | 190 | # Darken 191 | r = max(0, r - amount) 192 | g = max(0, g - amount) 193 | b = max(0, b - amount) 194 | 195 | # Convert back to hex 196 | return f"#{r:02x}{g:02x}{b:02x}" -------------------------------------------------------------------------------- /src/brain_utils.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | 3 | class ConsoleOutput: 4 | def __init__(self, text_edit): 5 | self.text_edit = text_edit 6 | 7 | def write(self, text): 8 | cursor = self.text_edit.textCursor() 9 | format = QtGui.QTextCharFormat() 10 | 11 | if text.startswith("Previous value:"): 12 | format.setForeground(QtGui.QColor("red")) 13 | elif text.startswith("New value:"): 14 | format.setForeground(QtGui.QColor("green")) 15 | else: 16 | format.setForeground(QtGui.QColor("black")) 17 | 18 | cursor.insertText(text, format) 19 | self.text_edit.setTextCursor(cursor) 20 | self.text_edit.ensureCursorVisible() 21 | 22 | def flush(self): 23 | pass 24 | 25 | -------------------------------------------------------------------------------- /src/certificate.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from PyQt5 import QtCore, QtGui, QtWidgets 3 | 4 | class SquidCertificateWindow(QtWidgets.QDialog): 5 | def __init__(self, parent=None, tamagotchi_logic=None): 6 | super().__init__(parent) 7 | self.tamagotchi_logic = tamagotchi_logic 8 | self.setWindowTitle("Squid Certificate") 9 | self.setMinimumSize(800, 1000) # Increased minimum size 10 | 11 | layout = QtWidgets.QVBoxLayout(self) 12 | 13 | self.certificate_view = QtWidgets.QTextBrowser() 14 | self.certificate_view.setOpenExternalLinks(False) 15 | layout.addWidget(self.certificate_view) 16 | 17 | # Add print button with larger text 18 | print_button = QtWidgets.QPushButton("Print Certificate") 19 | print_button.setStyleSheet("font-size: 18px; padding: 10px;") 20 | print_button.clicked.connect(self.print_certificate) 21 | layout.addWidget(print_button, alignment=QtCore.Qt.AlignRight) 22 | 23 | self.update_certificate() 24 | 25 | def update_certificate(self): 26 | if not self.tamagotchi_logic or not self.tamagotchi_logic.squid: 27 | return 28 | 29 | squid = self.tamagotchi_logic.squid 30 | current_date = datetime.datetime.now().strftime("%B %d, %Y") 31 | personality = str(squid.personality).split('.')[-1].lower().capitalize() 32 | squid_name = getattr(squid, 'name', 'Squid') 33 | 34 | certificate_html = f""" 35 | 36 | 37 | 150 | 151 | 152 |
153 |
154 |

Certificate of Squidship

155 |

Presented by the International Dosidicus Society

156 |
157 | 158 |
159 |

This certifies that

160 |

{squid_name}

161 |

is an officially recognized Dosidicus electronicae of the

162 |

{personality} Personality Type

163 |
164 | 165 |
166 |

Statistics

167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
Happiness: {squid.happiness}/100Hunger: {squid.hunger}/100
Cleanliness: {squid.cleanliness}/100Sleepiness: {squid.sleepiness}/100
Anxiety: {squid.anxiety}/100Curiosity: {squid.curiosity}/100
181 |
182 | 183 |
184 |

Achievements:

185 | 191 |
192 | 193 |
194 |

Issued on this day, {current_date}

195 |
OFFICIAL
196 |
197 |
198 | 199 | 200 | """ 201 | 202 | self.certificate_view.setHtml(certificate_html) 203 | 204 | def print_certificate(self): 205 | from PyQt5 import QtPrintSupport 206 | printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution) 207 | dialog = QtPrintSupport.QPrintDialog(printer, self) 208 | if dialog.exec_() == QtWidgets.QDialog.Accepted: 209 | self.certificate_view.print_(printer) -------------------------------------------------------------------------------- /src/config_manager.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | import random 4 | from PyQt5 import QtCore 5 | from ast import literal_eval 6 | 7 | class ConfigManager: 8 | def __init__(self, config_path="config.ini"): 9 | self.config_path = config_path 10 | self.config = configparser.ConfigParser() 11 | self.load_config() 12 | 13 | def load_config(self): 14 | if not os.path.exists(self.config_path): 15 | self.create_default_config() 16 | self.config.read(self.config_path) 17 | 18 | def create_default_config(self): 19 | # Rock Interactions 20 | self.config['RockInteractions'] = { 21 | 'pickup_probability': '0.9', 22 | 'throw_probability': '0.8', 23 | 'min_carry_duration': '3.0', 24 | 'max_carry_duration': '8.0', 25 | 'cooldown_after_throw': '5.0', 26 | 'happiness_boost': '15', 27 | 'satisfaction_boost': '20', 28 | 'anxiety_reduction': '10', 29 | 'memory_decay_rate': '0.95', 30 | 'max_rock_memories': '5' 31 | } 32 | 33 | # Neurogenesis 34 | self.config['Neurogenesis'] = { 35 | 'enabled': 'True', 36 | 'cooldown': '300.0', 37 | 'max_neurons': '20', 38 | 'initial_neuron_count': '7' 39 | } 40 | 41 | # Neurogenesis Triggers 42 | self.config['Neurogenesis.Novelty'] = { 43 | 'enabled': 'True', 44 | 'threshold': '0.7', 45 | 'decay_rate': '0.95', 46 | 'max_counter': '10.0', 47 | 'min_curiosity': '0.3', 48 | 'adventurous_modifier': '1.2', 49 | 'timid_modifier': '0.8' 50 | } 51 | 52 | self.config['Neurogenesis.Stress'] = { 53 | 'enabled': 'True', 54 | 'threshold': '0.8', 55 | 'decay_rate': '0.9', 56 | 'max_counter': '10.0', 57 | 'min_anxiety': '0.4', 58 | 'timid_modifier': '1.5', 59 | 'energetic_modifier': '0.7' 60 | } 61 | 62 | self.config['Neurogenesis.Reward'] = { 63 | 'enabled': 'True', 64 | 'threshold': '0.6', 65 | 'decay_rate': '0.85', 66 | 'max_counter': '10.0', 67 | 'min_satisfaction': '0.5', 68 | 'boost_multiplier': '1.1' 69 | } 70 | 71 | # Neuron Properties 72 | self.config['Neurogenesis.NeuronProperties'] = { 73 | 'base_activation': '0.5', 74 | 'position_variance': '50', 75 | 'default_connections': 'True', 76 | 'connection_strength': '0.3', 77 | 'reciprocal_strength': '0.15' 78 | } 79 | 80 | # Appearance 81 | self.config['Neurogenesis.Appearance'] = { 82 | 'novelty_color': '255,255,150', 83 | 'stress_color': '255,150,150', 84 | 'reward_color': '150,255,150', 85 | 'novelty_shape': 'triangle', 86 | 'stress_shape': 'square', 87 | 'reward_shape': 'circle' 88 | } 89 | 90 | # Visual Effects 91 | self.config['Neurogenesis.VisualEffects'] = { 92 | 'highlight_duration': '5.0', 93 | 'highlight_radius': '40', 94 | 'pulse_effect': 'True', 95 | 'pulse_speed': '0.5' 96 | } 97 | 98 | with open(self.config_path, 'w') as f: 99 | self.config.write(f) 100 | 101 | def get_rock_config(self): 102 | return { 103 | 'pickup_prob': float(self.config['RockInteractions']['pickup_probability']), 104 | 'throw_prob': float(self.config['RockInteractions']['throw_probability']), 105 | 'min_carry_duration': float(self.config['RockInteractions']['min_carry_duration']), 106 | 'max_carry_duration': float(self.config['RockInteractions']['max_carry_duration']), 107 | 'cooldown_after_throw': float(self.config['RockInteractions']['cooldown_after_throw']), 108 | 'happiness_boost': int(self.config['RockInteractions']['happiness_boost']), 109 | 'satisfaction_boost': int(self.config['RockInteractions']['satisfaction_boost']), 110 | 'anxiety_reduction': int(self.config['RockInteractions']['anxiety_reduction']), 111 | 'memory_decay_rate': float(self.config['RockInteractions']['memory_decay_rate']), 112 | 'max_rock_memories': int(self.config['RockInteractions']['max_rock_memories']) 113 | } 114 | 115 | def get_poop_config(self): 116 | return { 117 | 'min_carry_duration': 2.0, 118 | 'max_carry_duration': 9.0, 119 | 'pickup_prob': 0.2, 120 | 'throw_prob': 0.3, 121 | 'happiness_penalty': 5, 122 | 'anxiety_increase': 10 123 | } 124 | 125 | def get_neurogenesis_config(self): 126 | """Returns the complete neurogenesis configuration as a dictionary""" 127 | return { 128 | 'general': { 129 | 'enabled': self.config.getboolean('Neurogenesis', 'enabled'), 130 | 'cooldown': self.config.getfloat('Neurogenesis', 'cooldown'), 131 | 'max_neurons': self.config.getint('Neurogenesis', 'max_neurons'), 132 | 'initial_neuron_count': self.config.getint('Neurogenesis', 'initial_neuron_count') 133 | }, 134 | 'triggers': { 135 | 'novelty': { 136 | 'enabled': self.config.getboolean('Neurogenesis.Novelty', 'enabled'), 137 | 'threshold': self.config.getfloat('Neurogenesis.Novelty', 'threshold'), 138 | 'decay_rate': self.config.getfloat('Neurogenesis.Novelty', 'decay_rate'), 139 | 'max_counter': self.config.getfloat('Neurogenesis.Novelty', 'max_counter'), 140 | 'min_curiosity': self.config.getfloat('Neurogenesis.Novelty', 'min_curiosity'), 141 | 'personality_modifiers': { 142 | 'adventurous': self.config.getfloat('Neurogenesis.Novelty', 'adventurous_modifier'), 143 | 'timid': self.config.getfloat('Neurogenesis.Novelty', 'timid_modifier') 144 | } 145 | }, 146 | 'stress': { 147 | 'enabled': self.config.getboolean('Neurogenesis.Stress', 'enabled'), 148 | 'threshold': self.config.getfloat('Neurogenesis.Stress', 'threshold'), 149 | 'decay_rate': self.config.getfloat('Neurogenesis.Stress', 'decay_rate'), 150 | 'max_counter': self.config.getfloat('Neurogenesis.Stress', 'max_counter'), 151 | 'min_anxiety': self.config.getfloat('Neurogenesis.Stress', 'min_anxiety'), 152 | 'personality_modifiers': { 153 | 'timid': self.config.getfloat('Neurogenesis.Stress', 'timid_modifier'), 154 | 'energetic': self.config.getfloat('Neurogenesis.Stress', 'energetic_modifier') 155 | } 156 | }, 157 | 'reward': { 158 | 'enabled': self.config.getboolean('Neurogenesis.Reward', 'enabled'), 159 | 'threshold': self.config.getfloat('Neurogenesis.Reward', 'threshold'), 160 | 'decay_rate': self.config.getfloat('Neurogenesis.Reward', 'decay_rate'), 161 | 'max_counter': self.config.getfloat('Neurogenesis.Reward', 'max_counter'), 162 | 'min_satisfaction': self.config.getfloat('Neurogenesis.Reward', 'min_satisfaction'), 163 | 'boost_multiplier': self.config.getfloat('Neurogenesis.Reward', 'boost_multiplier') 164 | } 165 | }, 166 | 'neuron_properties': { 167 | 'base_activation': self.config.getfloat('Neurogenesis.NeuronProperties', 'base_activation'), 168 | 'position_variance': self.config.getint('Neurogenesis.NeuronProperties', 'position_variance'), 169 | 'default_connections': self.config.getboolean('Neurogenesis.NeuronProperties', 'default_connections'), 170 | 'connection_strength': self.config.getfloat('Neurogenesis.NeuronProperties', 'connection_strength'), 171 | 'reciprocal_strength': self.config.getfloat('Neurogenesis.NeuronProperties', 'reciprocal_strength') 172 | }, 173 | 'appearance': { 174 | 'colors': { 175 | 'novelty': [int(x) for x in self.config['Neurogenesis.Appearance']['novelty_color'].split(',')], 176 | 'stress': [int(x) for x in self.config['Neurogenesis.Appearance']['stress_color'].split(',')], 177 | 'reward': [int(x) for x in self.config['Neurogenesis.Appearance']['reward_color'].split(',')] 178 | }, 179 | 'shapes': { 180 | 'novelty': self.config['Neurogenesis.Appearance']['novelty_shape'], 181 | 'stress': self.config['Neurogenesis.Appearance']['stress_shape'], 182 | 'reward': self.config['Neurogenesis.Appearance']['reward_shape'] 183 | } 184 | }, 185 | 'visual_effects': { 186 | 'highlight_duration': self.config.getfloat('Neurogenesis.VisualEffects', 'highlight_duration'), 187 | 'highlight_radius': self.config.getint('Neurogenesis.VisualEffects', 'highlight_radius'), 188 | 'pulse_effect': self.config.getboolean('Neurogenesis.VisualEffects', 'pulse_effect'), 189 | 'pulse_speed': self.config.getfloat('Neurogenesis.VisualEffects', 'pulse_speed') 190 | } 191 | } 192 | 193 | def get_random_carry_duration(self): 194 | """Returns random duration between min and max carry duration""" 195 | config = self.get_rock_config() 196 | return random.uniform(config['min_carry_duration'], config['max_carry_duration']) 197 | 198 | def _parse_config_value(self, value): 199 | """Parse configuration values that might contain comments""" 200 | # Remove everything after comment markers 201 | for comment_marker in [';', '#', '//']: 202 | if comment_marker in value: 203 | value = value.split(comment_marker)[0] 204 | 205 | value = value.strip() 206 | 207 | # Try to convert to appropriate type 208 | if value.lower() == 'true': 209 | return True 210 | elif value.lower() == 'false': 211 | return False 212 | elif value.isdigit(): 213 | return int(value) 214 | try: 215 | return float(value) 216 | except ValueError: 217 | return value -------------------------------------------------------------------------------- /src/decision_engine.py: -------------------------------------------------------------------------------- 1 | # Decision engine version 2.1 April 2025 2 | 3 | import random 4 | from .personality import Personality 5 | 6 | class DecisionEngine: 7 | def __init__(self, squid): 8 | self.squid = squid 9 | 10 | def make_decision(self): 11 | """Decision-making based primarily on neural network state with minimal hardcoding""" 12 | # Get current state as a complete picture 13 | current_state = { 14 | "hunger": self.squid.hunger, 15 | "happiness": self.squid.happiness, 16 | "cleanliness": self.squid.cleanliness, 17 | "sleepiness": self.squid.sleepiness, 18 | "satisfaction": self.squid.satisfaction, 19 | "anxiety": self.squid.anxiety, 20 | "curiosity": self.squid.curiosity, 21 | "is_sick": self.squid.is_sick, 22 | "is_sleeping": self.squid.is_sleeping, 23 | "has_food_visible": bool(self.squid.get_visible_food()), 24 | "carrying_rock": self.squid.carrying_rock, 25 | "carrying_poop": self.squid.carrying_poop, 26 | "rock_throw_cooldown": getattr(self.squid, 'rock_throw_cooldown', 0), 27 | "poop_throw_cooldown": getattr(self.squid, 'poop_throw_cooldown', 0) 28 | } 29 | 30 | # Get brain network state - this provides opportunity for emergent behavior 31 | brain_state = self.squid.tamagotchi_logic.squid_brain_window.brain_widget.state 32 | 33 | # Collect active memories to influence decisions 34 | active_memories = self.squid.memory_manager.get_active_memories_data(3) 35 | memory_influence = {} 36 | for memory in active_memories: 37 | if isinstance(memory.get('raw_value'), dict): 38 | for key, value in memory['raw_value'].items(): 39 | if key in memory_influence: 40 | memory_influence[key] += value * 0.5 # Memory has half weight of current state 41 | else: 42 | memory_influence[key] = value * 0.5 43 | 44 | # Apply memory influence to current state 45 | for key, value in memory_influence.items(): 46 | if key in current_state and isinstance(current_state[key], (int, float)): 47 | current_state[key] = min(100, max(0, current_state[key] + value)) 48 | 49 | # Check for extreme conditions that should override neural decisions 50 | if self.squid.sleepiness >= 95: 51 | self.squid.go_to_sleep() 52 | return "exhausted" 53 | 54 | if self.squid.is_sleeping: 55 | if self.squid.sleepiness > 90: 56 | return "sleeping deeply" 57 | else: 58 | return "sleeping peacefully" 59 | 60 | # Check for emotional state overrides 61 | if self.squid.anxiety > 80: 62 | return "extremely anxious" 63 | elif self.squid.anxiety > 60: 64 | return "anxious" 65 | elif self.squid.anxiety > 40: 66 | return "nervous" 67 | 68 | if self.squid.curiosity > 80: 69 | return "extremely curious" 70 | elif self.squid.curiosity > 60: 71 | return "curious" 72 | elif self.squid.curiosity > 40: 73 | return "inquisitive" 74 | 75 | if self.squid.happiness > 80 and self.squid.anxiety < 30: 76 | return "content" 77 | 78 | if self.squid.happiness > 80 and self.squid.curiosity > 60: 79 | return "excited" 80 | 81 | if self.squid.happiness < 30: 82 | return "grumpy" 83 | 84 | if self.squid.satisfaction > 80: 85 | return "satisfied" 86 | 87 | if (self.squid.sleepiness > 70): 88 | return "drowsy" 89 | 90 | if self.squid.is_sick: 91 | return "feeling sick" 92 | 93 | # Calculate decision weights for each possible action based on neural state 94 | decision_weights = { 95 | "exploring": brain_state.get("curiosity", 50) * 0.8 * (1 - (brain_state.get("anxiety", 50) / 100)), 96 | "eating": brain_state.get("hunger", 50) * 1.2 if self.squid.get_visible_food() else 0, 97 | "approaching_rock": brain_state.get("curiosity", 50) * 0.7 if not self.squid.carrying_rock else 0, 98 | "throwing_rock": brain_state.get("satisfaction", 50) * 0.7 if self.squid.carrying_rock else 0, 99 | "approaching_poop": brain_state.get("curiosity", 50) * 0.7 if not self.squid.carrying_poop and len(self.squid.tamagotchi_logic.poop_items) > 0 else 0, 100 | "throwing_poop": brain_state.get("satisfaction", 50) * 0.7 if self.squid.carrying_poop else 0, 101 | "avoiding_threat": brain_state.get("anxiety", 50) * 0.9, 102 | "organizing": brain_state.get("satisfaction", 50) * 0.5 103 | } 104 | 105 | # Personality modifiers 106 | if self.squid.personality == Personality.TIMID: 107 | decision_weights["avoiding_threat"] *= 1.5 108 | decision_weights["approaching_rock"] *= 0.7 109 | decision_weights["approaching_poop"] *= 0.7 110 | elif self.squid.personality == Personality.ADVENTUROUS: 111 | decision_weights["exploring"] *= 1.3 112 | decision_weights["approaching_rock"] *= 1.2 113 | decision_weights["approaching_poop"] *= 1.2 114 | elif self.squid.personality == Personality.GREEDY: 115 | decision_weights["eating"] *= 1.5 116 | 117 | # Add randomness to create more unpredictable behavior 118 | for key in decision_weights: 119 | decision_weights[key] *= random.uniform(0.85, 1.15) 120 | 121 | # Choose the highest weighted decision 122 | best_decision = max(decision_weights, key=decision_weights.get) 123 | 124 | # Implement the chosen decision with expanded status descriptions 125 | if best_decision == "eating" and self.squid.get_visible_food(): 126 | closest_food = min(self.squid.get_visible_food(), 127 | key=lambda f: self.squid.distance_to(f[0], f[1])) 128 | self.squid.move_towards(closest_food[0], closest_food[1]) 129 | 130 | # Food-specific statuses depending on hunger and distance 131 | food_distance = self.squid.distance_to(closest_food[0], closest_food[1]) 132 | if food_distance > 100: 133 | return "eyeing food" 134 | elif food_distance > 50: 135 | if self.squid.hunger > 70: 136 | return "approaching food eagerly" 137 | else: 138 | return "cautiously approaching food" 139 | else: 140 | return "moving toward food" 141 | 142 | elif best_decision == "approaching_rock" and not self.squid.carrying_rock: 143 | nearby_rocks = [d for d in self.squid.tamagotchi_logic.get_nearby_decorations( 144 | self.squid.squid_x, self.squid.squid_y, 150) 145 | if getattr(d, 'can_be_picked_up', False)] 146 | if nearby_rocks: 147 | self.squid.current_rock_target = random.choice(nearby_rocks) 148 | 149 | rock_distance = self.squid.distance_to( 150 | self.squid.current_rock_target.pos().x(), 151 | self.squid.current_rock_target.pos().y()) 152 | 153 | if rock_distance > 70: 154 | return "interested in rock" 155 | else: 156 | return "examining rock curiously" 157 | 158 | elif best_decision == "throwing_rock" and self.squid.carrying_rock: 159 | direction = random.choice(["left", "right"]) 160 | if self.squid.throw_rock(direction): 161 | if random.random() < 0.3: 162 | return "tossing rock around" 163 | else: 164 | return "playfully throwing rock" 165 | 166 | elif best_decision == "approaching_poop" and not self.squid.carrying_poop: 167 | nearby_poops = [d for d in self.squid.tamagotchi_logic.poop_items 168 | if self.squid.distance_to(d.pos().x(), d.pos().y()) < 150] 169 | if nearby_poops: 170 | self.squid.current_poop_target = random.choice(nearby_poops) 171 | return "approaching poop" 172 | 173 | elif best_decision == "throwing_poop" and self.squid.carrying_poop: 174 | direction = random.choice(["left", "right"]) 175 | if self.squid.throw_poop(direction): 176 | return "throwing poop" 177 | 178 | elif best_decision == "organizing" and self.squid.should_organize_decorations(): 179 | action = self.squid.organize_decorations() 180 | if action == "hoarding": 181 | if self.squid.personality == Personality.GREEDY: 182 | return "hoarding items" 183 | else: 184 | return "organizing decorations" 185 | elif action == "approaching_decoration": 186 | return "redecorating" 187 | else: 188 | return "arranging environment" 189 | 190 | elif best_decision == "avoiding_threat" and self.squid.anxiety > 70: 191 | # Move away from potential threats 192 | if len(self.squid.tamagotchi_logic.poop_items) > 0: 193 | self.squid.move_erratically() 194 | return "feeling uncomfortable" 195 | if self.squid.personality == Personality.TIMID: 196 | if self.squid.is_near_plant(): 197 | return "hiding behind plant" 198 | else: 199 | return "nervously watching" 200 | return "hiding" 201 | 202 | # Default to exploration with varying patterns 203 | # Create more descriptive exploration states 204 | exploration_options = [] 205 | 206 | # Add personality-specific exploration options 207 | if self.squid.personality == Personality.TIMID: 208 | exploration_options.extend(["cautiously exploring", "nervously watching"]) 209 | elif self.squid.personality == Personality.ADVENTUROUS: 210 | exploration_options.extend(["boldly exploring", "seeking adventure", "investigating bravely"]) 211 | elif self.squid.personality == Personality.GREEDY: 212 | exploration_options.extend(["searching for treasures", "eagerly collecting"]) 213 | elif self.squid.personality == Personality.STUBBORN: 214 | exploration_options.extend(["stubbornly patrolling", "demanding attention"]) 215 | elif self.squid.personality == Personality.LAZY: 216 | exploration_options.extend(["resting comfortably", "conserving energy", "lounging"]) 217 | elif self.squid.personality == Personality.ENERGETIC: 218 | exploration_options.extend(["zooming around", "buzzing with energy", "restlessly swimming"]) 219 | 220 | # Add general exploration options 221 | exploration_options.extend([ 222 | "exploring surroundings", 223 | "wandering aimlessly", 224 | "patrolling territory", 225 | "swimming lazily", 226 | "investigating" 227 | ]) 228 | 229 | # Select a random exploration style 230 | exploration_style = random.choice(exploration_options) 231 | 232 | if exploration_style in ["resting comfortably", "conserving energy", "lounging"]: 233 | self.squid.move_slowly() 234 | elif exploration_style in ["zooming around", "buzzing with energy", "restlessly swimming"]: 235 | self.squid.move_erratically() 236 | else: 237 | self.squid.move_randomly() 238 | 239 | return exploration_style -------------------------------------------------------------------------------- /src/decoration_stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "plant01.png": { 3 | "happiness": 2, 4 | "cleanliness": 1, 5 | "anxiety": -2, 6 | "category": "plant" 7 | }, 8 | "plant02.png": { 9 | "happiness": 2, 10 | "cleanliness": 1, 11 | "anxiety": -2, 12 | "category": "plant" 13 | }, 14 | "plant03.png": { 15 | "happiness": 2, 16 | "cleanliness": 1, 17 | "anxiety": -2, 18 | "category": "plant" 19 | }, 20 | "plant04.png": { 21 | "happiness": 2, 22 | "cleanliness": 1, 23 | "anxiety": -2, 24 | "category": "plant" 25 | }, 26 | "plant05.png": { 27 | "happiness": 2, 28 | "cleanliness": 1, 29 | "anxiety": -2, 30 | "category": "plant" 31 | }, 32 | "plant06.png": { 33 | "happiness": 2, 34 | "cleanliness": 1, 35 | "category": "plant" 36 | }, 37 | "plant07.png": { 38 | "happiness": 2, 39 | "cleanliness": 2, 40 | "anxiety": -2, 41 | "category": "plant" 42 | }, 43 | "plant08.png": { 44 | "happiness": 2, 45 | "cleanliness": 2, 46 | "anxiety": -2, 47 | "category": "plant" 48 | }, 49 | "plant09.png": { 50 | "happiness": 1, 51 | "cleanliness": 2, 52 | "anxiety": -2, 53 | "category": "plant" 54 | }, 55 | "plant10.png": { 56 | "happiness": 1, 57 | "cleanliness": 2, 58 | "anxiety": -2, 59 | "category": "plant" 60 | }, 61 | "plant11.png": { 62 | "happiness": 1, 63 | "cleanliness": 2, 64 | "anxiety": -2, 65 | "category": "plant" 66 | }, 67 | "sml_plant01.png": { 68 | "happiness": 1, 69 | "cleanliness": 2, 70 | "anxiety": -3, 71 | "category": "plant" 72 | }, 73 | "sml_plant02.png": { 74 | "happiness": 1, 75 | "cleanliness": 2, 76 | "anxiety": -3, 77 | "category": "plant" 78 | }, 79 | "rock01.png": { 80 | "happiness": 1, 81 | "satisfaction": 1, 82 | "category": "rock" 83 | }, 84 | "rock02.png": { 85 | "happiness": 1, 86 | "satisfaction": 1, 87 | "category": "rock" 88 | }, 89 | "rock03.png": { 90 | "happiness": 1, 91 | "satisfaction": 2, 92 | "category": "rock" 93 | } 94 | } -------------------------------------------------------------------------------- /src/display_scaling.py: -------------------------------------------------------------------------------- 1 | # Create new file: display_scaling.py 2 | class DisplayScaling: 3 | DESIGN_WIDTH = 2880 4 | DESIGN_HEIGHT = 1920 5 | _scale_factor = 1.0 6 | 7 | # Default scale factor (1.0 means no scaling) 8 | _scale_factor = 1.0 9 | 10 | @classmethod 11 | def initialize(cls, current_width, current_height): 12 | width_ratio = current_width / cls.DESIGN_WIDTH 13 | height_ratio = current_height / cls.DESIGN_HEIGHT 14 | base_scale_factor = min(width_ratio, height_ratio) 15 | 16 | if current_width <= 1920 and current_height <= 1080: 17 | cls._scale_factor = base_scale_factor * 0.85 18 | print(f"1080p display detected: applying 85% scaling (factor={cls._scale_factor:.2f})") 19 | else: 20 | cls._scale_factor = base_scale_factor 21 | print(f"High resolution display ({current_width}x{current_height}): standard scaling (factor={cls._scale_factor:.2f})") 22 | 23 | @classmethod 24 | def scale(cls, value): 25 | return int(value * cls._scale_factor) 26 | 27 | @classmethod 28 | def font_size(cls, size): 29 | scaled = cls.scale(size) 30 | return max(8, scaled) 31 | 32 | @classmethod 33 | def get_scale_factor(cls): 34 | return cls._scale_factor 35 | 36 | @classmethod 37 | def scale_css(cls, css_string): 38 | import re 39 | pattern = r'font-size:\s*(\d+)px' 40 | def replace_size(match): 41 | original_size = int(match.group(1)) 42 | scaled_size = cls.font_size(original_size) 43 | return f'font-size: {scaled_size}px' 44 | return re.sub(pattern, replace_size, css_string) -------------------------------------------------------------------------------- /src/image_cache.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtGui 2 | 3 | class ImageCache: 4 | """Global image cache to prevent duplicate loading""" 5 | _cache = {} 6 | 7 | @classmethod 8 | def get_pixmap(cls, path): 9 | """Get a pixmap from cache or load it""" 10 | if path not in cls._cache: 11 | pixmap = QtGui.QPixmap(path) 12 | cls._cache[path] = pixmap 13 | return cls._cache[path] 14 | 15 | @classmethod 16 | def clear(cls): 17 | """Clear cache to free memory""" 18 | cls._cache.clear() -------------------------------------------------------------------------------- /src/mental_states.py: -------------------------------------------------------------------------------- 1 | # Mental states 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | import os 5 | 6 | class MentalState: 7 | def __init__(self, name, icon_filename): 8 | self.name = name 9 | self.icon_filename = icon_filename 10 | self.is_active = False 11 | self.icon_item = None 12 | 13 | class MentalStateManager: 14 | def __init__(self, squid, scene): 15 | self.squid = squid 16 | self.scene = scene 17 | self.icon_offset = QtCore.QPointF(0, -100) # Offset for all icons above the squid 18 | self.mental_states_enabled = True 19 | 20 | self.mental_states = { # List of possible mental states: 21 | "sick": MentalState("sick", "sick.png"), # SICK 22 | "thinking": MentalState("thinking", "think.png"), # THINKING 23 | "startled": MentalState("startled", "startled.png"), # STARTLED 24 | "curious": MentalState("curious", "curious.png") # CURIOUS 25 | } 26 | 27 | def set_mental_states_enabled(self, enabled): 28 | self.mental_states_enabled = enabled 29 | if not enabled: 30 | self.clear_optional_states() 31 | 32 | def set_state(self, state_name, is_active): 33 | if state_name in self.mental_states: 34 | if state_name == "sick" or self.mental_states_enabled: 35 | self.mental_states[state_name].is_active = is_active 36 | self.update_mental_state_icons() 37 | 38 | def update_mental_state_icons(self): 39 | for state in self.mental_states.values(): 40 | if state.name == "sick" or self.mental_states_enabled: 41 | self.update_icon_state(state) 42 | 43 | def update_icon_state(self, state): 44 | if state.is_active: 45 | if state.icon_item is None: 46 | icon_pixmap = QtGui.QPixmap(os.path.join("images", state.icon_filename)) 47 | state.icon_item = QtWidgets.QGraphicsPixmapItem(icon_pixmap) 48 | self.scene.addItem(state.icon_item) 49 | self.update_icon_position(state.icon_item) 50 | else: 51 | if state.icon_item is not None: 52 | self.scene.removeItem(state.icon_item) 53 | state.icon_item = None 54 | 55 | def update_icon_position(self, icon_item): 56 | icon_item.setPos(self.squid.squid_x + self.squid.squid_width // 2 - icon_item.pixmap().width() // 2 + self.icon_offset.x(), 57 | self.squid.squid_y + self.icon_offset.y()) 58 | 59 | def update_positions(self): 60 | self.update_mental_state_icons() 61 | 62 | def is_state_active(self, state_name): 63 | if state_name == "sick" or self.mental_states_enabled: 64 | return self.mental_states.get(state_name, MentalState(state_name, "")).is_active 65 | return False 66 | 67 | def clear_optional_states(self): 68 | for state in self.mental_states.values(): 69 | if state.name != "sick": 70 | if state.icon_item is not None: 71 | self.scene.removeItem(state.icon_item) 72 | state.icon_item = None 73 | state.is_active = False -------------------------------------------------------------------------------- /src/personality.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class Personality(Enum): 4 | TIMID = "timid" 5 | ADVENTUROUS = "adventurous" 6 | LAZY = "lazy" 7 | ENERGETIC = "energetic" 8 | INTROVERT = "introvert" 9 | GREEDY = "greedy" 10 | STUBBORN = "stubborn" -------------------------------------------------------------------------------- /src/personality_traits.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from .personality import Personality 3 | 4 | personality_traits = {} 5 | 6 | def register_personality(name, decision_function, attribute_modifiers): 7 | # Base goal-oriented modifiers 8 | base_modifiers = { 9 | "organization_urgency": 1.0, 10 | "rock_interaction_chance": 1.0, 11 | "plant_seeking_urgency": 1.0, 12 | "goal_persistence": 1.0 13 | } 14 | 15 | # Personality-specific overrides 16 | 17 | 18 | if name == Personality.ADVENTUROUS: 19 | base_modifiers.update({ 20 | "organization_urgency": 1.8, 21 | "rock_interaction_chance": 2.2, 22 | "goal_persistence": 1.5, 23 | "exploration_bonus": 1.3 24 | }) 25 | elif name == Personality.TIMID: 26 | base_modifiers.update({ 27 | "organization_urgency": 0.6, 28 | "rock_interaction_chance": 0.4, 29 | "plant_seeking_urgency": 1.2, # Timid squids prefer plants 30 | "goal_persistence": 0.7 31 | }) 32 | elif name == Personality.LAZY: 33 | base_modifiers.update({ 34 | "organization_urgency": 0.3, 35 | "rock_interaction_chance": 0.8, 36 | "goal_persistence": 0.5 37 | }) 38 | elif name == Personality.ENERGETIC: 39 | base_modifiers.update({ 40 | "organization_urgency": 1.5, 41 | "rock_interaction_chance": 1.7, 42 | "goal_persistence": 1.8 43 | }) 44 | elif name == Personality.GREEDY: 45 | base_modifiers.update({ 46 | "organization_urgency": 0.9, 47 | "rock_interaction_chance": 1.1, 48 | "food_seeking_priority": 2.0 # Overrides other goals when hungry 49 | }) 50 | elif name == Personality.STUBBORN: 51 | base_modifiers.update({ 52 | "organization_urgency": 1.2, 53 | "rock_interaction_chance": 0.3, 54 | "goal_persistence": 2.0 # Very persistent once committed 55 | }) 56 | 57 | # Combine with passed modifiers 58 | attribute_modifiers.update(base_modifiers) 59 | 60 | personality_traits[name] = { 61 | "decision_function": decision_function, 62 | "attribute_modifiers": attribute_modifiers, 63 | "goal_weights": { 64 | "organize": base_modifiers["organization_urgency"], 65 | "interact": base_modifiers["rock_interaction_chance"], 66 | "clean": base_modifiers["plant_seeking_urgency"] 67 | } 68 | } 69 | return name 70 | 71 | # Register all personality types 72 | def register_all_personalities(): 73 | register_personality( 74 | Personality.TIMID, 75 | lambda squid: squid.anxiety * 1.5, # Decision function 76 | {"anxiety_growth": 1.3, "curiosity_growth": 0.7} 77 | ) 78 | 79 | register_personality( 80 | Personality.ADVENTUROUS, 81 | lambda squid: squid.curiosity * 1.8, 82 | {"curiosity_growth": 1.5, "exploration_boost": 1.4} 83 | ) 84 | 85 | register_personality( 86 | Personality.LAZY, 87 | lambda squid: squid.sleepiness * 1.2, 88 | {"energy_drain": 0.6, "movement_speed": 0.8} 89 | ) 90 | 91 | register_personality( 92 | Personality.ENERGETIC, 93 | lambda squid: 100 - squid.sleepiness, 94 | {"energy_drain": 1.4, "movement_speed": 1.3} 95 | ) 96 | 97 | register_personality( 98 | Personality.GREEDY, 99 | lambda squid: squid.hunger * 2.0, 100 | {"hunger_growth": 1.3, "satisfaction_decay": 1.2} 101 | ) 102 | 103 | register_personality( 104 | Personality.STUBBORN, 105 | lambda squid: 100 if squid.hunger > 70 else 30, 106 | {"food_preference": "sushi", "adaptability": 0.3} 107 | ) 108 | 109 | register_all_personalities() -------------------------------------------------------------------------------- /src/plugin_manager_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import os 3 | 4 | class PluginManagerDialog(QtWidgets.QDialog): 5 | def __init__(self, plugin_manager, parent=None): 6 | super().__init__(parent) 7 | self.plugin_manager = plugin_manager 8 | self.setWindowTitle("Plugin Manager") 9 | self.resize(600, 400) 10 | 11 | self.setup_ui() 12 | self.load_plugin_data() 13 | 14 | def setup_ui(self): 15 | # Main layout 16 | layout = QtWidgets.QVBoxLayout(self) 17 | 18 | # Plugin list 19 | self.plugin_list = QtWidgets.QListWidget() 20 | self.plugin_list.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) 21 | self.plugin_list.currentItemChanged.connect(self.on_plugin_selected) 22 | layout.addWidget(self.plugin_list, 2) 23 | 24 | # Plugin details group 25 | details_group = QtWidgets.QGroupBox("Plugin Details") 26 | details_layout = QtWidgets.QFormLayout(details_group) 27 | 28 | self.plugin_name = QtWidgets.QLabel() 29 | details_layout.addRow("Name:", self.plugin_name) 30 | 31 | self.plugin_version = QtWidgets.QLabel() 32 | details_layout.addRow("Version:", self.plugin_version) 33 | 34 | self.plugin_author = QtWidgets.QLabel() 35 | details_layout.addRow("Author:", self.plugin_author) 36 | 37 | self.plugin_description = QtWidgets.QLabel() 38 | self.plugin_description.setWordWrap(True) 39 | details_layout.addRow("Description:", self.plugin_description) 40 | 41 | self.plugin_requires = QtWidgets.QLabel() 42 | details_layout.addRow("Dependencies:", self.plugin_requires) 43 | 44 | self.plugin_status = QtWidgets.QLabel() 45 | details_layout.addRow("Status:", self.plugin_status) 46 | 47 | layout.addWidget(details_group, 1) 48 | 49 | # Actions group 50 | actions_group = QtWidgets.QGroupBox("Actions") 51 | actions_layout = QtWidgets.QHBoxLayout(actions_group) 52 | 53 | self.enable_button = QtWidgets.QPushButton("Enable") 54 | self.enable_button.clicked.connect(self.enable_selected_plugin) 55 | actions_layout.addWidget(self.enable_button) 56 | 57 | self.disable_button = QtWidgets.QPushButton("Disable") 58 | self.disable_button.clicked.connect(self.disable_selected_plugin) 59 | actions_layout.addWidget(self.disable_button) 60 | 61 | self.refresh_button = QtWidgets.QPushButton("Refresh") 62 | self.refresh_button.clicked.connect(self.load_plugin_data) 63 | actions_layout.addWidget(self.refresh_button) 64 | 65 | layout.addWidget(actions_group) 66 | 67 | # Close button 68 | self.close_button = QtWidgets.QPushButton("Close") 69 | self.close_button.clicked.connect(self.accept) 70 | layout.addWidget(self.close_button) 71 | 72 | def load_plugin_data(self): 73 | """Load plugin data into the list""" 74 | self.plugin_list.clear() 75 | 76 | # Load plugins from plugin_manager 77 | loaded_plugins = {} 78 | enabled_plugins = self.plugin_manager.get_enabled_plugins() 79 | 80 | # First, add loaded plugins 81 | for plugin_name in self.plugin_manager.get_loaded_plugins(): 82 | plugin_data = self.plugin_manager.plugins.get(plugin_name, {}) 83 | 84 | item = QtWidgets.QListWidgetItem(plugin_name) 85 | item.setData(QtCore.Qt.UserRole, plugin_data) 86 | 87 | # Set icon based on status 88 | if plugin_name in enabled_plugins: 89 | # Green dot for enabled 90 | item.setIcon(self.get_status_icon("enabled")) 91 | else: 92 | # Yellow dot for loaded but not enabled 93 | item.setIcon(self.get_status_icon("loaded")) 94 | 95 | self.plugin_list.addItem(item) 96 | loaded_plugins[plugin_name] = True 97 | 98 | # Then add discovered plugins that aren't loaded 99 | if hasattr(self.plugin_manager, '_discovered_plugins'): 100 | for plugin_name, plugin_data in self.plugin_manager._discovered_plugins.items(): 101 | if plugin_name not in loaded_plugins: 102 | item = QtWidgets.QListWidgetItem(plugin_name) 103 | item.setData(QtCore.Qt.UserRole, plugin_data) 104 | 105 | # Gray dot for discovered but not loaded 106 | item.setIcon(self.get_status_icon("discovered")) 107 | self.plugin_list.addItem(item) 108 | 109 | # Select the first plugin if available 110 | if self.plugin_list.count() > 0: 111 | self.plugin_list.setCurrentRow(0) 112 | else: 113 | self.clear_plugin_details() 114 | 115 | def get_status_icon(self, status): 116 | """Create a colored dot icon for the plugin status""" 117 | pixmap = QtGui.QPixmap(16, 16) 118 | pixmap.fill(QtCore.Qt.transparent) 119 | 120 | painter = QtGui.QPainter(pixmap) 121 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 122 | 123 | if status == "enabled": 124 | color = QtGui.QColor(0, 200, 0) # Green 125 | elif status == "loaded": 126 | color = QtGui.QColor(200, 200, 0) # Yellow 127 | else: 128 | color = QtGui.QColor(150, 150, 150) # Gray 129 | 130 | painter.setBrush(QtGui.QBrush(color)) 131 | painter.setPen(QtGui.QPen(QtCore.Qt.black, 1)) 132 | painter.drawEllipse(2, 2, 12, 12) 133 | painter.end() 134 | 135 | return QtGui.QIcon(pixmap) 136 | 137 | def on_plugin_selected(self, current, previous): 138 | """Handle plugin selection change""" 139 | if not current: 140 | self.clear_plugin_details() 141 | return 142 | 143 | # Get plugin data 144 | plugin_data = current.data(QtCore.Qt.UserRole) 145 | plugin_name = current.text() 146 | 147 | # Update details 148 | self.plugin_name.setText(plugin_data.get('name', plugin_name)) 149 | self.plugin_version.setText(plugin_data.get('version', 'Unknown')) 150 | self.plugin_author.setText(plugin_data.get('author', 'Unknown')) 151 | self.plugin_description.setText(plugin_data.get('description', 'No description available')) 152 | 153 | # Dependencies 154 | requires = plugin_data.get('requires', []) 155 | if requires: 156 | self.plugin_requires.setText(", ".join(requires)) 157 | else: 158 | self.plugin_requires.setText("None") 159 | 160 | # Status 161 | is_loaded = plugin_name in self.plugin_manager.get_loaded_plugins() 162 | is_enabled = plugin_name in self.plugin_manager.get_enabled_plugins() 163 | 164 | if is_enabled: 165 | self.plugin_status.setText("Enabled") 166 | self.plugin_status.setStyleSheet("color: green; font-weight: bold;") 167 | elif is_loaded: 168 | self.plugin_status.setText("Loaded (Not Enabled)") 169 | self.plugin_status.setStyleSheet("color: orange; font-weight: bold;") 170 | else: 171 | self.plugin_status.setText("Discovered (Not Loaded)") 172 | self.plugin_status.setStyleSheet("color: gray;") 173 | 174 | # Enable/disable buttons based on status 175 | self.enable_button.setEnabled(is_loaded and not is_enabled) 176 | self.disable_button.setEnabled(is_enabled) 177 | 178 | def clear_plugin_details(self): 179 | """Clear all plugin details""" 180 | self.plugin_name.clear() 181 | self.plugin_version.clear() 182 | self.plugin_author.clear() 183 | self.plugin_description.clear() 184 | self.plugin_requires.clear() 185 | self.plugin_status.clear() 186 | self.plugin_status.setStyleSheet("") 187 | 188 | self.enable_button.setEnabled(False) 189 | self.disable_button.setEnabled(False) 190 | 191 | def enable_selected_plugin(self): 192 | """Enable the selected plugin""" 193 | current_item = self.plugin_list.currentItem() 194 | if not current_item: 195 | return 196 | 197 | plugin_name = current_item.text() 198 | 199 | # Try to enable the plugin 200 | success = False 201 | 202 | # First check if plugin has a custom enable method 203 | if plugin_name in self.plugin_manager.plugins: 204 | plugin_instance = self.plugin_manager.plugins[plugin_name].get('instance') 205 | if plugin_instance and hasattr(plugin_instance, 'enable'): 206 | try: 207 | success = plugin_instance.enable() 208 | if success: 209 | self.plugin_manager.enable_plugin(plugin_name) 210 | except Exception as e: 211 | QtWidgets.QMessageBox.warning( 212 | self, 213 | "Error", 214 | f"Error enabling plugin {plugin_name}: {str(e)}" 215 | ) 216 | return 217 | else: 218 | success = self.plugin_manager.enable_plugin(plugin_name) 219 | 220 | if success: 221 | QtWidgets.QMessageBox.information( 222 | self, 223 | "Success", 224 | f"Plugin {plugin_name} enabled successfully" 225 | ) 226 | self.load_plugin_data() 227 | else: 228 | QtWidgets.QMessageBox.warning( 229 | self, 230 | "Error", 231 | f"Failed to enable plugin {plugin_name}" 232 | ) 233 | 234 | def disable_selected_plugin(self): 235 | """Disable the selected plugin""" 236 | current_item = self.plugin_list.currentItem() 237 | if not current_item: 238 | return 239 | 240 | plugin_name = current_item.text() 241 | 242 | # Try to disable the plugin 243 | success = self.plugin_manager.disable_plugin(plugin_name) 244 | 245 | if success: 246 | # Call custom disable method if available 247 | if plugin_name in self.plugin_manager.plugins: 248 | plugin_instance = self.plugin_manager.plugins[plugin_name].get('instance') 249 | if plugin_instance and hasattr(plugin_instance, 'disable'): 250 | try: 251 | plugin_instance.disable() 252 | except Exception as e: 253 | print(f"Error in plugin disable method: {e}") 254 | 255 | QtWidgets.QMessageBox.information( 256 | self, 257 | "Success", 258 | f"Plugin {plugin_name} disabled successfully" 259 | ) 260 | self.load_plugin_data() 261 | else: 262 | QtWidgets.QMessageBox.warning( 263 | self, 264 | "Error", 265 | f"Failed to disable plugin {plugin_name}" 266 | ) -------------------------------------------------------------------------------- /src/rps_game.py: -------------------------------------------------------------------------------- 1 | import random 2 | from PyQt5 import QtCore, QtGui, QtWidgets 3 | from src.squid import Personality 4 | 5 | class RPSGame: 6 | def __init__(self, tamagotchi_logic): 7 | self.tamagotchi_logic = tamagotchi_logic 8 | self.squid = tamagotchi_logic.squid 9 | self.choices = ['rock', 'paper', 'scissors'] 10 | self.game_window = None 11 | self.player_choice = None 12 | self.squid_choice = None 13 | self.result = None 14 | 15 | def start_game(self): 16 | # Move squid to bottom left 17 | self.tamagotchi_logic.move_squid_to_bottom_left(self.create_game_window) 18 | 19 | def create_game_window(self): 20 | # Pause the simulation 21 | self.tamagotchi_logic.set_simulation_speed(0) 22 | 23 | # Change squid image 24 | self.squid.change_to_rps_image() 25 | 26 | self.game_window = QtWidgets.QWidget() 27 | self.game_window.setWindowTitle("Rock, Paper, Scissors") 28 | layout = QtWidgets.QVBoxLayout() 29 | 30 | for choice in self.choices: 31 | button = QtWidgets.QPushButton(choice.capitalize()) 32 | button.clicked.connect(lambda _, c=choice: self.play_round(c)) 33 | layout.addWidget(button) 34 | 35 | self.result_label = QtWidgets.QLabel("Make your choice!") 36 | layout.addWidget(self.result_label) 37 | 38 | self.squid_choice_label = QtWidgets.QLabel("Squid's choice: ") 39 | layout.addWidget(self.squid_choice_label) 40 | 41 | self.game_window.setLayout(layout) 42 | self.game_window.show() 43 | 44 | # Connect the close event 45 | self.game_window.closeEvent = self.handle_close_event 46 | 47 | def handle_close_event(self, event): 48 | self.end_game() 49 | event.accept() 50 | 51 | def play_round(self, player_choice): 52 | self.player_choice = player_choice 53 | self.squid_choice = self.get_squid_choice() 54 | self.squid_choice_label.setText(f"Squid's choice: {self.squid_choice.capitalize()}") 55 | 56 | if self.player_choice == self.squid_choice: 57 | self.result = "It's a tie!" 58 | elif ( 59 | (self.player_choice == 'rock' and self.squid_choice == 'scissors') or 60 | (self.player_choice == 'paper' and self.squid_choice == 'rock') or 61 | (self.player_choice == 'scissors' and self.squid_choice == 'paper') 62 | ): 63 | self.result = "You win!" 64 | self.handle_player_win() 65 | else: 66 | self.result = "Squid wins!" 67 | self.handle_squid_win() 68 | 69 | self.result_label.setText(self.result) 70 | self.update_squid_stats() 71 | 72 | # Check if the "Play Again" button already exists in the layout 73 | play_again_button = self.find_button("Play Again") 74 | if play_again_button is None: 75 | # Add a button to play again 76 | play_again_button = QtWidgets.QPushButton("Play Again") 77 | play_again_button.clicked.connect(self.reset_game) 78 | self.game_window.layout().addWidget(play_again_button) 79 | 80 | # Check if the "End Game" button already exists in the layout 81 | end_game_button = self.find_button("End Game") 82 | if end_game_button is None: 83 | # Add a button to end the game 84 | end_game_button = QtWidgets.QPushButton("End Game") 85 | end_game_button.clicked.connect(self.end_game) 86 | self.game_window.layout().addWidget(end_game_button) 87 | 88 | def find_button(self, text): 89 | # Helper function to find a button with the given text 90 | layout = self.game_window.layout() 91 | for i in range(layout.count()): 92 | widget = layout.itemAt(i).widget() 93 | if isinstance(widget, QtWidgets.QPushButton) and widget.text() == text: 94 | return widget 95 | return None 96 | 97 | def get_squid_choice(self): 98 | # Biased choice based on squid's curiosity 99 | if self.squid.curiosity > 70: 100 | # More likely to choose a random option 101 | return random.choice(self.choices) 102 | else: 103 | # More likely to choose a "safe" option (rock) 104 | return random.choices(self.choices, weights=[0.5, 0.25, 0.25])[0] 105 | 106 | def handle_player_win(self): 107 | self.squid.happiness = min(100, self.squid.happiness + 5) 108 | self.squid.curiosity = min(100, self.squid.curiosity + 10) 109 | self.tamagotchi_logic.points += 5 110 | 111 | def handle_squid_win(self): 112 | self.squid.happiness = min(100, self.squid.happiness + 10) 113 | self.squid.satisfaction = min(100, self.squid.satisfaction + 5) 114 | self.tamagotchi_logic.points += 2 115 | 116 | def update_squid_stats(self): 117 | self.squid.sleepiness = min(100, self.squid.sleepiness + 5) 118 | self.squid.hunger = min(100, self.squid.hunger + 3) 119 | self.tamagotchi_logic.update_statistics() 120 | self.tamagotchi_logic.user_interface.update_points(self.tamagotchi_logic.points) 121 | 122 | def reset_game(self): 123 | # Remove play again and end game buttons 124 | layout = self.game_window.layout() 125 | for i in reversed(range(layout.count())): 126 | widget = layout.itemAt(i).widget() 127 | if isinstance(widget, QtWidgets.QPushButton) and widget.text() in ["Play Again", "End Game"]: 128 | layout.removeWidget(widget) 129 | widget.deleteLater() 130 | 131 | # Reset labels 132 | self.result_label.setText("Make your choice!") 133 | self.squid_choice_label.setText("Squid's choice: ") 134 | 135 | def end_game(self): 136 | if self.game_window: 137 | self.game_window.close() 138 | self.game_window = None 139 | self.squid.restore_normal_image() 140 | self.tamagotchi_logic.set_simulation_speed(1) # Resume normal speed 141 | -------------------------------------------------------------------------------- /src/save_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import zipfile 4 | from datetime import datetime 5 | import shutil 6 | 7 | class DateTimeEncoder(json.JSONEncoder): 8 | """Custom JSON encoder that handles datetime objects.""" 9 | def default(self, obj): 10 | if isinstance(obj, datetime): 11 | return obj.isoformat() 12 | return super().default(obj) 13 | 14 | class SaveManager: 15 | def __init__(self, save_directory="saves"): 16 | self.save_directory = save_directory 17 | if not os.path.exists(save_directory): 18 | os.makedirs(save_directory) 19 | self.autosave_path = os.path.join(save_directory, "autosave.zip") 20 | self.manual_save_path = os.path.join(save_directory, "save_data.zip") 21 | 22 | def save_exists(self): 23 | """Check if any save file exists.""" 24 | return os.path.exists(self.autosave_path) or os.path.exists(self.manual_save_path) 25 | 26 | def extract_memories(self, zip_path, memory_dir): 27 | with zipfile.ZipFile(zip_path, 'r') as zipf: 28 | # Remove existing memory files 29 | if os.path.exists(memory_dir): 30 | shutil.rmtree(memory_dir) 31 | os.makedirs(memory_dir) 32 | 33 | # Extract new memory files 34 | for filename in ['ShortTerm.json', 'LongTerm.json']: 35 | if filename in zipf.namelist(): 36 | zipf.extract(filename, memory_dir) 37 | 38 | def get_latest_save(self): 39 | """Get the path of the most recent save file.""" 40 | if os.path.exists(self.autosave_path): 41 | return self.autosave_path 42 | elif os.path.exists(self.manual_save_path): 43 | return self.manual_save_path 44 | return None 45 | 46 | def save_game(self, save_data, is_autosave=False): 47 | filepath = self.autosave_path if is_autosave else self.manual_save_path 48 | try: 49 | with zipfile.ZipFile(filepath, 'w') as zipf: 50 | for key, data in save_data.items(): 51 | zipf.writestr(f"{key}.json", json.dumps(data, indent=4, cls=DateTimeEncoder)) 52 | return filepath 53 | except Exception as e: 54 | print(f"Error saving game: {str(e)}") 55 | import traceback 56 | traceback.print_exc() 57 | return None 58 | 59 | def load_game(self): 60 | latest_save = self.get_latest_save() 61 | if latest_save: 62 | save_data = {} 63 | with zipfile.ZipFile(latest_save, 'r') as zipf: 64 | for filename in zipf.namelist(): 65 | with zipf.open(filename) as f: 66 | key = os.path.splitext(filename)[0] 67 | save_data[key] = json.loads(f.read().decode('utf-8')) 68 | 69 | # Extract memory files one directory level above 70 | extract_path = os.path.join(os.path.dirname(os.path.dirname(latest_save)), '_memory') 71 | self.extract_memories(latest_save, extract_path) 72 | 73 | return save_data 74 | return None 75 | 76 | def delete_save(self, is_autosave=False): 77 | """Delete a save file.""" 78 | filepath = self.autosave_path if is_autosave else self.manual_save_path 79 | if os.path.exists(filepath): 80 | os.remove(filepath) 81 | return True 82 | return False 83 | 84 | def get_save_timestamp(self, is_autosave=False): 85 | """Get the timestamp of a save file.""" 86 | filepath = self.autosave_path if is_autosave else self.manual_save_path 87 | if os.path.exists(filepath): 88 | return os.path.getmtime(filepath) 89 | return None 90 | 91 | def get_save_size(self, is_autosave=False): 92 | """Get the size of a save file in bytes.""" 93 | filepath = self.autosave_path if is_autosave else self.manual_save_path 94 | if os.path.exists(filepath): 95 | return os.path.getsize(filepath) 96 | return None -------------------------------------------------------------------------------- /src/splash_screen.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import os 3 | from .display_scaling import DisplayScaling 4 | 5 | class SplashScreen(QtWidgets.QWidget): 6 | finished = QtCore.pyqtSignal() 7 | second_frame = QtCore.pyqtSignal() # New signal for second frame 8 | 9 | def __init__(self, parent=None): 10 | super().__init__(parent) 11 | 12 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint) 13 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 14 | 15 | self.frame_index = 0 16 | self.frames = [] 17 | 18 | # Load frames 19 | for i in range(1, 7): 20 | image_path = os.path.join("images", "egg", f"anim0{i}.jpg") 21 | if os.path.exists(image_path): 22 | original_pixmap = QtGui.QPixmap(image_path) 23 | if not original_pixmap.isNull(): 24 | scaled_size = original_pixmap.size() * DisplayScaling.get_scale_factor() 25 | scaled_pixmap = original_pixmap.scaled( 26 | scaled_size, 27 | QtCore.Qt.KeepAspectRatio, 28 | QtCore.Qt.SmoothTransformation 29 | ) 30 | self.frames.append(scaled_pixmap) 31 | else: 32 | print(f"Failed to load image: {image_path}") 33 | else: 34 | print(f"Image file not found: {image_path}") 35 | 36 | if not self.frames: 37 | print("No frames were loaded successfully.") 38 | self.label = QtWidgets.QLabel("No images loaded", self) 39 | self.setFixedSize(256, 256) 40 | else: 41 | self.label = QtWidgets.QLabel(self) 42 | self.label.setPixmap(self.frames[0]) 43 | self.setFixedSize(self.frames[0].size()) 44 | 45 | # Create timer but don't start it yet 46 | self.timer = QtCore.QTimer(self) 47 | self.timer.timeout.connect(self.next_frame) 48 | 49 | def start_animation(self): 50 | """Start the animation sequence after window is ready""" 51 | self.timer.start(1500) # 1.5 seconds between frames 52 | 53 | def next_frame(self): 54 | self.frame_index += 1 55 | if self.frame_index < len(self.frames): 56 | self.label.setPixmap(self.frames[self.frame_index]) 57 | if self.frame_index == 1: # Second frame (index 1) 58 | self.second_frame.emit() # Emit signal for second frame 59 | elif self.frame_index == len(self.frames): 60 | # Last frame shown, schedule hiding 61 | QtCore.QTimer.singleShot(1500, self.end_animation) 62 | else: 63 | self.timer.stop() 64 | 65 | def end_animation(self): 66 | print(" ") 67 | print(" ******************************") 68 | print(" *** A SQUID HAS HATCHED! ***") 69 | print(" YOU NEED TO LOOK AFTER HIM.. ") 70 | print(" ******************************") 71 | self.hide() 72 | self.finished.emit() 73 | 74 | def showEvent(self, event): 75 | self.move(self.parent().rect().center() - self.rect().center()) 76 | super().showEvent(event) -------------------------------------------------------------------------------- /src/statistics_window.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | 3 | class StatBox(QtWidgets.QWidget): 4 | def __init__(self, label, parent=None): 5 | super().__init__(parent) 6 | 7 | # Get screen resolution 8 | screen = QtWidgets.QApplication.primaryScreen() 9 | screen_size = screen.size() 10 | 11 | layout = QtWidgets.QVBoxLayout(self) 12 | layout.setContentsMargins(5, 5, 5, 5) 13 | 14 | # Resolution-specific styling 15 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 16 | # 1080p - significantly smaller text and box 17 | value_font_size = 18 # Reduced from 22 18 | label_font_size = 11 # Reduced from 14 19 | box_width = 85 # Reduced from 100 20 | box_height = 70 # Reduced from 80 21 | else: 22 | # Higher resolutions - original sizes 23 | value_font_size = 28 24 | label_font_size = 16 25 | box_width = 120 26 | box_height = 100 27 | 28 | self.value_label = QtWidgets.QLabel("0") 29 | self.value_label.setAlignment(QtCore.Qt.AlignCenter) 30 | self.value_label.setStyleSheet(f"font-size: {value_font_size}px; font-weight: bold; border: 2px solid black; background-color: white;") 31 | layout.addWidget(self.value_label) 32 | 33 | self.value_edit = QtWidgets.QLineEdit() 34 | self.value_edit.setAlignment(QtCore.Qt.AlignCenter) 35 | self.value_edit.setStyleSheet(f"font-size: {value_font_size}px; font-weight: bold; border: 2px solid black; background-color: white;") 36 | self.value_edit.hide() # Initially hidden 37 | layout.addWidget(self.value_edit) 38 | 39 | name_label = QtWidgets.QLabel(label) 40 | name_label.setAlignment(QtCore.Qt.AlignCenter) 41 | name_label.setStyleSheet(f"font-size: {label_font_size}px; font-weight: bold;") 42 | layout.addWidget(name_label) 43 | 44 | self.setFixedSize(box_width, box_height) 45 | 46 | def set_value(self, value): 47 | self.value_label.setText(str(int(value))) 48 | self.value_edit.setText(str(int(value))) 49 | 50 | def get_value(self): 51 | return int(self.value_edit.text()) 52 | 53 | def set_editable(self, editable): 54 | self.value_label.setVisible(not editable) 55 | self.value_edit.setVisible(editable) 56 | 57 | class StatisticsWindow(QtWidgets.QWidget): 58 | def __init__(self, squid): 59 | super().__init__() 60 | self.squid = squid 61 | 62 | # Get screen resolution 63 | screen = QtWidgets.QApplication.primaryScreen() 64 | screen_size = screen.size() 65 | 66 | self.setWindowTitle("Statistics") 67 | self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint) 68 | 69 | # Set position to top-left corner 70 | self.move(0, 0) 71 | 72 | # Resolution-specific window sizing 73 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 74 | # Much smaller window for 1080p 75 | window_width = 320 # Reduced from 380 76 | window_height = 420 # Reduced from 500 77 | else: 78 | # Original sizes for higher resolutions 79 | window_width = 450 80 | window_height = 600 81 | 82 | # Set window size 83 | self.setFixedSize(window_width, window_height) 84 | 85 | # Layout setup 86 | main_layout = QtWidgets.QVBoxLayout(self) 87 | 88 | # Adjust layout spacing for 1080p 89 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 90 | main_layout.setSpacing(6) # Reduced from 10 91 | else: 92 | main_layout.setSpacing(10) 93 | 94 | grid_layout = QtWidgets.QGridLayout() 95 | 96 | # Adjust grid spacing for 1080p 97 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 98 | grid_layout.setSpacing(6) # Reduced from 10 99 | else: 100 | grid_layout.setSpacing(10) 101 | 102 | main_layout.addLayout(grid_layout) 103 | 104 | self.stat_boxes = { 105 | "hunger": StatBox("Hunger"), 106 | "happiness": StatBox("Happiness"), 107 | "cleanliness": StatBox("Cleanliness"), 108 | "sleepiness": StatBox("Sleepiness"), 109 | "health": StatBox("Health"), 110 | "satisfaction": StatBox("Satisfaction"), 111 | "curiosity": StatBox("Curiosity"), 112 | "anxiety": StatBox("Anxiety") 113 | } 114 | 115 | # Add stat boxes to the grid 116 | grid_layout.addWidget(self.stat_boxes["health"], 0, 0) 117 | grid_layout.addWidget(self.stat_boxes["hunger"], 0, 1) 118 | grid_layout.addWidget(self.stat_boxes["happiness"], 0, 2) 119 | grid_layout.addWidget(self.stat_boxes["cleanliness"], 1, 0) 120 | grid_layout.addWidget(self.stat_boxes["sleepiness"], 1, 1) 121 | 122 | # Add a separator 123 | separator = QtWidgets.QFrame() 124 | separator.setFrameShape(QtWidgets.QFrame.HLine) 125 | separator.setFrameShadow(QtWidgets.QFrame.Sunken) 126 | main_layout.addWidget(separator) 127 | 128 | new_neurons_layout = QtWidgets.QHBoxLayout() 129 | 130 | # Adjust neuron layout spacing for 1080p 131 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 132 | new_neurons_layout.setSpacing(10) # Reduced from 15 133 | else: 134 | new_neurons_layout.setSpacing(15) 135 | 136 | main_layout.addLayout(new_neurons_layout) 137 | new_neurons_layout.addWidget(self.stat_boxes["satisfaction"]) 138 | new_neurons_layout.addWidget(self.stat_boxes["curiosity"]) 139 | new_neurons_layout.addWidget(self.stat_boxes["anxiety"]) 140 | 141 | # Status label 142 | self.status_label = QtWidgets.QLabel() 143 | self.status_label.setAlignment(QtCore.Qt.AlignCenter) 144 | 145 | # Resolution-specific styling for status label 146 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 147 | self.status_label.setStyleSheet("font-size: 18px;") # Reduced from 24px 148 | else: 149 | self.status_label.setStyleSheet("font-size: 24px;") 150 | 151 | main_layout.addWidget(self.status_label) 152 | 153 | # Resolution-specific styling for score label 154 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 155 | score_font_size = 20 # Reduced from 24 156 | else: 157 | score_font_size = 32 # Original size 158 | 159 | self.score_label = QtWidgets.QLabel("Score: 0") 160 | self.score_label.setAlignment(QtCore.Qt.AlignCenter) 161 | self.score_label.setStyleSheet(f"font-size: {score_font_size}px; font-weight: bold;") 162 | main_layout.addWidget(self.score_label) 163 | 164 | # Apply button (initially hidden) 165 | self.apply_button = QtWidgets.QPushButton("Apply Changes") 166 | self.apply_button.clicked.connect(self.apply_changes) 167 | self.apply_button.hide() 168 | 169 | # Size the apply button appropriately for 1080p 170 | if screen_size.width() <= 1920 and screen_size.height() <= 1080: 171 | self.apply_button.setStyleSheet("font-size: 14px;") # Reduced from 18px 172 | else: 173 | self.apply_button.setStyleSheet("font-size: 18px;") 174 | 175 | main_layout.addWidget(self.apply_button) 176 | 177 | def update_statistics(self): 178 | if self.squid is not None: 179 | for key, box in self.stat_boxes.items(): 180 | if hasattr(self.squid, key): 181 | box.set_value(getattr(self.squid, key)) 182 | self.status_label.setText(f"Status: {self.squid.status}") 183 | self.update_score() 184 | 185 | def set_debug_mode(self, enabled): 186 | for key, box in self.stat_boxes.items(): 187 | if key not in ["satisfaction", "curiosity", "anxiety"]: # Exclude these attributes from being editable 188 | box.set_editable(enabled) 189 | self.apply_button.setVisible(enabled) 190 | 191 | def apply_changes(self): 192 | if self.squid is not None: 193 | for key, box in self.stat_boxes.items(): 194 | if hasattr(self.squid, key): 195 | setattr(self.squid, key, box.get_value()) 196 | self.update_statistics() 197 | 198 | def update_score(self): 199 | if self.squid is not None: 200 | curiosity_factor = self.squid.curiosity / 100 201 | anxiety_factor = 1 - self.squid.anxiety / 100 202 | happiness_factor = self.squid.happiness / 100 203 | health_factor = self.squid.health / 100 204 | 205 | base_score = curiosity_factor * anxiety_factor * 100 206 | multiplier = (happiness_factor + health_factor) / 2 207 | 208 | if self.squid.is_sick or self.squid.hunger >= 80 or self.squid.happiness <= 20: 209 | multiplier = -multiplier 210 | 211 | if self.squid.health == 0: 212 | base_score -= 200 213 | 214 | final_score = int(base_score * multiplier) 215 | self.score_label.setText(f"Score: {final_score}") 216 | 217 | def closeEvent(self, event): 218 | event.accept() -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | dosidicus: 2.1.5 2 | brain_tool: 2.0nf 3 | decision_engine: 2.1 --------------------------------------------------------------------------------