Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion HorizontalStrat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def one_step(self, x, y, vision, food):
self.direction = "WEST"
return self.direction
elif self.direction == "WEST":
if x > 0:
if x > 1:
return self.direction
else:
self.direction = "EAST"
Expand Down
56 changes: 35 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
Players code the behavior of ants that compete in teams to bring food back to their anthill.

## Introduction
Each team has 1-4 ants, which are placed on a grid. At the end of 200 rounds, whichever team has dropped the most food at their anthill wins. Ants have the ability to move, get food, drop food, and pass messages to their teammates.
Each team has 3-5 ants, which are placed on a grid. At the end of 200 rounds, whichever team has dropped the most food at their anthill wins. Ants have the ability to move, get food, drop food, and pass messages to their teammates.

### The Map
The map is randomly generated each round with 20-24 rows and columns.
It's printed to the console using the following symbols:

| Symbol | Meaning |
| :------------- | :------------- |
| # | Wall |
| 1-9 | Food pile |
| . | Empty |
| A-D, @ | North team ants & anthill |
| E-H, X | South team ants & anthill |
| Symbol | Meaning |
| :------------- | :------------- |
| # | Wall |
| 1-9 | Food pile |
| . | Empty |
| @ | North anthill |
| X | South anthill |
| A-C, [D, E] | North team of 3-5 ants |
| F-H, [I, J] | South team of 3-5 ants |

### Ant Commands
Ants have four moves they can make on their turn:
Expand All @@ -23,7 +25,7 @@ Ants have four moves they can make on their turn:
3. DROP [direction]: If carrying food, it will be dropped at the coordinates in the direction given.
4. GET [direction]: If there is food in the direction given, pick up the food. Ants can carry one food at a time.

All ants move simutaneously. If the actions of two or more ants conflict (possible with moving or GET), these ants' actions won't be executed. Raising an exception, taking too long, or returning an invalid command will result in your ant being eliminated. The game will print a hint of what went wrong.
All ants move simutaneously. If the actions of two or more ants conflict (possible with moving or GET), these ants' actions won't be executed. If two or more ants attempt to drop food on a cell, resulting in the cell having more than 9 food items at a time, these ants' actions also won't be executed. Raising an exception, taking too long, or returning an invalid command will result in your ant being eliminated. The game will print a hint of what went wrong.

The supported cardinal directions are as follows:
| | | |
Expand All @@ -47,12 +49,18 @@ Each `AntStrategy` has three methods for you to complete, which will be called i
2. `one_step()`: Using the state information about your ant passed in as arguments, decide on and return the next move for your ant.
3. `send_info()`: Return any messages you want to send this round.

All ants also have some information about the state of the game:
- Grid size: The dimensions of the grid, including the outer walls, will be passed to the ant upon initialization.
- Anthill symbol: The symbol for the ant's anthill, either `'X'` or `'@'`, will be passed to the ant upon initialization.
- Current coordinates: The ant's current position on the matrix will be passed to the ant every round in the `one_step()` method.

This class is fully documented, so see `AntStrategy.py` for more details about these methods and how the game interacts with the class.

## Running the Simulation
First, add an import to the list of `AntStrategy` subclass imports in `main.py` to import your class.
Then, find the two tuples called `team1` and `team2`.
Change the contents of these so that they're the names of the 1-5 AntStrategy classes you want on each team.
Change the contents of these so that they're the names of the 3-5 AntStrategy classes you want on each team.
Each team must have the same number of ants.
If using CodeHS, click Run! If using something else, execute `main.py` (on the commandline: `$ python3 main.py`).

### Saving and Loading a Map from a File
Expand All @@ -69,40 +77,46 @@ Always enter *relative* paths to the files you are saving from or loading
### Ant Vision
One of the pieces of state information passed to the `one_step()` method is the ant's `vision`.
This is a 3x3 list representing the locations in the map immediately under and around the ant with the ant at the center (`vision[1][1]`).
Ant vision prioritizes showing items and objects in the vicinity in the priority Ant > Food or Anthill or Wall > Empty space, meaning vision will not display the anthill or food in a cell if another ant is occupying that space.
Each index will have one of symbols in the key above, indicating what is around the ant in the map.
(0,0) is northwest of the ant.
Ants can "see" one unit in all directions.

### Message Passing
Each round, your ant will have the ability to receive messages sent by your team in the prior round and send messages.
Your team will need to decide what information you want to communicate and how to format your messages.
Suppose your team wants to share whether they're carrying food at the end of each round.

