Building the Brains

Each ant is going to have four states in its state machine, which should be enough to simulate ant-like behavior. The first step in defining the state machine is to work out what each state should do, which are the actions for the state (see Table 7-1).

Table 7-1. Actions for the Ant States

State

Actions

Exploring

Walk toward a random point in the world.

Seeking

Head toward a leaf.

Delivering

Deliver something to the nest.

Hunting

Chase a spider.

We also need to define the links that connect states together. These take the form of a condition and the name of the state to switch to if the condition is met. The exploring state, for example, has two such links (see Table 7-2).

Table 7-2. Links from Exploring State

Condition

Destination State

Seen a leaf?

Seeking

Spider attacking base?

Hunting

Once we have defined the links between the states, we have a state machine that can be used as the brain for an entity. Figure 7-3 shows the complete state machine that we will be building for the ant. Drawing a state machine out on paper like this is a great way of visualizing how it all fits together, and will help you when you need to turn it into code.

Let's put this into practice and create the code for the state machine. We will begin by defining a base class for an individual state (Listing 7-6). Later we will create another class for the state machine as a whole that will manage the states it contains.

The base State class doesn't actually do anything other than store the name of the state in the constructor. The remaining functions in State do nothing—the pass keyword simply tells Python that you intentionally left the function blank. We need these empty functions because not all of the states we will be building will implement all of the functions in the base class. The exploring state, for example, has no exit actions. When we come to implement the AntStateExploring class, we can omit the exit_actions function because it will safely fall back to the do-nothing version of the function in the base class (State).

Listing 7-6. Base Class for a State class State(object):

def __init__(self, name): self.name = name def do_actions(self): pass def check_conditions(self): pass def entry_actions(self): pass def exit_actions(self): pass

Before we build the states, we need to build a class that will manage them. The StateMachine class (Listing 7-7) stores an instance of each of the states in a dictionary and manages the currently active state. The think function runs once per frame, and calls the do_actions on the active state— to do whatever the state was designed to do; the exploring state will select random places to walk to, the seeking state will move toward the leaf, and so forth. The think function also calls the state's check_conditions function to check all of the link conditions. If check_conditions returns a string, a new active state will be selected and any exit and entry actions will run.

Listing 7-7. The State Machine Class class StateMachine(object):

self.states = {} # Stores the states self.active_state = None # The currently active state def add_state(self, state):

# Add a state to the internal dictionary self.states[state.name] = state def think(self):

# Only continue if there is an active state if self.active_state is None: return

# Perform the actions of the active state, and check conditions self.active_state.do_actions()

new_state_name = self.active_state.check_conditions() if new_state_name is not None: self.set_state(new_state_name)

def set_state(self, new_state_name):

# Change states and perform any exit / entry actions if self.active_state is not None:

self.active_state.exit_actions()

self.active_state = self.states[new_state_name] self.active_state.entry_actions()

Now that we have a functioning state machine class, we can start implementing each of the individual states by deriving from the State class and implementing some of its functions. The first state we will implement is the exploring state, which we will call AntStateExploring (see Listing 7-8). The entry actions for this state give the ant a random speed and set its destination to a random point on the screen. The main actions, in the do_actions function, select another random destination if the expression randint(1, 20) == 1 is true, which will happen in about 1 in every 20 calls, since randint (in the random module) selects a random number that is greater than or equal to the first parameter, and less than or equal to the second. This gives us the antlike random searching behavior we are looking for.

The two outgoing links for the exploring state are implemented in the check_conditions function. The first condition looks for a leaf entity that is within 100 pixels from an ant's location (because that's how far our ants can see). If there is a nearby leaf, then check_conditions records its id and returns the string seeking, which will instruct the state machine to switch to the seeking state. The remaining condition will switch to hunting if there are any spiders inside the nest and within 100 pixels of the ant's location.

■Caution Random numbers are a good way to make your game more fun, because predictable games can get dull after a while. But be careful with random numbers—if something goes wrong, it may be difficult to reproduce the problem!

Listing 7-8. The Exploring State for Ants (AntStateExploring) class AntStateExploring(State):

# Call the base class constructor to initialize the State State._init_(self, "exploring")

# Set the ant that this State will manipulate self.ant = ant def random_destination(self):

# Select a point in the screen w, h = SCREEN_SIZE

self.ant.destination = Vector2(randint(0, w), randint(0, h))

def do_actions(self):

# Change direction, 1 in 20 calls if randint(1, 20) == 1:

self.random_destination()

def check_conditions(self):

# If there is a nearby leaf, switch to seeking state leaf = self.ant.world.get_close_entity("leaf", self.ant.location)

if leaf is not None:

self.ant.leaf_id = leaf.id return "seeking"

# If there is a nearby spider, switch to hunting state spider = self.ant.world.get_close_entity("spider", NEST_POSITION, NEST_SIZE)

if spider is not None:

if self.ant.location.get_distance_to(spider.location) < 100.: self.ant.spider_id = spider.id return "hunting"

return None def entry_actions(self):

