Repository: joselc/life-sim-first-try
Issue category: Feature Request

Issue:
Plants seeds can die before growing
A plant seed can die before evolving to a growing plant. This behavior will be decided randomly during the seed phase of the plant. A threshold will be established to decide if the seed evolves or not. The seed will be able to evolve at the end of its seed phase, where it will be decided randomly.
A seed that succesfully evolves will go to growing phase. A seed that dies, will evolve directly to ground (last plant status).
Plants seeds can die before growing
A plant seed can die before evolving to a growing plant. This behavior will be decided randomly during the seed phase of the plant. A threshold will be established to decide if the seed evolves or not. The seed will be able to evolve at the end of its seed phase, where it will be decided randomly.
A seed that succesfully evolves will go to growing phase. A seed that dies, will evolve directly to ground (last plant status).


Hints:

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..56b8df8
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,25 @@
+# Description
+
+Please include a summary of the changes and the related issue.
+
+Fixes # (issue)
+
+## Type of change
+
+Please delete options that are not relevant.
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] This change requires a documentation update
+
+## Checklist:
+
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] New and existing unit tests pass locally with my changes
+- [ ] Any dependent changes have been merged and published in downstream modules 
\ No newline at end of file
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..1afba64
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,39 @@
+codecov:
+  require_ci_to_pass: true
+  notify:
+    wait_for_ci: true
+
+coverage:
+  precision: 2
+  round: down
+  range: "70...100"
+  
+  status:
+    project:
+      default:
+        target: 90%
+        threshold: 1%
+    patch:
+      default:
+        target: 90%
+        threshold: 1%
+    changes: false
+
+parsers:
+  gcov:
+    branch_detection:
+      conditional: yes
+      loop: yes
+      method: no
+      macro: no
+
+comment:
+  layout: "reach,diff,flags,tree"
+  behavior: default
+  require_changes: false
+  require_base: false
+  require_head: true
+  show_carryforward_flags: true
+
+github_checks:
+  annotations: true 
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
index 129b79d..9951070 100644
--- a/src/config.py
+++ b/src/config.py
@@ -12,6 +12,7 @@
     GRID_COLUMNS (int): Number of columns in the hexagonal grid
     GRID_ROWS (int): Number of rows in the hexagonal grid
     PLANT_SPAWN_PROBABILITY (float): Probability (0-1) of spawning a plant cell
+    SEED_SURVIVAL_THRESHOLD (float): Default probability (0-1) of a seed surviving
     
     COLORS (Dict[str, Tuple[int, int, int]]): Dictionary of RGB color definitions
         for various elements in the simulation
@@ -27,6 +28,7 @@
 GRID_COLUMNS = 16
 GRID_ROWS = 12
 PLANT_SPAWN_PROBABILITY = 0.5