At the beginning of each round, the game will call `receive_info()` on your ant. It will pass a `list` of messages from your teammates that were sent in the previous round.
Parse each message according to how your team decides to send information, and update your ant's state if needed.
For instance, suppose your team wants to share whether they're carrying food at the end of each round.
You might choose the message format `"ANT1 FOOD " + str(food)`.
Then, if all members of your team follow this format,
at the start of each round, your ant may receive a list of messages such as, `["ANT1 FOOD False", "ANT2 FOOD True", "ANT3 FOOD False"]`.

`GridBuilderStrat` and `ScoutStrat` send messages about what they've discovered on the map in the format `str(x) + " " + str(y) + " " + item`.
Each of these examples is a string, but the messages don't have to be strings.
`SmarterRandomStrat` isn't very creative and just sends the word `"message"` to its teammates.
If you try to run GridBuilderStrat and SmarterRandomStrat together, you will see an error because they try to parse each others' messages incorrectly!
If you try to run `GridBuilderStrat` and `SmarterRandomStrat` together, you will see an error because they try to parse each others' messages incorrectly!

At the beginning of each round, the game will call `receive_info` on your ant. It will pass a `list` of messages from your teammates.
Parse each message according to the your team's format and update your ant's state if needed.
For example, `GridBuilderStrat` uses the messages to fill in its internal map of the playing field and even has some basic checks to avoid causing an exception if a message is the wrong format.
`GridBuilderStrat` uses the messages to fill in its internal map of the playing field and even has some basic checks to avoid causing an exception if a message is the wrong format.
```python3
def receive_info(self, messages):
for m in messages:
words = m.split()
if len(words) != 3:
print("Message incorrectly formatted: " + m);
print("Message incorrectly formatted: " + m)
continue
x, y, agent = words
self.grid[int(x)][int(y)] = agent
```

Then, the game will call the `one_step` method.
Do not call `send_info` yourself from `one_step`; it's called by the game after `one_step`.
Since you may want to share information learned during `one_step` with your teammates, one solution is to create an instance variable for outgoing messages, e.g. `self.outbox = []`.
Then, the game will call the `one_step()` method.
Do not call `send_info()` yourself from `one_step()`; it's called by the game after `one_step()`.
Since you may want to share information learned during `one_step` with your teammates,
one solution is to create an instance variable for outgoing messages, e.g. `self.outbox = []`.

