October 02, 2026

Creating chatty robots

 

The Micromouse challenge is a well known robot competition which is mostly a hardware building plus programming challenge. The rules of the challenge can be modified so that the ability of a robot is determined to generate grounded language. In the screenshot, the robot has a text widget at the bottom for showing the text.

The human user controls the robot and at the same time the textual output of the robot gets updated. This textual output allows a rule based AI to control the robot automatically which wasn't implemented yet. The only difference to the normal micromouse challenge is, that in the example the mentioned text widget is visible.

The text gets produced with a dictionary of words. Possible words like [move, goal, blocked] are describing objects and events of the game. The program detects situations and uses the words to generate the output. Because there is a mapping between the game state and the textual output its called grounded language.

import math, sys, pygame

# 1. SETUP & CONFIGURATION
WIN_W, WIN_H, MAZE_H = 800, 700, 580
WHITE, BLACK, GREEN, BLUE, GRAY = (255, 255, 255), (0, 0, 0), (40, 180, 40), (40, 100, 220), (200, 200, 200)

# Grounded language vocabulary for rule-based AI parsing
DICT = {
    "verbs": ["detected", "reached", "turn", "move"],
    "nouns": ["wall", "path", "goal", "junction", "distance"],
    "adj": ["ahead", "left", "right", "clear", "blocked", "near"]
}

# Solvable Maze Walls [x, y, w, h] & Goal
WALLS = [
    pygame.Rect(0, 0, 800, 10), pygame.Rect(0, 0, 10, 580),
    pygame.Rect(790, 0, 10, 580), pygame.Rect(0, 570, 800, 10),
    pygame.Rect(150, 0, 10, 420), pygame.Rect(300, 160, 10, 420),
    pygame.Rect(450, 0, 10, 420), pygame.Rect(600, 160, 10, 420),
    pygame.Rect(150, 250, 80, 10), pygame.Rect(450, 350, 80, 10)
]
GOAL = pygame.Rect(680, 480, 80, 80)

# 2. ROBOT CLASS
class Micromouse:
    def __init__(self, x, y):
        self.x, self.y, self.angle, self.radius, self.sensor_len = x, y, 0, 14, 55

    def update(self, keys):
        speed = (2.5 if keys[pygame.K_UP] else 0) - (1.5 if keys[pygame.K_DOWN] else 0)
        self.angle = (self.angle + (3.5 if keys[pygame.K_RIGHT] else 0) - (3.5 if keys[pygame.K_LEFT] else 0)) % 360
        rad = math.radians(self.angle)
        nx, ny = self.x + math.cos(rad) * speed, self.y + math.sin(rad) * speed
        
        # Collision detection with walls
        nrect = pygame.Rect(nx - self.radius, ny - self.radius, self.radius * 2, self.radius * 2)
        collided = any(nrect.colliderect(w) for w in WALLS)
        if not collided:
            self.x, self.y = nx, ny
        return collided

    def sense(self):
        """Scans left (-45 deg), ahead (0 deg), right (+45 deg) for obstacles."""
        dirs, res = {"left": -45, "ahead": 0, "right": 45}, {}
        for d, off in dirs.items():
            rad = math.radians(self.angle + off)
            ex, ey = self.x + math.cos(rad) * self.sensor_len, self.y + math.sin(rad) * self.sensor_len
            res[d] = any(w.clipline((self.x, self.y), (ex, ey)) for w in WALLS)
        return res

    def draw(self, surface):
        pygame.draw.circle(surface, BLUE, (int(self.x), int(self.y)), self.radius)
        rad = math.radians(self.angle)
        pygame.draw.line(surface, WHITE, (self.x, self.y), (self.x + math.cos(rad)*self.radius, self.y + math.sin(rad)*self.radius), 3)

# 3. HELPER FOR WORD WRAPPING TEXT
def render_wrapped_text(surface, text, font, rect, color=BLACK):
    words = text.split(" ")
    lines, current_line = [], ""
    for word in words:
        test_line = f"{current_line} {word}".strip()
        if font.size(test_line)[0] <= rect.width:
            current_line = test_line
        else:
            lines.append(current_line)
            current_line = word
    lines.append(current_line)
    
    y = rect.y
    for line in lines:
        if y + font.get_height() <= rect.bottom:
            surface.blit(font.render(line, True, color), (rect.x, y))
            y += font.get_height() + 2

# 4. MAIN LOOP
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIN_W, WIN_H))
    pygame.display.set_caption("Grounded Language Micromouse")
    clock, font = pygame.time.Clock(), pygame.font.SysFont("Arial", 30, bold=False)
    mouse = Micromouse(50, 50)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()

        collided = mouse.update(pygame.key.get_pressed())
        sensors = mouse.sense()
        dist = int(math.hypot(mouse.x - GOAL.centerx, mouse.y - GOAL.centery))

        # Build rule-oriented grounded language output
        tokens = []
        if GOAL.collidepoint(mouse.x, mouse.y):
            tokens.append(f"{DICT['nouns'][2]} {DICT['verbs'][1]}")  # "goal reached"
        else:
            tokens.append(f"{DICT['nouns'][4]} to {DICT['nouns'][2]}: {dist}px")  # "distance to goal: Xpx"

        if collided:
            tokens.append(f"{DICT['nouns'][0]} collision {DICT['adj'][4]}")  # "wall collision blocked"
        
        # Tactical language for rule-based navigation AI
        blocked_dirs = [d for d, is_blocked in sensors.items() if is_blocked]
        if not blocked_dirs:
            tokens.append(f"{DICT['nouns'][1]} {DICT['adj'][3]}: {DICT['verbs'][3]} {DICT['adj'][0]}") # "path clear: move ahead"
        else:
            tokens.append(f"{DICT['nouns'][0]} {DICT['verbs'][0]} on " + " ".join(blocked_dirs))
            if sensors["ahead"]:
                suggested_turn = "right" if not sensors["right"] else ("left" if not sensors["left"] else "back")
                tokens.append(f"recommendation: {DICT['verbs'][2]} {suggested_turn}")

        # Render Scene
        screen.fill(WHITE)
        pygame.draw.rect(screen, GREEN, GOAL)
        screen.blit(font.render("GOAL", True, WHITE), (GOAL.x + 18, GOAL.y + 30))
        for w in WALLS: pygame.draw.rect(screen, BLACK, w)
        mouse.draw(screen)

        # Bottom Text Panel (White Background, Black Text, Word Wrapped)
        panel = pygame.Rect(0, MAZE_H, WIN_W, WIN_H - MAZE_H)
        pygame.draw.rect(screen, WHITE, panel)
        pygame.draw.line(screen, BLACK, (0, MAZE_H), (WIN_W, MAZE_H), 2)

        output_str = f"STATUS: [ {' | '.join(tokens)} ] | CONTROLS: Arrow Keys"
        render_wrapped_text(screen, output_str, font, pygame.Rect(10, MAZE_H + 8, WIN_W - 20, WIN_H - MAZE_H - 12))

        pygame.display.flip()
        clock.tick(60)

if __name__ == "__main__":
    main()

No comments:

Post a Comment