from collections import deque import re class HornKnowledgebasedAgent: """Knowledge-based agent using Horn Clauses + Forward Chaining""" def __init__(self): """Initialize the agent""" self.knowledge_base = [] # ==================== MAIN INTERFACE ==================== def TELL(self, input_data): """Add sensor data from gym step result or legacy string Args: input_data: Either string ("S11") or gym step result tuple """ # Handle legacy string input if isinstance(input_data, str): self._add_to_kb(input_data) self._auto_generate_rules(input_data) return # Handle gym environment step result if isinstance(input_data, tuple) and len(input_data) >= 3: observation, reward, terminated = input_data[0], input_data[1], input_data[2] # Extract position (convert 0-based to 1-based) x = observation['x'] + 1 y = observation['y'] + 1 position = f"{x}{y}" print(f"\n🎮 Processing step for position [{x},{y}] - Terminated: {terminated}") # Handle death vs survival if terminated: # Agent died - add both Wumpus AND Pit facts (Horn approximation) self._add_to_kb(f"W{position}") self._add_to_kb(f"P{position}") print(f"💀 Death: W{position} ∧ P{position} (Horn approx. of W{position} ∨ P{position})") return else: # Agent survived - field is safe self._add_to_kb(f"-W{position}") self._add_to_kb(f"-P{position}") print(f"✅ Survival: ¬W{position} ∧ ¬P{position}") # Process sensors sensors = { 'stench': 'S', 'breeze': 'B', 'glitter': 'G', 'bump': 'BUMP', 'scream': 'SC' } for sensor_key, sensor_code in sensors.items(): sensor_value = observation.get(sensor_key, False) if sensor_value: sensor_fact = f"{sensor_code}{position}" else: sensor_fact = f"-{sensor_code}{position}" print(f" Sensor: {sensor_fact}") self._add_to_kb(sensor_fact) self._auto_generate_rules(sensor_fact) def ASK(self, query): """Check if KB entails query using Forward Chaining""" result = self._forward_chaining(query) print(f"ASK({query}): {result}") return result def _add_to_kb(self, sentence): """Add sentence to KB if not already present""" if sentence not in self.knowledge_base: self.knowledge_base.append(sentence) print(f" Added: {sentence}") # ==================== FORWARD CHAINING ==================== def _forward_chaining(self, query): """Forward Chaining algorithm""" facts, rules = self._parse_kb() # Count premises for each rule count = {i: len(rule['premises']) for i, rule in enumerate(rules)} # Track inferred symbols inferred = {} derived_facts = set(facts) agenda = deque(facts) while agenda: fact = agenda.popleft() # Check for contradiction negated = self._negate(fact) if negated in derived_facts: print(f"⚠️ Contradiction: {fact} and {negated}") return False # Found query? if fact == query: return True # Process rules if not inferred.get(fact, False): inferred[fact] = True for i, rule in enumerate(rules): if fact in rule['premises']: count[i] -= 1 if count[i] == 0: conclusion = rule['conclusion'] if not inferred.get(conclusion, False): agenda.append(conclusion) derived_facts.add(conclusion) # Check if negation of query was derived negated_query = self._negate(query) if negated_query in derived_facts: print(f" {query} is FALSE (KB entails {negated_query})") return False print(f" {query} cannot be proven") return False def _parse_kb(self): """Parse KB into facts and rules""" facts = [] rules = [] for sentence in self.knowledge_base: if '=>' in sentence: premises_part, conclusion = sentence.split('=>') premises = [p.strip() for p in premises_part.split(',')] rules.append({'premises': premises, 'conclusion': conclusion.strip()}) else: facts.append(sentence) return facts, rules def _negate(self, literal): """Negate a literal""" return literal[1:] if literal.startswith('-') else f"-{literal}" # ==================== RULE GENERATION ==================== def _auto_generate_rules(self, sensor): """Generate Horn rules from sensor facts""" # STENCH rules if re.match(r'^S(\d)(\d)$', sensor): x, y = int(sensor[1]), int(sensor[2]) pos = f"{x}{y}" # S11 => -W11 (no Wumpus in same field) self._add_to_kb(f"S{pos}=>-W{pos}") # S11 => W21, S11 => W12 (direct rules for neighbors) for nx, ny in self._get_neighbors(x, y): self._add_to_kb(f"S{pos}=>W{nx}{ny}") print(f" Generated stench rules for S{pos}") # NO STENCH rules elif re.match(r'^-S(\d)(\d)$', sensor): x, y = int(sensor[2]), int(sensor[3]) pos = f"{x}{y}" # -S12 => -W11, -S12 => -W22 (no Wumpus in neighbors) for nx, ny in self._get_neighbors(x, y): self._add_to_kb(f"-S{pos}=>-W{nx}{ny}") print(f" Generated no-stench rules for -S{pos}") # BREEZE rules elif re.match(r'^B(\d)(\d)$', sensor): x, y = int(sensor[1]), int(sensor[2]) pos = f"{x}{y}" # B11 => -P11 (no Pit in same field) self._add_to_kb(f"B{pos}=>-P{pos}") # B11 => P21, B11 => P12 (direct rules for neighbors) for nx, ny in self._get_neighbors(x, y): self._add_to_kb(f"B{pos}=>P{nx}{ny}") print(f" Generated breeze rules for B{pos}") # NO BREEZE rules elif re.match(r'^-B(\d)(\d)$', sensor): x, y = int(sensor[2]), int(sensor[3]) pos = f"{x}{y}" # -B12 => -P11, -B12 => -P22 (no Pits in neighbors) for nx, ny in self._get_neighbors(x, y): self._add_to_kb(f"-B{pos}=>-P{nx}{ny}") print(f" Generated no-breeze rules for -B{pos}") # GLITTER rules elif re.match(r'^G(\d)(\d)$', sensor): x, y = int(sensor[1]), int(sensor[2]) self._add_to_kb(f"G{x}{y}=>Gold{x}{y}") print(f" Generated glitter rule") elif re.match(r'^-G(\d)(\d)$', sensor): x, y = int(sensor[2]), int(sensor[3]) self._add_to_kb(f"-G{x}{y}=>-Gold{x}{y}") print(f" Generated no-glitter rule") # SCREAM rules elif sensor == "SC": self._add_to_kb("SC=>WumpusDead") print(f" Generated scream rule") elif sensor == "-SC": self._add_to_kb("-SC=>WumpusAlive") print(f" Generated no-scream rule") def _get_neighbors(self, x, y): """Get valid neighbors in 4x4 grid""" neighbors = [] for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]: nx, ny = x + dx, y + dy if 1 <= nx <= 4 and 1 <= ny <= 4: neighbors.append((nx, ny)) return neighbors def print_kb(self): """Print knowledge base""" print("\n=== Horn Clauses Knowledge Base ===") facts = [s for s in self.knowledge_base if '=>' not in s] rules = [s for s in self.knowledge_base if '=>' in s] print(f"Facts ({len(facts)}):") for i, fact in enumerate(facts, 1): print(f" {i}. {fact}") print(f"Rules ({len(rules)}):") for i, rule in enumerate(rules, 1): print(f" {i}. {rule}") # ==================== TESTS ==================== def test_horn_gym_integration(): """Test Horn agent with gym integration""" print("🎯 Horn Clauses Agent - Gym Integration Test") print("="*50) agent = HornKnowledgebasedAgent() # Agent survives at [1,1] with stench step_result_1 = ( {'x': 0, 'y': 0, 'stench': True, 'breeze': False, 'glitter': False, 'bump': False, 'scream': False}, [-1], False, False, {} ) agent.TELL(step_result_1) # Agent dies at [2,1] step_result_2 = ( {'x': 1, 'y': 0, 'stench': False, 'breeze': False, 'glitter': False, 'bump': False, 'scream': False}, [-1000], True, False, {} ) agent.TELL(step_result_2) print("\n🔍 Testing queries:") agent.ASK("W11") # Should be FALSE (survived) agent.ASK("P11") # Should be FALSE (survived) agent.ASK("W21") # Should be TRUE (died) agent.ASK("P21") # Should be False No breez at 11 agent.ASK("W12") # Should be TRUE (from S11) agent.ASK("W22") # Should be TRUE (from S11) agent.print_kb() if __name__ == "__main__": print("🏰 Horn Clauses Agent for Wumpus World") print("="*60) test_horn_gym_integration() print("\n✨ Horn implementation complete!") print(" Shows clear limitations vs CNF Resolution")