In `send_info`, your ant can return a `list` of messages.
Here's an example where an ant shares if it's carrying food. Note that this method still returns a list even though there's only one message.
Finally, in `send_info()`, your ant must return a `list` of messages.
Here's an example where an ant shares if it's carrying food. Note that this method still returns a list even though there's only one message. It may return an empty list if there are no messages to send.
```python3
def send_info(self):
'''Send whether or not I'm carrying food'''
Expand Down
104 changes: 84 additions & 20 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
from GridBuilderStrat import GridBuilderStrat
from ScoutStrat import ScoutStrat
from StarterStrat import StarterStrat
from FoodHoardingStrat import FoodHoardingStrat

# B. Register strategy class names in team1/team2 tuples below, 1-5 ants per team
team1 = (RandomStrat, SmarterRandomStrat, StraightHomeStrat, ScoutStrat, RandomStrat)
team2 = (GridBuilderStrat, SmarterRandomStrat, HorizontalStrat, VerticalStrat, RandomStrat)
# B. Register strategy class names in team1/team2 tuples below, 3-5 ants per team
team1 = (GridBuilderStrat, RandomStrat, SmarterRandomStrat, StraightHomeStrat, HorizontalStrat)
team2 = (RandomStrat, HorizontalStrat, StarterStrat, HorizontalStrat, ScoutStrat)
DEBUG = False # Change this to True to get more detailed errors from ant strategies

# --- Begin Game ---
Expand Down Expand Up @@ -105,6 +106,7 @@ def get_symbol(self):
def __repr__(self):
return self.symbol

# utility functions
def is_open_cell(matrix, x, y, ant=''):
"""Check if a cell in matrix is in bounds and not a wall."""
if (x > 0 and x < len(matrix) and y > 0 and y < len(matrix[0]) and not matrix[x][y].wall):
Expand All @@ -114,14 +116,15 @@ def is_open_cell(matrix, x, y, ant=''):

return False

def initialize_ants(team1_strats, team1_locs, team2_strats, team2_locs, rows, cols):
def initialize_ants(team1_strats, team1_locs, team2_strats, team2_locs, rows, cols, config):
"""Instantiate ant classes for each team.

Populate ants list and dictionary of Ant->matrixSymbol mappings. Takes two
lists for each team: AntStrategy class names and inital (x, y) positions
"""

# Team 1
for Strat, sym in zip(team1_strats, NORTH_SYMS):
for Strat, sym in zip(team1_strats, NORTH_SYMS[:config['ants_count']]):
try:
ant_strat = Strat(rows, cols, NORTH_HILL)
except Exception as e:
Expand All @@ -132,7 +135,7 @@ def initialize_ants(team1_strats, team1_locs, team2_strats, team2_locs, rows, co
ants.append(Ant(ant_strat, team1_locs[sym][0], team1_locs[sym][1], 1, sym))

# Team 2
for Strat, sym in zip(team2_strats, SOUTH_SYMS):
for Strat, sym in zip(team2_strats, SOUTH_SYMS[:config['ants_count']]):
try:
ant_strat = Strat(rows, cols, SOUTH_HILL)
except Exception as e:
Expand All @@ -142,18 +145,20 @@ def initialize_ants(team1_strats, team1_locs, team2_strats, team2_locs, rows, co
continue
ants.append(Ant(ant_strat, team2_locs[sym][0], team2_locs[sym][1], 2, sym))

def generate_game_config():
def generate_game_config(team1_strats, team2_strats):
"""Prompt user for game configuration options, including saved map file

Returns: dict[str, boolean or str], with the following keys:
'fast_forward': boolean, continue to end without stopping
'load_map': boolean, use saved map
'save_file': str, filename if load_map is True
'ants_count': int, number of ants in each team
"""
config = {
'fast_forward': False,
'load_map': False,
'save_file': None,
'fast_forward': False,
'load_map': False,
'save_file': None,
'ants_count': 0,
}

fast_forward = input("Run to the end without pausing? (yes/<enter>) ")
Expand All @@ -172,12 +177,27 @@ def generate_game_config():
print("File not found, using new map: " + filepath)
config['load_map'] = False

if (len(team1_strats) == len(team2_strats)):
if (len(team1_strats) <= 5 and len(team1_strats) >= 3):
config['ants_count'] = len(team1_strats)
else:
raise ValueError("Teams must have between 3-5 ants (inclusive).")
else:
raise ValueError("Teams must have the same number of ants.")

# uncomment these lines if varying number of active players per team per game
# ants_count = input("Number of ants on each team? (3-5) ")
# while(not ants_count.isnumeric() or int(ants_count) > 5 or int(ants_count) < 3):
# print("Teams must have between 3 to 5 ants.")
# ants_count = input("Number of ants on each team? (3-5) ")
# config['ants_count'] = int(ants_count)

return config

def load_save_file(filename):
"""Load saved game data from a file.

Trusts that map is valid format, with walls, 10 ants, and 2 anthills.
Trusts that map is valid format, with walls, 6-10 ants, and 2 anthills.

Returns:
Dict[str, int or str] of game data with following keys:
Expand Down Expand Up @@ -323,10 +343,22 @@ def construct_map(config):
bottom_hill = cols-(int((cols)/2))-1
else:
bottom_hill = cols-(int((cols)/2))
team1_starting = {'A': (3,1), 'B': (6,1), 'C': (top_hill,1), 'D': (cols-7,1), 'E': (cols-4,1)}
team2_starting = {'F': (3,rows-2), 'G': (6,rows-2), 'H': (bottom_hill, rows-2), 'I': (cols-7,rows-2), 'J': (cols-4,rows-2)}

initialize_ants(team1, team1_starting, team2, team2_starting, len(matrix), len(matrix[0]))
match config['ants_count']:
case 3:
team1_starting = {'A': (4,1), 'B': (top_hill,1), 'C': (cols-5,1)}
team2_starting = {'F': (4,rows-2), 'G': (bottom_hill,rows-2), 'H': (cols-5,rows-2)}
case 4:
team1_starting = {'A': (3,1), 'B': (7,1), 'C': (cols-8,1), 'D': (cols-4,1)}
team2_starting = {'F': (3,rows-2), 'G': (7,rows-2), 'H': (cols-8,rows-2), 'I': (cols-4,rows-2)}
case 5:
team1_starting = {'A': (3,1), 'B': (6,1), 'C': (top_hill,1), 'D': (cols-7,1), 'E': (cols-4,1)}
team2_starting = {'F': (3,rows-2), 'G': (6,rows-2), 'H': (bottom_hill,rows-2), 'I': (cols-7,rows-2), 'J': (cols-4,rows-2)}
case _:
team1_starting = {'A': (3,1), 'B': (6,1), 'C': (top_hill,1), 'D': (cols-7,1), 'E': (cols-4,1)}
team2_starting = {'F': (3,rows-2), 'G': (6,rows-2), 'H': (bottom_hill,rows-2), 'I': (cols-7,rows-2), 'J': (cols-4,rows-2)}

initialize_ants(team1, team1_starting, team2, team2_starting, len(matrix), len(matrix[0]), config)
place_ants(matrix, ants)
return matrix

Expand Down Expand Up @@ -443,7 +475,9 @@ def game_loop(matrix, ants, config):

# Parse moves
proposed_moves = {}
conflict_sites = {}
proposed_gets = {}
proposed_drops = {}
for a, move in moves.items():
loc = (a.x, a.y)

Expand Down Expand Up @@ -481,21 +515,33 @@ def game_loop(matrix, ants, config):
team1_points += 1
elif cell.anthill == SOUTH_HILL:
team2_points += 1

else:
cell.food += 1
if (target_x, target_y) in proposed_drops:
proposed_drops[(target_x, target_y)].append(a)
else:
proposed_drops[(target_x, target_y)] = [a]

elif move[0] != "PASS":
print("Invalid move from " + a.symbol + ": " + str(move))
kill_ant(a)
continue

# Attempt to place this ant in next phase of simulation. Ants in conflict must go back
print("current ant:", a)

if loc not in proposed_moves:
proposed_moves[loc] = a
else:
conflict_ant = proposed_moves[loc]
print("Collision between " + a.symbol + " and " + conflict_ant.symbol)

# Record conflict site
print("loc", loc)
print(conflict_sites)
if (loc in conflict_sites):
conflict_sites[loc].append(a.symbol)
else:
conflict_sites[loc] = [a.symbol, conflict_ant.symbol]

# Return this ant to original position, resolving any chains of conflicts
proposed_moves[loc] = None # No one gets to be here
current_ant = a
Expand All @@ -515,7 +561,7 @@ def game_loop(matrix, ants, config):
else:
if conflict_ant:
proposed_moves[(conflict_ant.x, conflict_ant.y)] = conflict_ant

# Resolve proposed gets
for (target_x, target_y), aList in proposed_gets.items():
if matrix[target_x][target_y].food > 0 and matrix[target_x][target_y].food >= len(aList): #here
Expand All @@ -525,13 +571,31 @@ def game_loop(matrix, ants, config):
else: ## insufficient food
print("Invalid GET in " + a.symbol + ": " + str(move))

# Resolve proposed drops
for (target_x, target_y), aList in proposed_drops.items():
if (matrix[target_x][target_y].food < 9 - len(aList)):
matrix[target_x][target_y].food += len(aList)
else: ## too much food on a tile
for a in aList:
a.food = True ## don't allow ant to drop here
print("Invalid DROP in " + a.symbol + ": " + str(move))

# Update arena & redraw screen
for loc, a in proposed_moves.items():
if a: # May be none if it was the site of a movement conflict
if (a): # May be none if it was the site of a movement conflict
matrix[a.x][a.y].ant = None
a.x = loc[0]
a.y = loc[1]

# Notify user of collisions
for loc, aList in conflict_sites.items():
if (len(aList) == 2):
print("Ants " + aList[0] + " and " + aList[1] + " collided.")
elif (len(aList) > 2):
print("Ants " + ", ".join(aList[:-1]) + ", and " + aList[-1] + " collided.")
else:
print("Something went wrong.")

place_ants(matrix, ants)
print_map(matrix)
print("Round:", lap, "Team 1:", str(team1_points), "Team 2:", str(team2_points))
Expand Down Expand Up @@ -611,7 +675,7 @@ def prompt_save_map(initial_matrix):
if __name__ == '__main__':
random.seed()
ants = []
config = generate_game_config()
config = generate_game_config(team1, team2)
matrix = construct_map(config)
initial_matrix = matrix_to_str_list(matrix)
game_loop(matrix, ants, config)
Expand Down