+SEED_SURVIVAL_THRESHOLD = 0.7  # 70% chance of survival by default
 
 # Colors
 COLORS = {
diff --git a/src/hexagons/plant_states.py b/src/hexagons/plant_states.py
index 2862f39..d7a918b 100644
--- a/src/hexagons/plant_states.py
+++ b/src/hexagons/plant_states.py
@@ -1,6 +1,8 @@
 """Plant states and state management for the life simulation."""
 
 from enum import Enum, auto
+import random
+from ..config import SEED_SURVIVAL_THRESHOLD
 
 
 class PlantState(Enum):
@@ -31,6 +33,7 @@ class PlantStateManager:
         time_in_state (float): How long the plant has been in current state
         health (float): Plant's health from 0.0 to 1.0
         growth (float): Plant's growth progress from 0.0 to 1.0
+        seed_survival_threshold (float): Probability (0-1) of a seed surviving to growing phase
     """
     
     # State transition thresholds
@@ -45,6 +48,38 @@ def __init__(self):
         self.time_in_state = 0.0
         self.health = 1.0
         self.growth = 0.0
+        self.seed_survival_threshold = SEED_SURVIVAL_THRESHOLD
+    
+    @property
+    def seed_survival_threshold(self) -> float:
+        """Get the probability threshold for seed survival.
+        
+        Returns:
+            float: The probability (0-1) that a seed will survive to growing phase
+        """
+        return self._seed_survival_threshold
+
+    @seed_survival_threshold.setter
+    def seed_survival_threshold(self, value: float) -> None:
+        """Set the seed survival threshold.
+        
+        Args:
+            value (float): The new threshold value (0-1)
+            
+        Raises:
+            ValueError: If value is not between 0 and 1
+        """
+        if not 0 <= value <= 1:
+            raise ValueError("Seed survival threshold must be between 0 and 1")
+        self._seed_survival_threshold = value
+
+    def _check_seed_survival(self) -> bool:
+        """Check if the seed survives to growing phase.
+        
+        Returns:
+            bool: True if the seed survives, False if it dies
+        """
+        return random.random() < self.seed_survival_threshold
     
     def update(self, dt: float) -> None:
         """Update the plant's state based on time passed.
@@ -55,9 +90,13 @@ def update(self, dt: float) -> None:
         self.time_in_state += dt
         
         if self.state == PlantState.SEED:
-            # Stay in seed state for SEED_DURATION
+            # Check for state transition at the end of seed phase
             if self.time_in_state >= self.SEED_DURATION:
-                self.state = PlantState.GROWING
+                # Determine if seed survives
+                if self._check_seed_survival():
+                    self.state = PlantState.GROWING
+                else:
+                    self.state = PlantState.DYING
                 self.time_in_state = 0.0
             
         elif self.state == PlantState.GROWING:
diff --git a/src/mesh/hex_mesh.py b/src/mesh/hex_mesh.py
index 5f98cb7..9871aa2 100644
--- a/src/mesh/hex_mesh.py
+++ b/src/mesh/hex_mesh.py
@@ -2,10 +2,11 @@
 
 import math
 import random
-from typing import List, Union
+from typing import List, Union, Tuple
 from ..hexagons.plant import PlantHexagon
 from ..hexagons.ground import GroundHexagon
 from ..config import PLANT_SPAWN_PROBABILITY
+from ..hexagons.plant_states import PlantState
 
 
 class HexMesh:
@@ -17,6 +18,10 @@ class HexMesh:
 
     Attributes:
         hexagons (List[Union[PlantHexagon, GroundHexagon]]): List of all hexagonal cells in the grid
+        num_columns (int): Number of columns in the grid
+        num_rows (int): Number of rows in the grid
+        grid_bounds (Tuple[float, float, float, float]): Grid boundaries (left, right, top, bottom)
+        cell_size (float): Size of hexagon cells (side length)
     """
 
     def __init__(self, num_columns: int, num_rows: int, display_width: int, display_height: int) -> None:
@@ -31,36 +36,95 @@ def __init__(self, num_columns: int, num_rows: int, display_width: int, display_
             display_width (int): Width of the display surface in pixels
             display_height (int): Height of the display surface in pixels
         """
+        self.num_columns = num_columns
+        self.num_rows = num_rows
+        
         # Calculate side length 'a' so that vertical span fits display_height exactly
-        a = display_height / (num_rows * math.sqrt(3))  # side length
+        self.cell_size = display_height / (num_rows * math.sqrt(3))  # side length
         # Total grid width = 2*a + (num_columns - 1)*1.5*a
-        grid_width = 2 * a + (num_columns - 1) * (1.5 * a)
-        offset_x = (display_width - grid_width) / 2
-        offset_y = 0  # even columns will touch top
+        grid_width = 2 * self.cell_size + (num_columns - 1) * (1.5 * self.cell_size)
+        self.offset_x = (display_width - grid_width) / 2
+        self.offset_y = 0  # even columns will touch top
+        
+        # Calculate grid boundaries
+        self.grid_bounds = (
+            self.offset_x,  # left
+            self.offset_x + grid_width,  # right
+            self.offset_y,  # top
+            display_height  # bottom
+        )
         
         self.hexagons: List[Union[PlantHexagon, GroundHexagon]] = []
         for i in range(num_columns):
             for j in range(num_rows):
                 # Compute center of hexagon for column i, row j
-                cx = offset_x + a + i * (1.5 * a)
+                cx = self.offset_x + self.cell_size + i * (1.5 * self.cell_size)
                 if i % 2 == 0:
-                    cy = offset_y + (a * math.sqrt(3) / 2) + j * (a * math.sqrt(3))
+                    cy = self.offset_y + (self.cell_size * math.sqrt(3) / 2) + j * (self.cell_size * math.sqrt(3))
                 else:
-                    cy = offset_y + a * math.sqrt(3) + j * (a * math.sqrt(3))
+                    cy = self.offset_y + self.cell_size * math.sqrt(3) + j * (self.cell_size * math.sqrt(3))
                 
                 # Randomly choose between a plant hexagon and ground hexagon
                 if random.random() < PLANT_SPAWN_PROBABILITY:
-                    hexagon = PlantHexagon(cx, cy, a)
+                    hexagon = PlantHexagon(cx, cy, self.cell_size)
                 else:
-                    hexagon = GroundHexagon(cx, cy, a)
+                    hexagon = GroundHexagon(cx, cy, self.cell_size)
                 
                 self.hexagons.append(hexagon)
 
+    def _is_position_valid(self, cx: float, cy: float) -> bool:
+        """Check if a position is within the grid bounds.
+        
+        Args:
+            cx (float): X-coordinate of the position
+            cy (float): Y-coordinate of the position
+            
+        Returns:
+            bool: True if the position is within bounds, False otherwise
+        """
+        left, right, top, bottom = self.grid_bounds
+        # Add a small margin (half a cell) to account for hexagon extent
+        margin = self.cell_size / 2
+        return (left - margin <= cx <= right + margin and 
+                top - margin <= cy <= bottom + margin)
+
+    def _convert_plants_to_ground(self, dead_plants: List[Tuple[int, PlantHexagon]]) -> None:
+        """Convert multiple dead plants to ground hexagons in bulk.
+        
+        Args:
+            dead_plants (List[Tuple[int, PlantHexagon]]): List of (index, plant) pairs to convert
+            
+        Raises:
+            ValueError: If any plant's position is outside the grid bounds
+        """
+        # First validate all positions
+        for index, plant in dead_plants:
+            if not self._is_position_valid(plant.cx, plant.cy):
+                raise ValueError(
+                    f"Plant position ({plant.cx}, {plant.cy}) is outside grid bounds {self.grid_bounds}"
+                )
+        
+        # Then perform all conversions
+        for index, plant in dead_plants:
+            self.hexagons[index] = GroundHexagon(plant.cx, plant.cy, plant.a)
+
     def update(self, t: float) -> None:
         """Update all cells in the grid.
 
+        Updates each cell and converts dead plants to ground in bulk.
+
         Args:
             t (float): Current simulation time in seconds
         """
+        # First update all hexagons
         for hexagon in self.hexagons:
-            hexagon.update(t) 
\ No newline at end of file
+            hexagon.update(t)
+        
+        # Then identify and convert dead plants in bulk
+        dead_plants = [
+            (i, hexagon) for i, hexagon in enumerate(self.hexagons)
+            if isinstance(hexagon, PlantHexagon) and hexagon.state_manager.state == PlantState.DEAD
+        ]
+        
+        if dead_plants:
+            self._convert_plants_to_ground(dead_plants) 
\ No newline at end of file


Test patch:
diff --git a/tests/hexagons/test_plant.py b/tests/hexagons/test_plant.py
index cd752d1..fbaa086 100644
--- a/tests/hexagons/test_plant.py
+++ b/tests/hexagons/test_plant.py
@@ -1,6 +1,7 @@
 """Unit tests for the PlantHexagon class."""
 
 import unittest
+from unittest.mock import Mock, patch
 from src.hexagons.plant import PlantHexagon
 from src.hexagons.plant_states import PlantState
 from tests.test_config import MOCK_CELL_SIZE, MOCK_COLORS
@@ -16,76 +17,77 @@ def test_initialization(self):
         self.assertEqual(self.plant.cx, 50)
         self.assertEqual(self.plant.cy, 50)
         self.assertEqual(self.plant.a, MOCK_CELL_SIZE)
-        self.assertEqual(self.plant.state_manager.state, PlantState.SEED)
-        self.assertEqual(self.plant.state_manager.time_in_state, 0.0)
-        self.assertEqual(self.plant.state_manager.health, 1.0)
-        self.assertEqual(self.plant.state_manager.growth, 0.0)
-
-    def test_lifecycle(self):
-        """Test that plant goes through its lifecycle states correctly."""
-        # Start as seed
-        self.assertEqual(self.plant.state_manager.state, PlantState.SEED)
-        
-        # Update should stay in seed state initially
+        # Only verify that a state manager exists, not its internal state
+        self.assertIsNotNone(self.plant.state_manager)
+
+    def test_update_delegates_to_state_manager(self):
+        """Test that update properly delegates to the state manager."""
+        # Create a mock state manager
+        mock_state_manager = Mock()
+        self.plant.state_manager = mock_state_manager
+
+        # Call update
         self.plant.update(0.1)
-        self.assertEqual(self.plant.state_manager.state, PlantState.SEED)
-        
-        # After SEED_DURATION, should transition to growing
-        self.plant.update(self.plant.state_manager.SEED_DURATION)
-        self.assertEqual(self.plant.state_manager.state, PlantState.GROWING)
-        
-        # Should be growing for a while
-        self.plant.update(self.plant.state_manager.GROWTH_THRESHOLD / 2)
-        self.assertEqual(self.plant.state_manager.state, PlantState.GROWING)
-        self.assertGreater(self.plant.state_manager.growth, 0.0)
-        
-        # After growth threshold, should be mature
-        self.plant.update(self.plant.state_manager.GROWTH_THRESHOLD)  # Complete growth
-        self.assertEqual(self.plant.state_manager.state, PlantState.MATURE)
-        
-        # Should stay mature for a while
-        self.plant.update(self.plant.state_manager.MATURE_MAX_TIME - 0.1)
-        self.assertEqual(self.plant.state_manager.state, PlantState.MATURE)
-        
-        # Eventually should start dying
-        self.plant.update(0.2)  # Push over max time
-        self.assertEqual(self.plant.state_manager.state, PlantState.DYING)
-        
-        # Finally should die
-        self.plant.update(self.plant.state_manager.DYING_DURATION)
-        self.assertEqual(self.plant.state_manager.state, PlantState.DEAD)
-
-    def test_color_transitions(self):
-        """Test that color changes appropriately with state transitions."""
-        # Start as seed (should be brown with yellow dot)
-        self.assertEqual(self.plant.state_manager.state, PlantState.SEED)
-        self.assertEqual(self.plant.base_color, MOCK_COLORS['BROWN'])
-        
-        # Growing should be brown with green dot
-        self.plant.update(self.plant.state_manager.SEED_DURATION + 0.1)
-        self.assertEqual(self.plant.state_manager.state, PlantState.GROWING)
-        self.assertEqual(self.plant.base_color, MOCK_COLORS['BROWN'])
-        
-        # Progress growth
-        self.plant.update(self.plant.state_manager.GROWTH_THRESHOLD / 2)
-        self.assertGreater(self.plant.state_manager.growth, 0.0)
-        self.assertLess(self.plant.state_manager.growth, 1.0)
-        
-        # Mature should be solid green
-        self.plant.update(self.plant.state_manager.GROWTH_THRESHOLD)  # Complete growth
-        self.assertEqual(self.plant.state_manager.state, PlantState.MATURE)
-        self.assertEqual(self.plant.base_color, MOCK_COLORS['MATURE'])
-        
-        # Start dying
-        self.plant.update(self.plant.state_manager.MATURE_MAX_TIME + 0.1)
-        self.assertEqual(self.plant.state_manager.state, PlantState.DYING)
-        self.assertEqual(self.plant.base_color, MOCK_COLORS['DYING'])
-
-    def assertColorCloserTo(self, color, target, other):
-        """Assert that color is closer to target than to other color."""
-        target_dist = sum((c1 - c2) ** 2 for c1, c2 in zip(color, target))
-        other_dist = sum((c1 - c2) ** 2 for c1, c2 in zip(color, other))
-        self.assertLess(target_dist, other_dist)
+
+        # Verify the state manager's update was called with correct time
+        mock_state_manager.update.assert_called_once_with(0.1)
+
+    def test_base_color_mapping(self):
+        """Test that base colors are correctly mapped to states."""
+        # Create a mock state manager
+        mock_state_manager = Mock()
+        self.plant.state_manager = mock_state_manager
+
+        # Test each state
+        state_color_map = {
+            PlantState.SEED: MOCK_COLORS['BROWN'],
+            PlantState.GROWING: MOCK_COLORS['BROWN'],
+            PlantState.MATURE: MOCK_COLORS['MATURE'],
+            PlantState.DYING: MOCK_COLORS['DYING'],
+            PlantState.DEAD: MOCK_COLORS['DEAD']
+        }
+
+        for state, expected_color in state_color_map.items():
+            mock_state_manager.state = state
+            self.assertEqual(self.plant.base_color, expected_color)
+
+    def test_detail_color_mapping(self):
+        """Test that detail colors are correctly mapped to states."""
+        # Create a mock state manager
+        mock_state_manager = Mock()
+        self.plant.state_manager = mock_state_manager
+
+        # Test each state
+        state_color_map = {
+            PlantState.SEED: MOCK_COLORS['YELLOW'],
+            PlantState.GROWING: MOCK_COLORS['GROWING'],
+            PlantState.MATURE: (0, 0, 0),  # Black (invisible)
+            PlantState.DYING: (0, 0, 0),   # Black (invisible)
+            PlantState.DEAD: (0, 0, 0)     # Black (invisible)
+        }
+
+        for state, expected_color in state_color_map.items():
+            mock_state_manager.state = state
+            self.assertEqual(self.plant.detail_color, expected_color)
+
+    def test_detail_radius_mapping(self):
+        """Test that detail radius is correctly mapped to states."""
+        # Create a mock state manager
+        mock_state_manager = Mock()
+        self.plant.state_manager = mock_state_manager
+
+        # Test each state
+        state_radius_map = {
+            PlantState.SEED: self.plant.SEED_DOT_RADIUS,
+            PlantState.GROWING: self.plant.GROWING_DOT_RADIUS,
+            PlantState.MATURE: 0.0,
+            PlantState.DYING: 0.0,
+            PlantState.DEAD: 0.0
+        }
+
+        for state, expected_radius in state_radius_map.items():
+            mock_state_manager.state = state
+            self.assertEqual(self.plant.detail_radius, expected_radius)
 
 
 if __name__ == '__main__':
diff --git a/tests/hexagons/test_plant_states.py b/tests/hexagons/test_plant_states.py
index 13e0266..e84c73d 100644
--- a/tests/hexagons/test_plant_states.py
+++ b/tests/hexagons/test_plant_states.py
@@ -1,7 +1,9 @@
 """Unit tests for the plant state management system."""
 
 import unittest
+from unittest.mock import patch
 from src.hexagons.plant_states import PlantState, PlantStateManager
+from src.config import SEED_SURVIVAL_THRESHOLD
 
 
 class TestPlantStateManager(unittest.TestCase):
@@ -15,89 +17,191 @@ def test_initialization(self):
         self.assertEqual(self.manager.time_in_state, 0.0)
         self.assertEqual(self.manager.health, 1.0)
         self.assertEqual(self.manager.growth, 0.0)
+        self.assertEqual(self.manager.seed_survival_threshold, SEED_SURVIVAL_THRESHOLD)
+
+    def test_seed_survival_check(self):
+        """Test that seed survival check works correctly."""
+        # Test with random value below threshold (survives)
+        with patch('random.random', return_value=0.5):
+            self.manager.seed_survival_threshold = 0.7
+            self.assertTrue(self.manager._check_seed_survival())
+
+        # Test with random value above threshold (dies)
+        with patch('random.random', return_value=0.8):
+            self.manager.seed_survival_threshold = 0.7
+            self.assertFalse(self.manager._check_seed_survival())
+
+        # Test edge cases
+        with patch('random.random', return_value=0.0):
+            self.manager.seed_survival_threshold = 0.1
+            self.assertTrue(self.manager._check_seed_survival())  # Just survives
+
+        with patch('random.random', return_value=1.0):
+            self.manager.seed_survival_threshold = 0.9
+            self.assertFalse(self.manager._check_seed_survival())  # Just dies
+
+    def test_seed_survival_transition(self):
+        """Test state transitions based on seed survival."""
+        # Test survival case
+        with patch('random.random', return_value=0.5):
+            self.manager.seed_survival_threshold = 0.7  # Will survive
+            self.assertEqual(self.manager.state, PlantState.SEED)
+            self.manager.update(self.manager.SEED_DURATION + 0.1)  # Trigger check
+            self.assertEqual(self.manager.state, PlantState.GROWING)
+            self.assertEqual(self.manager.time_in_state, 0.0)
+
+        # Test death case
+        manager2 = PlantStateManager()  # Fresh instance for death test
+        with patch('random.random', return_value=0.9):
+            manager2.seed_survival_threshold = 0.7  # Will die
+            self.assertEqual(manager2.state, PlantState.SEED)
+            manager2.update(manager2.SEED_DURATION + 0.1)  # Trigger check
+            self.assertEqual(manager2.state, PlantState.DYING)
+            self.assertEqual(manager2.time_in_state, 0.0)
+
+    def test_seed_death_progression(self):
+        """Test that a dying seed progresses through states correctly."""
+        with patch('random.random', return_value=0.9):  # Will die
+            self.manager.seed_survival_threshold = 0.7
+            
+            # Progress to dying state
+            self.manager.update(self.manager.SEED_DURATION + 0.1)
+            self.assertEqual(self.manager.state, PlantState.DYING)
+            self.assertEqual(self.manager.health, 1.0)  # Starts with full health
+            
+            # Half-way through dying
+            self.manager.update(self.manager.DYING_DURATION / 2)
+            self.assertEqual(self.manager.state, PlantState.DYING)
+            self.assertAlmostEqual(self.manager.health, 0.5, places=2)
+            
+            # Complete death
+            self.manager.update(self.manager.DYING_DURATION / 2)
+            self.assertEqual(self.manager.state, PlantState.DEAD)
+            self.assertEqual(self.manager.health, 0.0)
+
+    def test_seed_survival_threshold_validation(self):
+        """Test that seed survival threshold validates input correctly."""
+        # Test valid values
+        valid_values = [0.0, 0.5, 1.0]
+        for value in valid_values:
+            self.manager.seed_survival_threshold = value
+            self.assertEqual(self.manager.seed_survival_threshold, value)
+
+        # Test invalid values
+        invalid_values = [-0.1, 1.1, -1, 2]
+        for value in invalid_values:
+            with self.assertRaises(ValueError):
+                self.manager.seed_survival_threshold = value
+
+    def test_seed_survival_threshold_persistence(self):
+        """Test that seed survival threshold persists through state changes."""
+        # Change threshold
+        new_threshold = 0.8
+        self.manager.seed_survival_threshold = new_threshold
+        
+        # Progress through states
+        self.manager.update(self.manager.SEED_DURATION + 0.1)  # To GROWING
+        self.assertEqual(self.manager.seed_survival_threshold, new_threshold)
+        
+        self.manager.update(self.manager.GROWTH_THRESHOLD)  # To MATURE
+        self.assertEqual(self.manager.seed_survival_threshold, new_threshold)
+        
+        self.manager.update(self.manager.MATURE_MAX_TIME + 0.1)  # To DYING
+        self.assertEqual(self.manager.seed_survival_threshold, new_threshold)
+        
+        self.manager.update(self.manager.DYING_DURATION)  # To DEAD
+        self.assertEqual(self.manager.seed_survival_threshold, new_threshold)
 
     def test_seed_to_growing_transition(self):
         """Test transition from seed to growing state after SEED_DURATION."""
-        self.assertEqual(self.manager.state, PlantState.SEED)
-        # Update with less than SEED_DURATION
-        self.manager.update(0.1)
-        self.assertEqual(self.manager.state, PlantState.SEED)
-        # Update past SEED_DURATION
-        self.manager.update(self.manager.SEED_DURATION)
-        self.assertEqual(self.manager.state, PlantState.GROWING)
-        self.assertEqual(self.manager.time_in_state, 0.0)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold of 0.7
+            self.assertEqual(self.manager.state, PlantState.SEED)
+            # Update with less than SEED_DURATION
+            self.manager.update(0.1)
+            self.assertEqual(self.manager.state, PlantState.SEED)
+            # Update past SEED_DURATION
+            self.manager.update(self.manager.SEED_DURATION)
+            self.assertEqual(self.manager.state, PlantState.GROWING)
+            self.assertEqual(self.manager.time_in_state, 0.0)
 
     def test_growth_progression(self):
         """Test that growth progresses correctly over time."""
-        # Get to growing state
-        self.manager.update(self.manager.SEED_DURATION + 0.1)
-        self.assertEqual(self.manager.state, PlantState.GROWING)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold
+            # Get to growing state
+            self.manager.update(self.manager.SEED_DURATION + 0.1)
+            self.assertEqual(self.manager.state, PlantState.GROWING)
 
-        # Partial growth
-        self.manager.update(self.manager.GROWTH_THRESHOLD / 2)
-        self.assertEqual(self.manager.state, PlantState.GROWING)
-        self.assertAlmostEqual(self.manager.growth, 0.5, places=2)
+            # Partial growth
+            self.manager.update(self.manager.GROWTH_THRESHOLD / 2)
+            self.assertEqual(self.manager.state, PlantState.GROWING)
+            self.assertAlmostEqual(self.manager.growth, 0.5, places=2)
 
-        # Complete growth
-        self.manager.update(self.manager.GROWTH_THRESHOLD / 2)
-        self.assertEqual(self.manager.state, PlantState.MATURE)
-        self.assertEqual(self.manager.growth, 1.0)
+            # Complete growth
+            self.manager.update(self.manager.GROWTH_THRESHOLD / 2)
+            self.assertEqual(self.manager.state, PlantState.MATURE)
+            self.assertEqual(self.manager.growth, 1.0)
 
     def test_mature_to_dying_transition(self):
         """Test transition from mature to dying state."""
-        # Get to mature state
-        self.manager.update(self.manager.SEED_DURATION + 0.1)  # Seed -> Growing
-        self.manager.update(self.manager.GROWTH_THRESHOLD)  # Complete growth
-        self.assertEqual(self.manager.state, PlantState.MATURE)
-
-        # Not enough time passed for dying
-        self.manager.update(self.manager.MATURE_MAX_TIME - 0.1)
-        self.assertEqual(self.manager.state, PlantState.MATURE)
-
-        # Enough time passed, should start dying
-        self.manager.update(0.2)  # Push over the threshold
-        self.assertEqual(self.manager.state, PlantState.DYING)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold
+            # Get to mature state
+            self.manager.update(self.manager.SEED_DURATION + 0.1)  # Seed -> Growing
+            self.manager.update(self.manager.GROWTH_THRESHOLD)  # Complete growth
+            self.assertEqual(self.manager.state, PlantState.MATURE)
+            self.assertEqual(self.manager.time_in_state, 0.0)
+
+            # Update with less than MATURE_MAX_TIME
+            self.manager.update(self.manager.MATURE_MAX_TIME / 2)
+            self.assertEqual(self.manager.state, PlantState.MATURE)
+
+            # Update past MATURE_MAX_TIME
+            self.manager.update(self.manager.MATURE_MAX_TIME / 2 + 0.1)
+            self.assertEqual(self.manager.state, PlantState.DYING)
+            self.assertEqual(self.manager.time_in_state, 0.0)
 
     def test_dying_progression(self):
         """Test that health decreases correctly while dying."""
-        # Get to dying state
-        self.manager.update(self.manager.SEED_DURATION + 0.1)  # Seed -> Growing
-        self.manager.update(self.manager.GROWTH_THRESHOLD)  # Complete growth
-        self.manager.update(self.manager.MATURE_MAX_TIME + 0.1)  # Start dying
-        self.assertEqual(self.manager.state, PlantState.DYING)
-        self.assertEqual(self.manager.health, 1.0)
-
-        # Half-way through dying
-        self.manager.update(self.manager.DYING_DURATION / 2)
-        self.assertAlmostEqual(self.manager.health, 0.5, places=2)
-
-        # Complete death
-        self.manager.update(self.manager.DYING_DURATION / 2)
-        self.assertEqual(self.manager.state, PlantState.DEAD)
-        self.assertEqual(self.manager.health, 0.0)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold
+            # Get to dying state
+            self.manager.update(self.manager.SEED_DURATION + 0.1)  # Seed -> Growing
+            self.manager.update(self.manager.GROWTH_THRESHOLD)  # Complete growth
+            self.manager.update(self.manager.MATURE_MAX_TIME + 0.1)  # Start dying
+            self.assertEqual(self.manager.state, PlantState.DYING)
+            self.assertEqual(self.manager.health, 1.0)  # Full health at start of dying
+
+            # Update halfway through dying duration
+            self.manager.update(self.manager.DYING_DURATION / 2)
+            self.assertEqual(self.manager.state, PlantState.DYING)
+            self.assertAlmostEqual(self.manager.health, 0.5, places=2)
+
+            # Complete death
+            self.manager.update(self.manager.DYING_DURATION / 2)
+            self.assertEqual(self.manager.state, PlantState.DEAD)
+            self.assertEqual(self.manager.health, 0.0)
 
     def test_color_factor(self):
         """Test that color factor is appropriate for each state."""
-        # Seed state
-        self.assertEqual(self.manager.color_factor, 0.0)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold
+            # Seed state
+            self.assertEqual(self.manager.color_factor, 0.0)
 
-        # Growing state
-        self.manager.update(self.manager.SEED_DURATION + 0.1)  # Start growing
-        self.manager.update(self.manager.GROWTH_THRESHOLD / 2)  # Half growth
-        self.assertAlmostEqual(self.manager.color_factor, 0.5, places=2)
+            # Growing state
+            self.manager.update(self.manager.SEED_DURATION + 0.1)  # Start growing
+            self.manager.update(self.manager.GROWTH_THRESHOLD / 2)  # Half growth
+            self.assertAlmostEqual(self.manager.color_factor, 0.5, places=2)
 
-        # Mature state
-        self.manager.update(self.manager.GROWTH_THRESHOLD / 2)  # Complete growth
-        self.assertEqual(self.manager.color_factor, 1.0)
+            # Mature state
+            self.manager.update(self.manager.GROWTH_THRESHOLD / 2)  # Complete growth
+            self.assertEqual(self.manager.color_factor, 1.0)
 
-        # Dying state
-        self.manager.update(self.manager.MATURE_MAX_TIME + 0.1)  # Start dying
-        self.manager.update(self.manager.DYING_DURATION / 2)  # Half dead
-        self.assertAlmostEqual(self.manager.color_factor, 0.5, places=2)
+            # Dying state
+            self.manager.update(self.manager.MATURE_MAX_TIME + 0.1)  # Start dying
+            self.manager.update(self.manager.DYING_DURATION / 2)  # Half dead
+            self.assertAlmostEqual(self.manager.color_factor, 0.5, places=2)
 
-        # Dead state
-        self.manager.update(self.manager.DYING_DURATION / 2)  # Complete death
-        self.assertEqual(self.manager.color_factor, 0.0)
+            # Dead state
+            self.manager.update(self.manager.DYING_DURATION / 2)  # Complete death
+            self.assertEqual(self.manager.color_factor, 0.0)
 
     def test_time_tracking(self):
         """Test that time in state is tracked correctly."""
@@ -117,32 +221,33 @@ def test_time_tracking(self):
 
     def test_state_sequence(self):
         """Test that states always follow the correct sequence."""
-        states_seen = []
-        last_state = None
-        
-        # Run through entire lifecycle with smaller time steps
-        while self.manager.state != PlantState.DEAD:
-            current_state = self.manager.state
-            # Only record state when it changes
-            if current_state != last_state:
-                states_seen.append(current_state)
-                last_state = current_state
-            self.manager.update(0.5)  # Larger time step for quicker transitions
-
-        # Add final state
-        if self.manager.state not in states_seen:
-            states_seen.append(PlantState.DEAD)
-        
-        # Verify sequence
-        expected_sequence = [
-            PlantState.SEED,
-            PlantState.GROWING,
-            PlantState.MATURE,
-            PlantState.DYING,
-            PlantState.DEAD
-        ]
-        
-        self.assertEqual(states_seen, expected_sequence)
+        with patch('random.random', return_value=0.5):  # Will survive with default threshold
+            states_seen = []
+            last_state = None
+
+            # Run through entire lifecycle with smaller time steps
+            while self.manager.state != PlantState.DEAD:
+                current_state = self.manager.state
+                # Only record state when it changes
+                if current_state != last_state:
+                    states_seen.append(current_state)
+                    last_state = current_state
+                self.manager.update(0.5)  # Larger time step for quicker transitions
+
+            # Add final state
+            if self.manager.state not in states_seen:
+                states_seen.append(PlantState.DEAD)
+
+            # Verify sequence
+            expected_sequence = [
+                PlantState.SEED,
+                PlantState.GROWING,
+                PlantState.MATURE,
+                PlantState.DYING,
+                PlantState.DEAD
+            ]
+
+            self.assertEqual(states_seen, expected_sequence)
 
 
 if __name__ == '__main__':
diff --git a/tests/mesh/test_hex_mesh.py b/tests/mesh/test_hex_mesh.py
index 989aa5f..4f59a1f 100644
--- a/tests/mesh/test_hex_mesh.py
+++ b/tests/mesh/test_hex_mesh.py
@@ -13,6 +13,7 @@
     MOCK_COLUMNS,
     MOCK_ROWS
 )