# Start with random speed and heading self.ant.speed = 120. + randint(-30, 30) self.random_destination()

As you can see from Listing 7-8, the code for an individual state need not be very complex because the states work together to produce something that is more than the sum of its parts. The other states are similar to AntStateExploring in that they pick a destination based on the goal of that state and switch to another state if they have accomplished that goal, or it no longer becomes relevant.

There is not a great deal left to do in the main loop of the game. Once the World object has been created, we simply call process and render once per frame to update and draw everything in the simulation. Also in the main loop are a few lines of code to create leaf entities at random positions in the world and occasionally create spider entities that wander in from the left side of the screen.

Listing 7-9 shows the entire simulation. When you run it, you will see something like Figure 7-4; the ants roam around the screen collecting leaves and killing spiders, which they will pile up in the nest. You can see that the ants satisfy the criteria of being AIs because they are aware of their environment—in a limited sense—and take actions accordingly.

Although there is no player character in this simulation, this is the closest we have come to a true game. We have a world, an entity framework, and artificial intelligence. It could be turned into a game with the addition of a player character. You could define a completely new entity for the player, perhaps a praying mantis that has to eat the ants, or add keyboard control to the spider entity and have it collect eggs from the nest. Alternatively, the simulation is a great starting point for a strategy game where groups of ants can be sent to collect leaves or raid neighboring nests. Game developers should be as imaginative as possible!

Game Pygame
Figure 7-4. The ant simulation

Listing 7-9. The CompleteAISimulation (antstatemachine.py)

# Some constants you can modify SCREEN_SIZE = (640, 480) NEST_POSITION = (320, 240) ANT_COUNT = 20 NEST_SIZE = 100.

import pygame from pygame.locals import *

from random import randint, choice from gameobjects.vector2 import Vector2

class State(object):

self.name = name def do_actions(self): pass def check_conditions(self): pass def entry_actions(self): pass def exit_actions(self): pass class StateMachine(object):

self.states = {} self.active_state = None def add_state(self, state):

self.states[state.name] = state def think(self):

if self.active_state is None: return self.active_state.do_actions()

new_state_name = self.active_state.check_conditions() if new_state_name is not None: self.set_state(new_state_name)

def set_state(self, new_state_name):

if self.active_state is not None: self.active_state.exit_actions()

self.active_state = self.states[new_state_name] self.active_state.entry_actions()

class World(object):

self.background = pygame.surface.Surface(SCREEN_SIZE).convert() self.background.fill((255, 255, 255))

pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION,^ int(NEST_SIZE))

def add_entity(self, entity):

self.entities[self.entity_id] = entity entity.id = self.entity_id self.entity_id += 1

def remove_entity(self, entity):

del self.entities[entity.id]

def get(self, entity_id):

if entity_id in self.entities:

return self.entities[entity_id] else:

return None def process(self, time_passed):

time_passed_seconds = time_passed / 1000.0 for entity in self.entities.values(): entity.process(time_passed_seconds)

def render(self, surface):

surface.blit(self.background, (0, 0)) for entity in self.entities.itervalues(): entity.render(surface)

def get_close_entity(self, name, location, range=100.):

location = Vector2(*location)

for entity in self.entities.itervalues(): if entity.name == name:

distance = location.get_distance_to(entity.location) if distance < range: return entity return None class GameEntity(object):

self.world = world self.name = name self.image = image self.location = Vector2(0, 0) self.destination = Vector2(0, 0) self.speed = 0.

self.brain = StateMachine()

def render(self, surface):

def process(self, time_passed):

self.brain.think()

if self.speed > 0. and self.location != self.destination:

vec_to_destination = self.destination - self.location distance_to_destination = vec_to_destination.get_length() heading = vec_to_destination.get_normalized()

travel_distance = min(distance_to_destination, time_passed * self.speed) self.location += travel_distance * heading class Leaf(GameEntity):

GameEntity.__init__(self, world, "leaf", image)

class Spider(GameEntity):

GameEntity._init_(self, world, "spider", image)

# Make a 'dead' spider image by turning it upside down self.dead_image = pygame.transform.flip(image, 0, 1)

self.health = 25

self.speed = 50. + randint(-20, 20) def bitten(self):

# Spider as been bitten self.health -= 1

if self.health <= 0: self.speed = 0. self.image = self.dead_image self.speed = 140.

def render(self, surface):

GameEntity.render(self, surface)

# Draw a health bar x, y = self.location w, h = self.image.get_size() bar_x = x - 12 bar_y = y + h/2

surface.fill( (255, 0, 0), (bar_x, bar_y, 25, 4)) surface.fill( (0, 255, 0), (bar_x, bar_y, self.health, 4))

def process(self, time_passed):

self.world.remove_entity(self) return

GameEntity.process(self, time_passed)

class Ant(GameEntity):

GameEntity._init_(self, world, "ant", image)

# State classes are defined below exploring_state = AntStateExploring(self) seeking_state = AntStateSeeking(self) delivering_state = AntStateDelivering(self) hunting_state = AntStateHunting(self)

