284 lines
9.6 KiB
Python
284 lines
9.6 KiB
Python
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") |