Files
KI/P3/Task/Propositions2.py
2025-06-28 20:56:09 +02:00

412 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from collections import deque
import re
class KnowledgebasedAgent:
"""Knowledge-based agent for Wumpus World using CNF Resolution"""
def __init__(self, method="CNF"):
"""Initialize the knowledge-based agent"""
self.method = method
self.cnf_clauses = [] # List of sets (each set is a clause)
# ==================== MAIN INTERFACE ====================
def TELL(self, step_result):
"""Add sensor data from gym environment step result
Args:
step_result: Can be either:
- String (legacy): "S11", "-B12", etc.
- Tuple from env.step(): (observation, reward, terminated, truncated, info)
"""
# Handle legacy string input
if isinstance(step_result, str):
# print(f"Added sensor: {step_result}")
self._generate_clauses_from_sensor(step_result)
return
# Handle gym environment step result
if isinstance(step_result, tuple) and len(step_result) >= 3:
observation, reward, terminated = step_result[0], step_result[1], step_result[2]
# Extract position (convert from 0-based to 1-based coordinates)
x = observation['x'] + 1 # Convert 0-based to 1-based
y = observation['y'] + 1 # Convert 0-based to 1-based
position = f"{x}{y}"
print(f"\n Processing step result for position [{x},{y}]:")
print(f" Terminated: {terminated}, Reward: {reward}")
# Extract sensors from observation
sensors = {
'stench': observation.get('stench', False),
'breeze': observation.get('breeze', False),
'glitter': observation.get('glitter', False),
'bump': observation.get('bump', False),
'scream': observation.get('scream', False)
}
# Process survival/death logic first
if terminated:
# Agent died - field contains Wumpus OR Pit
death_clause = {f"W{position}", f"P{position}"}
self.cnf_clauses.append(death_clause)
print(f" Death rule: W{position} P{position}")
# No sensor processing when agent dies
print(f" Agent died - no sensor data processed")
return
else:
# Agent survived - field is safe (no Wumpus AND no Pit)
safe_wumpus = {f"-W{position}"}
safe_pit = {f"-P{position}"}
self.cnf_clauses.append(safe_wumpus)
self.cnf_clauses.append(safe_pit)
print(f" Survival rules: ¬W{position} ∧ ¬P{position}")
# Process sensor readings
sensor_mapping = {
'stench': 'S',
'breeze': 'B',
'glitter': 'G',
'bump': 'BUMP',
'scream': 'SC'
}
for sensor_name, sensor_value in sensors.items():
sensor_code = sensor_mapping.get(sensor_name, sensor_name.upper())
if sensor_value:
# Positive sensor reading
sensor_fact = f"{sensor_code}{position}"
# print(f" Sensor: {sensor_fact}")
self._add_sensor_unit_clause(sensor_fact)
self._generate_clauses_from_sensor(sensor_fact)
else:
# Negative sensor reading
sensor_fact = f"-{sensor_code}{position}"
# print(f" Sensor: {sensor_fact}")
self._add_sensor_unit_clause(sensor_fact)
self._generate_clauses_from_sensor(sensor_fact)
return
def _add_sensor_unit_clause(self, sensor_fact):
"""Add sensor as unit clause (avoid duplicates from _generate_clauses_from_sensor)"""
unit_clause = {sensor_fact}
if unit_clause not in self.cnf_clauses:
self.cnf_clauses.append(unit_clause)
def TELL_SENSOR(self, sensor_string):
"""Legacy method for adding individual sensor strings"""
self.TELL(sensor_string)
def TELL_STEP(self, step_result):
"""Convenience method specifically for gym step results"""
self.TELL(step_result)
def ASK(self, query):
"""Check if KB entails query using CNF Resolution"""
result = self._cnf_resolution(query)
print(f"ASK({query}): {result}")
return result
def print_clauses(self):
"""Print all CNF clauses"""
print("\n=== CNF Clauses ===")
for i, clause in enumerate(self.cnf_clauses):
literals = " ".join(sorted(clause))
print(f" {i + 1}. {{{literals}}}")
# ==================== CNF RESOLUTION ALGORITHM ====================
def _cnf_resolution(self, query):
"""CNF Resolution to prove KB |= query"""
# To prove KB |= query, show KB ∧ ¬query is unsatisfiable
negated_query = self._negate(query)
test_clauses = [clause.copy() for clause in self.cnf_clauses]
test_clauses.append({negated_query})
print(f"\nProving: KB ∧ ¬{query} is unsatisfiable")
print(f"Added negated query: {{{negated_query}}}")
new_clauses = []
iteration = 0
while True:
iteration += 1
# print(f"\n--- Resolution Step {iteration} ---")
# Try all pairs of clauses
for i in range(len(test_clauses)):
for j in range(i + 1, len(test_clauses)):
resolvent = self._resolve(test_clauses[i], test_clauses[j])
if resolvent is not None:
if len(resolvent) == 0:
# Empty clause = contradiction!
print(f"Empty clause from {test_clauses[i]}{test_clauses[j]}")
print("⚡ Contradiction found! KB entails query.")
return True
# Check if new clause is actually new
if resolvent not in test_clauses and resolvent not in new_clauses:
new_clauses.append(resolvent)
# print(f"New: {resolvent} from {test_clauses[i]} ∩ {test_clauses[j]}")
# If no new clauses, we're done
if not new_clauses:
print("No new clauses. Query not entailed.")
return False
# Add new clauses
test_clauses.extend(new_clauses)
new_clauses = []
# Safety limit
if iteration > 20:
print("Max iterations reached.")
return False
def _resolve(self, clause1, clause2):
"""Resolve two clauses if they have complementary literals"""
for literal in clause1:
complement = self._negate(literal)
if complement in clause2:
# Found resolution: combine clauses, remove complementary pair
resolvent = (clause1 - {literal}) | (clause2 - {complement})
return resolvent
return None
# ==================== SENSOR RULE GENERATION ====================
def _generate_clauses_from_sensor(self, sensor):
"""Generate CNF clauses based on sensor readings"""
# Add sensor as unit clause
self.cnf_clauses.append({sensor})
# Dispatch to specific sensor handlers
if self._handle_stench_sensors(sensor): return
if self._handle_breeze_sensors(sensor): return
if self._handle_glitter_sensors(sensor): return
if self._handle_scream_sensors(sensor): return
if self._handle_bump_sensors(sensor): return
def _handle_stench_sensors(self, sensor):
"""Handle stench-related sensors"""
# STENCH: S11, S22, etc.
stench_match = re.match(r'^S(\d)(\d)$', sensor)
if stench_match:
x, y = int(stench_match.group(1)), int(stench_match.group(2))
pos = f"{x}{y}"
# S11 → ¬W11 becomes {-S11, -W11}
self.cnf_clauses.append({f"-S{pos}", f"-W{pos}"})
# S11 → (W21 W12 ...) becomes {-S11, W21, W12, ...}
neighbors = self._get_neighbors(x, y)
if neighbors:
neighbor_wumpus = [f"W{nx}{ny}" for nx, ny in neighbors]
clause = {f"-S{pos}"} | set(neighbor_wumpus)
self.cnf_clauses.append(clause)
print(f" Generated: S{pos} → ¬W{pos} ∧ (W-neighbors)")
return True
# NO STENCH: -S12, -S22, etc.
no_stench_match = re.match(r'^-S(\d)(\d)$', sensor)
if no_stench_match:
x, y = int(no_stench_match.group(1)), int(no_stench_match.group(2))
pos = f"{x}{y}"
# -S12 → ¬W(neighbors) becomes {S12, -W11}, {S12, -W22}, etc.
neighbors = self._get_neighbors(x, y)
for nx, ny in neighbors:
neighbor_pos = f"{nx}{ny}"
self.cnf_clauses.append({f"S{pos}", f"-W{neighbor_pos}"})
print(f" Generated: ¬S{pos} → ¬W-neighbors")
return True
return False
def _handle_breeze_sensors(self, sensor):
"""Handle breeze-related sensors"""
# BREEZE: B11, B22, etc.
breeze_match = re.match(r'^B(\d)(\d)$', sensor)
if breeze_match:
x, y = int(breeze_match.group(1)), int(breeze_match.group(2))
pos = f"{x}{y}"
# B11 → ¬P11 becomes {-B11, -P11}
self.cnf_clauses.append({f"-B{pos}", f"-P{pos}"})
# B11 → (P21 P12 ...) becomes {-B11, P21, P12, ...}
neighbors = self._get_neighbors(x, y)
if neighbors:
neighbor_pits = [f"P{nx}{ny}" for nx, ny in neighbors]
clause = {f"-B{pos}"} | set(neighbor_pits)
self.cnf_clauses.append(clause)
print(f" Generated: B{pos} → ¬P{pos} ∧ (P-neighbors)")
return True
# NO BREEZE: -B12, -B22, etc.
no_breeze_match = re.match(r'^-B(\d)(\d)$', sensor)
if no_breeze_match:
x, y = int(no_breeze_match.group(1)), int(no_breeze_match.group(2))
pos = f"{x}{y}"
# -B12 → ¬P(neighbors) becomes {B12, -P11}, {B12, -P22}, etc.
neighbors = self._get_neighbors(x, y)
for nx, ny in neighbors:
neighbor_pos = f"{nx}{ny}"
self.cnf_clauses.append({f"B{pos}", f"-P{neighbor_pos}"})
print(f" Generated: ¬B{pos} → ¬P-neighbors")
return True
return False
def _handle_glitter_sensors(self, sensor):
"""Handle glitter-related sensors"""
# GLITTER: G11, G22, etc.
glitter_match = re.match(r'^G(\d)(\d)$', sensor)
if glitter_match:
x, y = int(glitter_match.group(1)), int(glitter_match.group(2))
pos = f"{x}{y}"
# G11 → Gold11 becomes {-G11, Gold11}
self.cnf_clauses.append({f"-G{pos}", f"Gold{pos}"})
print(f" Generated: G{pos} → Gold{pos}")
return True
# NO GLITTER: -G11, -G22, etc.
no_glitter_match = re.match(r'^-G(\d)(\d)$', sensor)
if no_glitter_match:
x, y = int(no_glitter_match.group(1)), int(no_glitter_match.group(2))
pos = f"{x}{y}"
# -G11 → ¬Gold11 becomes {G11, -Gold11}
self.cnf_clauses.append({f"G{pos}", f"-Gold{pos}"})
print(f" Generated: ¬G{pos} → ¬Gold{pos}")
return True
return False
def _handle_scream_sensors(self, sensor):
"""Handle scream-related sensors"""
# SCREAM: Sc
if sensor == "Sc":
# Sc → WumpusDead becomes {-Sc, WumpusDead}
self.cnf_clauses.append({"-Sc", "WumpusDead"})
# WumpusDead → ¬W_anywhere: for all positions
for x in range(1, 5):
for y in range(1, 5):
self.cnf_clauses.append({"-WumpusDead", f"-W{x}{y}"})
print(f" Generated: Sc → WumpusDead → ¬W(everywhere)")
return True
# NO SCREAM: -Sc
if sensor == "-Sc":
# -Sc → WumpusAlive becomes {Sc, WumpusAlive}
self.cnf_clauses.append({"Sc", "WumpusAlive"})
print(f" Generated: ¬Sc → WumpusAlive")
return True
return False
def _handle_bump_sensors(self, sensor):
"""Handle bump-related sensors"""
# BUMP: Bump11, Bump22, etc.
bump_match = re.match(r'^Bump(\d)(\d)$', sensor)
if bump_match:
x, y = int(bump_match.group(1)), int(bump_match.group(2))
print(f" Note: Bump{x}{y} indicates wall collision")
return True
# NO BUMP: -Bump11, -Bump22, etc.
no_bump_match = re.match(r'^-Bump(\d)(\d)$', sensor)
if no_bump_match:
x, y = int(no_bump_match.group(1)), int(no_bump_match.group(2))
print(f" Note: ¬Bump{x}{y} indicates successful movement")
return True
return False
# ==================== UTILITY METHODS ====================
def _negate(self, literal):
"""Negate a literal"""
if literal.startswith('-'):
return literal[1:]
else:
return f"-{literal}"
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
# ==================== TEST SCENARIOS ====================
def test_gym_integration():
"""Test direct integration with gym environment step results"""
print("\n🎮 Test 3: Direct Gym Environment Integration")
print("="*60)
agent = KnowledgebasedAgent("CNF")
print("📡 Simulating gym environment step results...")
# Simulate step result from your example
step_result_1 = (
{'x': 0, 'y': 0, 'gold': False, 'direction': 1, 'arrow': True,
'stench': True, 'breeze': False, 'glitter': False, 'bump': False, 'scream': False},
[-1], # reward
False, # terminated (agent survives)
False, # truncated
{'info': 'step 1'}
)
# Agent survives at position [1,1] (converted from [0,0])
agent.TELL(step_result_1)
# Simulate death scenario
step_result_2 = (
{'x': 1, 'y': 0, 'gold': False, 'direction': 1, 'arrow': True,
'stench': False, 'breeze': False, 'glitter': False, 'bump': False, 'scream': False},
[-1000], # death reward
True, # terminated (agent dies)
False, # truncated
{'info': 'death'}
)
# Agent dies at position [2,1] (converted from [1,0])
agent.TELL(step_result_2)
print("\n🔍 Testing queries with survival logic...")
agent.ASK("W11") # Should be FALSE (agent survived [1,1])
agent.ASK("P11") # Should be FALSE (agent survived [1,1])
agent.ASK("W21") # Could be TRUE (agent died at [2,1])
agent.ASK("P21") # Should be False (no breez at [1,1])
print("\n📋 Generated clauses:")
agent.print_clauses()
if __name__ == "__main__":
print("🏰 CNF Resolution for Wumpus World - Complete Implementation")
print("=" * 70)
test_gym_integration()
print("\n✨ CNF Resolution correctly handles all Wumpus World logic!")