self.brain.add_state(exploring_state) self.brain.add_state(seeking_state) self.brain.add_state(delivering_state) self.brain.add_state(hunting_state)

self.carry_image = None def carry(self, image):

self.carry_image = image def drop(self, surface):

if self.carry_image: x, y = self.location w, h = self.carry_image.get_size() surface.blit(self.carry_image, (x-w, y-h/2)) self.carry_image = None def render(self, surface):

GameEntity.render(self, surface)

if self.carry_image: x, y = self.location w, h = self.carry_image.get_size() surface.blit(self.carry_image, (x-w, y-h/2))

class AntStateExploring(State):

State._init_(self, "exploring")

self.ant = ant def random_destination(self):

self.ant.destination = Vector2(randint(0, w), randint(0, h))

def do_actions(self):

self.random_destination()

def check_conditions(self):

# If ant sees a leaf, go to the seeking state leaf = self.ant.world.get_close_entity("leaf", self.ant.location) if leaf is not None:

self.ant.leaf_id = leaf.id return "seeking"

# If the ant sees a spider attacking the base, go to hunting state spider = self.ant.world.get_close_entity("spider", NEST_POSITION, NEST_SIZE) if spider is not None:

if self.ant.location.get_distance_to(spider.location) < 100.: self.ant.spider_id = spider.id return "hunting"

return None def entry_actions(self):

self.ant.speed = 120. + randint(-30, 30) self.random_destination()

class AntStateSeeking(State):

State._init_(self, "seeking")

self.ant = ant self.leaf_id = None def check_conditions(self):

# If the leaf is gone, then go back to exploring leaf = self.ant.world.get(self.ant.leaf_id)

if leaf is None:

return "exploring"

# If we are next to the leaf, pick it up and deliver it if self.ant.location.get_distance_to(leaf.location) < 5.0:

self.ant.carry(leaf.image) self.ant.world.remove_entity(leaf) return "delivering"

return None def entry_actions(self):

# Set the destination to the location of the leaf leaf = self.ant.world.get(self.ant.leaf_id)

if leaf is not None:

self.ant.destination = leaf.location self.ant.speed = 160. + randint(-20, 20)

class AntStateDelivering(State):

State._init_(self, "delivering")

self.ant = ant def check_conditions(self):

# If inside the nest, randomly drop the object if Vector2(*NEST_POSITION).get_distance_to(self.ant.location) < NEST_SIZE: if (randint(1, 10) == 1):

self.ant.drop(self.ant.world.background) return "exploring"

return None def entry_actions(self):

# Move to a random point in the nest self.ant.speed = 60.

random_offset = Vector2(randint(-20, 20), randint(-20, 20)) self.ant.destination = Vector2(*NEST_POSITION) + random_offset class AntStateHunting(State):

State.__init__(self, "hunting") self.ant = ant self.got_kill = False def do_actions(self):

spider = self.ant.world.get(self.ant.spider_id)

if spider is None: return self.ant.destination = spider.location if self.ant.location.get_distance_to(spider.location) < 15.:

# Give the spider a fighting chance to avoid being killed! if randint(1, 5) == 1: spider.bitten()

# If the spider is dead, move it back to the nest if spider.health <= 0:

self.ant.carry(spider.image) self.ant.world.remove_entity(spider) self.got_kill = True def check_conditions(self):

if self.got_kill:

return "delivering"

spider = self.ant.world.get(self.ant.spider_id)

# If the spider has been killed then return to exploring state if spider is None:

return "exploring"

# If the spider gets far enough away, return to exploring state if spider.location.get_distance_to(NEST_POSITION) > NEST_SIZE * 3:

return "exploring"

return None def entry_actions(self):

self.speed = 160. + randint(0, 50) def exit_actions(self): self.got_kill = False def run():

pygame.init()

screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

clock = pygame.time.Clock()

ant_image = pygame.image.load("ant.png").convert_alpha() leaf_image = pygame.image.load("leaf.png").convert_alpha() spider_image = pygame.image.load("spider.png").convert_alpha()

# Add all our ant entities for ant_no in xrange(ANT_COUNT):

ant = Ant(world, ant_image)

ant.location = Vector2(randint(0, w), randint(0, h))

ant.brain.set_state("exploring")

world.add_entity(ant)

while True:

for event in pygame.event.get(): if event.type == QUIT: return time_passed = clock.tick(30)

# Add a leaf entity 1 in 20 frames if randint(1, 10) == 1:

leaf = Leaf(world, leaf_image)

leaf.location = Vector2(randint(0, w), randint(0, h)) world.add_entity(leaf)

# Add a spider entity 1 in 100 frames if randint(1, 100) == 1:

spider = Spider(world, spider_image) spider.location = Vector2(-50, randint(0, h)) spider.destination = Vector2(w+50, randint(0, h)) world.add_entity(spider)

world.process(time_passed) world.render(screen)

pygame.display.update()

Was this article helpful?

0 0

Post a comment