+from unittest.mock import patch, Mock
 
 
 class TestHexMesh(unittest.TestCase):
@@ -39,11 +40,7 @@ def test_hexagon_types(self):
             self.assertTrue(isinstance(hexagon, (PlantHexagon, GroundHexagon)))
 
     def test_grid_dimensions(self):
-        """Test that the grid has reasonable dimensions.
-        
-        The grid should roughly fit within the screen bounds, allowing for some
-        overlap at the edges due to hexagon geometry and offset calculations.
-        """
+        """Test that the grid has reasonable dimensions."""
         # Find the leftmost, rightmost, topmost, and bottommost points
         left = min(h.cx - h.a for h in self.mesh.hexagons)
         right = max(h.cx + h.a for h in self.mesh.hexagons)
@@ -66,27 +63,23 @@ def test_grid_dimensions(self):
 
     def test_update_propagation(self):
         """Test that update calls are propagated to all hexagons."""
-        # First update - should stay in seed state
-        self.mesh.update(0.1)
-        
-        # Second update - transition to growing
-        self.mesh.update(2.0)  # SEED_DURATION
-        
-        # Third update - allow for growth
-        self.mesh.update(1.5)  # Half of GROWTH_THRESHOLD
-        
-        # Check that all plant hexagons have been updated
-        plant_count = 0
+        # Replace all hexagons with mocks
+        mock_hexagons = []
         for hexagon in self.mesh.hexagons:
-            if isinstance(hexagon, PlantHexagon):
-                plant_count += 1
-                # Plants should have moved past SEED state and started growing
-                self.assertEqual(hexagon.state_manager.state, PlantState.GROWING)
-                self.assertGreater(hexagon.state_manager.growth, 0.0)
-                self.assertLess(hexagon.state_manager.growth, 1.0)
-        
-        # Ensure we actually tested some plants
-        self.assertGreater(plant_count, 0)
+            mock = Mock()
+            mock.cx = hexagon.cx
+            mock.cy = hexagon.cy
+            mock.a = hexagon.a
+            mock_hexagons.append(mock)
+        self.mesh.hexagons = mock_hexagons
+
+        # Update the mesh
+        update_time = 0.1
+        self.mesh.update(update_time)
+
+        # Verify that update was called on each hexagon
+        for mock in mock_hexagons:
+            mock.update.assert_called_once_with(update_time)
 
     def test_rendering(self):
         """Test that all hexagons can be rendered."""
@@ -102,6 +95,219 @@ def test_rendering(self):
             self.assertTrue(show_grid)
             self.assertTrue(isinstance(drawn_hexagon, (PlantHexagon, GroundHexagon)))
 
+    def test_dead_plant_conversion(self):
+        """Test that dead plants are converted to ground."""
+        # Find a plant hexagon
+        plant_index = None
+        for i, hexagon in enumerate(self.mesh.hexagons):
+            if isinstance(hexagon, PlantHexagon):
+                plant_index = i
+                break
+        
+        if plant_index is None:
+            # Create a plant if none exists
+            plant = PlantHexagon(50, 50, 10)
+            self.mesh.hexagons[0] = plant
+            plant_index = 0
+        
+        plant = self.mesh.hexagons[plant_index]
+        original_position = (plant.cx, plant.cy, plant.a)
+        
+        # Set plant to dead state
+        plant.state_manager.state = PlantState.DEAD
+        
+        # Update should trigger conversion
+        self.mesh.update(0.1)
+        
+        # Verify conversion
+        converted = self.mesh.hexagons[plant_index]
+        self.assertIsInstance(converted, GroundHexagon)
+        self.assertEqual((converted.cx, converted.cy, converted.a), original_position)
+
+    def test_multiple_dead_plant_conversions(self):
+        """Test that multiple dead plants are converted to ground correctly."""
+        # Create a mesh with only plants
+        with patch('random.random', return_value=0.0):  # Ensure all cells are plants
+            test_mesh = HexMesh(2, 2, MOCK_SCREEN_WIDTH, MOCK_SCREEN_HEIGHT)
+        
+        # Set some plants to dead state
+        dead_indices = [0, 2]  # First and third plants
+        for i in dead_indices:
+            plant = test_mesh.hexagons[i]
+            plant.state_manager.state = PlantState.DEAD
+        
+        # Update should convert dead plants
+        test_mesh.update(0.1)
+        
+        # Verify conversions
+        for i in dead_indices:
+            self.assertIsInstance(test_mesh.hexagons[i], GroundHexagon)
+        
+        # Verify other plants remain unchanged
+        for i in range(len(test_mesh.hexagons)):
+            if i not in dead_indices:
+                self.assertIsInstance(test_mesh.hexagons[i], PlantHexagon)
+
+    def test_position_preservation_after_conversion(self):
+        """Test that converted ground hexagons maintain the same position as the original plant."""
+        # Create a plant and record its position
+        plant = PlantHexagon(50, 50, 10)
+        self.mesh.hexagons[0] = plant
+        original_points = plant.points.copy()
+        
+        # Set to dead state and update
+        plant.state_manager.state = PlantState.DEAD
+        self.mesh.update(0.1)
+        
+        # Verify position is maintained
+        ground = self.mesh.hexagons[0]
+        self.assertIsInstance(ground, GroundHexagon)
+        self.assertEqual(ground.points, original_points)
+
+    def test_grid_bounds_initialization(self):
+        """Test that grid boundaries are correctly initialized."""
+        # Check that bounds are properly set
+        left, right, top, bottom = self.mesh.grid_bounds
+        
+        # Verify basic bounds properties
+        self.assertLess(left, right)  # Left bound should be less than right
+        self.assertLess(top, bottom)  # Top bound should be less than bottom
+        self.assertEqual(bottom, MOCK_SCREEN_HEIGHT)  # Bottom should match screen height
+        self.assertEqual(top, 0)  # Top should be 0
+        
+        # Verify grid is centered horizontally
+        grid_width = right - left
+        expected_left = (MOCK_SCREEN_WIDTH - grid_width) / 2
+        self.assertAlmostEqual(left, expected_left)
+
+    def test_position_validation_within_bounds(self):
+        """Test that positions within grid bounds are validated correctly."""
+        # Get a known valid position (center of first hexagon)
+        valid_hexagon = self.mesh.hexagons[0]
+        
+        # Test center position
+        self.assertTrue(
+            self.mesh._is_position_valid(valid_hexagon.cx, valid_hexagon.cy),
+            "Center position should be valid"
+        )
+        
+        # Test positions near the edges but within bounds
+        left, right, top, bottom = self.mesh.grid_bounds
+        margin = self.mesh.cell_size / 2
+        
+        # Test various valid positions
+        valid_positions = [
+            (left + margin, top + margin),  # Top-left with margin
+            (right - margin, top + margin),  # Top-right with margin
+            (left + margin, bottom - margin),  # Bottom-left with margin
+            (right - margin, bottom - margin),  # Bottom-right with margin
+            ((left + right) / 2, (top + bottom) / 2),  # Center of grid
+        ]
+        
+        for x, y in valid_positions:
+            self.assertTrue(
+                self.mesh._is_position_valid(x, y),
+                f"Position ({x}, {y}) should be valid"
+            )
+
+    def test_position_validation_outside_bounds(self):
+        """Test that positions outside grid bounds are invalidated correctly."""
+        left, right, top, bottom = self.mesh.grid_bounds
+        margin = self.mesh.cell_size / 2
+        
+        # Test various invalid positions
+        invalid_positions = [
+            (left - margin * 2, top),  # Too far left
+            (right + margin * 2, top),  # Too far right
+            (left, top - margin * 2),  # Too far up
+            (right, bottom + margin * 2),  # Too far down
+            (float('-inf'), 0),  # Extreme left
+            (float('inf'), 0),  # Extreme right
+            (0, float('-inf')),  # Extreme top
+            (0, float('inf')),  # Extreme bottom
+        ]
+        
+        for x, y in invalid_positions:
+            self.assertFalse(
+                self.mesh._is_position_valid(x, y),
+                f"Position ({x}, {y}) should be invalid"
+            )
+
+    def test_convert_invalid_position(self):
+        """Test that converting a plant with invalid position raises ValueError."""
+        # Create a plant with invalid position
+        left, right, top, bottom = self.mesh.grid_bounds
+        invalid_plant = PlantHexagon(
+            left - self.mesh.cell_size * 2,  # Way outside left bound
+            top,
+            self.mesh.cell_size
+        )
+        invalid_plant.state_manager.state = PlantState.DEAD
+        
+        # Try to convert the invalid plant
+        with self.assertRaises(ValueError) as context:
+            self.mesh._convert_plants_to_ground([(0, invalid_plant)])
+        
+        # Verify error message contains position and bounds information
+        error_msg = str(context.exception)
+        self.assertIn("outside grid bounds", error_msg)
+        self.assertIn(str(self.mesh.grid_bounds), error_msg)
+
+    def test_convert_edge_case_positions(self):
+        """Test conversion of plants at edge positions within the margin."""
+        left, right, top, bottom = self.mesh.grid_bounds
+        margin = self.mesh.cell_size / 2
+        
+        # Create plants at edge positions but within margin
+        edge_positions = [
+            (left - margin + 0.1, (top + bottom) / 2),  # Just inside left margin
+            (right + margin - 0.1, (top + bottom) / 2),  # Just inside right margin
+            ((left + right) / 2, top - margin + 0.1),  # Just inside top margin
+            ((left + right) / 2, bottom + margin - 0.1),  # Just inside bottom margin
+        ]
+        
+        for i, (x, y) in enumerate(edge_positions):
+            # Create a plant at the edge position
+            plant = PlantHexagon(x, y, self.mesh.cell_size)
+            plant.state_manager.state = PlantState.DEAD
+            
+            # Verify conversion succeeds
+            try:
+                self.mesh._convert_plants_to_ground([(i, plant)])
+                success = True
+            except ValueError:
+                success = False
+            
+            self.assertTrue(
+                success,
+                f"Conversion should succeed for position ({x}, {y})"
+            )
+
+    def test_bulk_conversion_validation(self):
+        """Test that bulk conversion validates all positions before converting any."""
+        # Create a mix of valid and invalid plants
+        valid_plant = self.mesh.hexagons[0]
+        if not isinstance(valid_plant, PlantHexagon):
+            valid_plant = PlantHexagon(valid_plant.cx, valid_plant.cy, valid_plant.a)
+        
+        left, right, top, bottom = self.mesh.grid_bounds
+        invalid_plant = PlantHexagon(
+            right + self.mesh.cell_size * 2,  # Way outside right bound
+            top,
+            self.mesh.cell_size
+        )
+        
+        # Set both plants as dead
+        valid_plant.state_manager.state = PlantState.DEAD
+        invalid_plant.state_manager.state = PlantState.DEAD
+        
+        # Try to convert both plants
+        with self.assertRaises(ValueError):
+            self.mesh._convert_plants_to_ground([(0, valid_plant), (1, invalid_plant)])
+        
+        # Verify that no conversions took place (the valid plant should not have been converted)
+        self.assertIsInstance(self.mesh.hexagons[0], PlantHexagon)
+
 
 if __name__ == '__main__':
     unittest.main() 
\ No newline at end of file
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 209951d..1fb8153 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -3,6 +3,7 @@
 import unittest
 import pygame
 import time
+import random
 from unittest.mock import patch
 from src.mesh.hex_mesh import HexMesh
 from src.game_state import GameStateManager, GameState
@@ -19,6 +20,8 @@
 class TestGameIntegration(unittest.TestCase):
     def setUp(self):
         """Set up test fixtures before each test method."""
+        # Set fixed seed for consistent plant generation
+        random.seed(12345)
         pygame.init()
         self.renderer = PygameRenderer()
         self.renderer.setup(MOCK_SCREEN_WIDTH, MOCK_SCREEN_HEIGHT)
@@ -203,6 +206,8 @@ def test_state_transitions_integration(self):
     def tearDown(self):
         """Clean up after each test method."""
         self.renderer.cleanup()
+        # Reset random seed
+        random.seed()
 
 
 if __name__ == '__main__':