Skip to content

Commit

Permalink
Base Bot (#55)
Browse files Browse the repository at this point in the history
* Most simple bots

* Fixed MineController

* Added autoselling

* Added take_action to OreOccupiableStation

* Fixes and allow multiple moves

* passed correct reference

* Used a result to get action list

* Drop rate mining

Incorporated drop rates to add multiple ores when the avatar mines

* thr.result default added

* Fixed cashing in and updated base client

* Patched scores and checked name length

* Switched bots to pure chaos, and allowed random

* removed out.mp4 and the generation cause. Fixed ore generation to allow for new ores to be generated. fixed infinite loop in inventory_manager.py

* bot updates

* Fixed crashing

* Copied to base_client_2

* Fixed tests

* Made requested changes

* Fixed tests

* Fixed activated sprites

---------

Co-authored-by: Jean A. Eckelberg <[email protected]>
Co-authored-by: KingPhilip14 <[email protected]>
  • Loading branch information
3 people authored Jan 3, 2024
1 parent 9b02cea commit 0172abe
Show file tree
Hide file tree
Showing 35 changed files with 378 additions and 155 deletions.
67 changes: 65 additions & 2 deletions base_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import random

from game.client.user_client import UserClient
from game.common.enums import *
from game.utils.vector import Vector


class State(Enum):
MINING = auto()
SELLING = auto()


class Client(UserClient):
Expand All @@ -12,7 +20,16 @@ def team_name(self):
Allows the team to set a team name.
:return: Your team name
"""
return 'Team 1'
return 'The King\'s Lambdas 3'

def first_turn_init(self, world, avatar):
"""
This is where you can put setup for things that should happen at the beginning of the first turn
"""
self.company = avatar.company
self.my_station_type = ObjectType.TURING_STATION if self.company == Company.TURING else ObjectType.CHURCH_STATION
self.current_state = State.MINING
self.base_position = world.get_objects(self.my_station_type)[0][0]

# This is where your AI will decide what to do
def take_turn(self, turn, actions, world, avatar):
Expand All @@ -22,4 +39,50 @@ def take_turn(self, turn, actions, world, avatar):
:param actions: This is the actions object that you will add effort allocations or decrees to.
:param world: Generic world information
"""
pass
if turn == 1:
self.first_turn_init(world, avatar)

current_tile = world.game_map[avatar.position.y][avatar.position.x] # set current tile to the tile that I'm standing on

# If I start the turn on my station, I should...
if current_tile.occupied_by.object_type == self.my_station_type:
# buy Improved Mining tech if I can...
if avatar.science_points >= avatar.get_tech_info('Improved Drivetrain').cost and not avatar.is_researched('Improved Drivetrain'):
return [ActionType.BUY_IMPROVED_DRIVETRAIN]
# otherwise set my state to mining
self.current_state = State.MINING

# If I have at least 5 items in my inventory, set my state to selling
if len([item for item in world.inventory_manager.get_inventory(self.company) if item is not None]) >= 5:
self.current_state = State.SELLING

# Make action decision for this turn
if self.current_state == State.SELLING:
# actions = [ActionType.MOVE_LEFT if self.company == Company.TURING else ActionType.MOVE_RIGHT] # If I'm selling, move towards my base
actions = self.generate_moves(avatar.position, self.base_position, turn % 2 == 0)
else:
if current_tile.occupied_by.object_type == ObjectType.ORE_OCCUPIABLE_STATION:
# If I'm mining and I'm standing on an ore, mine it
actions = [ActionType.MINE]
else:
# If I'm mining and I'm not standing on an ore, move randomly
actions = [random.choice([ActionType.MOVE_LEFT, ActionType.MOVE_RIGHT, ActionType.MOVE_UP, ActionType.MOVE_DOWN])]

return actions

def generate_moves(self, start_position, end_position, vertical_first):
"""
This function will generate a path between the start and end position. It does not consider walls and will
try to walk directly to the end position.
:param start_position: Position to start at
:param end_position: Position to get to
:param vertical_first: True if the path should be vertical first, False if the path should be horizontal first
:return: Path represented as a list of ActionType
"""
dx = end_position.x - start_position.x
dy = end_position.y - start_position.y

horizontal = [ActionType.MOVE_LEFT] * -dx if dx < 0 else [ActionType.MOVE_RIGHT] * dx
vertical = [ActionType.MOVE_UP] * -dy if dy < 0 else [ActionType.MOVE_DOWN] * dy

return vertical + horizontal if vertical_first else horizontal + vertical
67 changes: 65 additions & 2 deletions base_client_2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import random

from game.client.user_client import UserClient
from game.common.enums import *
from game.utils.vector import Vector


class State(Enum):
MINING = auto()
SELLING = auto()


class Client(UserClient):
Expand All @@ -12,7 +20,16 @@ def team_name(self):
Allows the team to set a team name.
:return: Your team name
"""
return 'Team 2'
return 'Unpaid Intern'

def first_turn_init(self, world, avatar):
"""
This is where you can put setup for things that should happen at the beginning of the first turn
"""
self.company = avatar.company
self.my_station_type = ObjectType.TURING_STATION if self.company == Company.TURING else ObjectType.CHURCH_STATION
self.current_state = State.MINING
self.base_position = world.get_objects(self.my_station_type)[0][0]

# This is where your AI will decide what to do
def take_turn(self, turn, actions, world, avatar):
Expand All @@ -22,4 +39,50 @@ def take_turn(self, turn, actions, world, avatar):
:param actions: This is the actions object that you will add effort allocations or decrees to.
:param world: Generic world information
"""
pass
if turn == 1:
self.first_turn_init(world, avatar)

current_tile = world.game_map[avatar.position.y][avatar.position.x] # set current tile to the tile that I'm standing on

# If I start the turn on my station, I should...
if current_tile.occupied_by.object_type == self.my_station_type:
# buy Improved Mining tech if I can...
if avatar.science_points >= avatar.get_tech_info('Improved Drivetrain').cost and not avatar.is_researched('Improved Drivetrain'):
return [ActionType.BUY_IMPROVED_DRIVETRAIN]
# otherwise set my state to mining
self.current_state = State.MINING

# If I have at least 5 items in my inventory, set my state to selling
if len([item for item in world.inventory_manager.get_inventory(self.company) if item is not None]) >= 5:
self.current_state = State.SELLING

# Make action decision for this turn
if self.current_state == State.SELLING:
# actions = [ActionType.MOVE_LEFT if self.company == Company.TURING else ActionType.MOVE_RIGHT] # If I'm selling, move towards my base
actions = self.generate_moves(avatar.position, self.base_position, turn % 2 == 0)
else:
if current_tile.occupied_by.object_type == ObjectType.ORE_OCCUPIABLE_STATION:
# If I'm mining and I'm standing on an ore, mine it
actions = [ActionType.MINE]
else:
# If I'm mining and I'm not standing on an ore, move randomly
actions = [random.choice([ActionType.MOVE_LEFT, ActionType.MOVE_RIGHT, ActionType.MOVE_UP, ActionType.MOVE_DOWN])]

return actions

def generate_moves(self, start_position, end_position, vertical_first):
"""
This function will generate a path between the start and end position. It does not consider walls and will
try to walk directly to the end position.
:param start_position: Position to start at
:param end_position: Position to get to
:param vertical_first: True if the path should be vertical first, False if the path should be horizontal first
:return: Path represented as a list of ActionType
"""
dx = end_position.x - start_position.x
dy = end_position.y - start_position.y

horizontal = [ActionType.MOVE_LEFT] * -dx if dx < 0 else [ActionType.MOVE_RIGHT] * dx
vertical = [ActionType.MOVE_UP] * -dy if dy < 0 else [ActionType.MOVE_DOWN] * dy

return vertical + horizontal if vertical_first else horizontal + vertical
20 changes: 13 additions & 7 deletions game/common/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@ def position(self) -> Vector | None:
return self.__position

@property
def movement_speed(self):
def movement_speed(self) -> int:
return self.__movement_speed

@property
def drop_rate(self):
def drop_rate(self) -> int:
return self.__drop_rate

@property
Expand Down Expand Up @@ -301,7 +301,8 @@ def __unlock_trap_defusal(self) -> None:

# Helper method to create a dictionary that stores bool values for which abilities the player unlocked
def __create_abilities_dict(self) -> dict:
abilities = {'Improved Drivetrain': False,
abilities = {'Mining Robotics': True,
'Improved Drivetrain': False,
'Superior Drivetrain': False,
'Overdrive Drivetrain': False,
'Improved Mining': False,
Expand Down Expand Up @@ -336,9 +337,6 @@ def buy_new_tech(self, tech_name: str) -> bool:

return successful

def get_tech_tree(self) -> TechTree:
return self.__tech_tree

def is_researched(self, tech_name: str) -> bool:
"""Returns if the given tech was researched."""
return self.__tech_tree.is_researched(tech_name)
Expand All @@ -350,6 +348,13 @@ def get_researched_techs(self) -> list[str]:
def get_all_tech_names(self) -> list[str]:
"""Returns a list of all possible tech names in a Tech Tree."""
return self.__tech_tree.tech_names()

def get_tech_info(self, tech_name: str) -> TechInfo | None:
"""
Returns a TechInfo object about the tech with the given name if the tech is found in the tree.
Returns None if the tech isn't found
"""
return self.__tech_tree.tech_info(tech_name)

# Dynamite placing functionality ----------------------------------------------------------------------------------
# if avatar calls place dynamite, set to true, i.e. they want to place dynamite
Expand Down Expand Up @@ -394,7 +399,8 @@ def from_json(self, data: dict) -> Self:
self.movement_speed = data['movement_speed']
self.drop_rate = data['drop_rate']
self.abilities = data['tech_tree']
self.__tech_tree = data['tech_tree']
self.__tech_tree = self.__create_tech_tree()
self.__tech_tree.from_json(data['tech_tree'])
self.dynamite_active_ability = DynamiteActiveAbility().from_json(data['dynamite_active_ability'])
self.landmine_active_ability = LandmineActiveAbility().from_json(data['landmine_active_ability'])
self.emp_active_ability = EMPActiveAbility().from_json(data['emp_active_ability'])
Expand Down
4 changes: 3 additions & 1 deletion game/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

MAX_SECONDS_PER_TURN = 0.1 # max number of basic operations clients have for their turns

MAX_NUMBER_OF_ACTIONS_PER_TURN = 2 # max number of actions per turn is currently set to 2
MAX_NUMBER_OF_ACTIONS_PER_TURN = 5 # master_controller will be handling max actions enforcement for Byte-le 2024 "Quarry Rush"

MIN_CLIENTS_START = None # minimum number of clients required to start running the game; should be None when SET_NUMBER_OF_CLIENTS is used
MAX_CLIENTS_START = None # maximum number of clients required to start running the game; should be None when SET_NUMBER_OF_CLIENTS is used
Expand All @@ -29,11 +29,13 @@

ALLOWED_MODULES = ["game.client.user_client", # modules that clients are specifically allowed to access
"game.common.enums",
"game.utils.vector",
"numpy",
"scipy",
"pandas",
"itertools",
"functools",
"random",
]

RESULTS_FILE_NAME = "results.json" # Name and extension of results file
Expand Down
3 changes: 2 additions & 1 deletion game/controllers/buy_tech_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def handle_actions(self, action: ActionType, client: Player, world: GameBoard):
case ActionType.BUY_TRAP_DEFUSAL:
tech_name = 'Trap Defusal'

client.avatar.buy_new_tech(tech_name) # buy the tech specified
if tech_name != '':
client.avatar.buy_new_tech(tech_name) # buy the tech specified

def __is_on_home_base(self, client: Player, world: GameBoard):
avatar_pos: Vector = client.avatar.position # get the position of the avatar
Expand Down
3 changes: 2 additions & 1 deletion game/controllers/interact_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from game.common.stations.station import Station
from game.common.map.game_board import GameBoard
from game.utils.vector import Vector
from game.quarry_rush.station.ore_occupiable_station import OreOccupiableStation


class InteractController(Controller):
Expand Down Expand Up @@ -64,5 +65,5 @@ def handle_actions(self, action: ActionType, client: Player, world: GameBoard, t

stat: Station | None = world.game_map[vector.y][vector.x].get_occupied_by(target)

if stat is not None and isinstance(stat, Station):
if stat is not None and isinstance(stat, Station) and not isinstance(stat, OreOccupiableStation):
stat.take_action(client.avatar, world.inventory_manager)
22 changes: 21 additions & 1 deletion game/controllers/master_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from game.controllers.movement_controller import MovementController
from game.controllers.controller import Controller
from game.controllers.interact_controller import InteractController
from game.controllers.mine_controller import MineController
from game.controllers.buy_tech_controller import BuyTechController
from game.controllers.place_controller import PlaceController
from game.common.map.game_board import GameBoard
from game.config import MAX_NUMBER_OF_ACTIONS_PER_TURN
from game.utils.vector import Vector
Expand Down Expand Up @@ -61,6 +64,9 @@ def __init__(self):
self.interact_controller: InteractController = InteractController()
self.dynamite_controller: DynamiteController = DynamiteController()
self.defuse_controller: DefuseController = DefuseController()
self.mine_controller: MineController = MineController()
self.buy_tech_controller: BuyTechController = BuyTechController()
self.place_controller: PlaceController = PlaceController()

# Receives all clients for the purpose of giving them the objects they will control
def give_clients_objects(self, clients: list[Player], world: dict):
Expand Down Expand Up @@ -119,17 +125,31 @@ def turn_logic(self, clients: list[Player], turn):
# during turn logic; handling controller logic
for client in clients:
client.avatar.state = 'idle' # set the state to idle to aid the visualizer
if len(client.actions) == 0:
continue
first = client.actions[0]
if first in [ActionType.MOVE_LEFT, ActionType.MOVE_RIGHT, ActionType.MOVE_UP, ActionType.MOVE_DOWN]:
client.actions = [action for action in client.actions if action in [ActionType.MOVE_LEFT, ActionType.MOVE_RIGHT, ActionType.MOVE_UP, ActionType.MOVE_DOWN]][:client.avatar.movement_speed]
else:
client.actions = [client.actions[0]]
client.actions.append(ActionType.INTERACT_CENTER)
for i in range(MAX_NUMBER_OF_ACTIONS_PER_TURN):
try:
self.movement_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
self.interact_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
self.mine_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
self.defuse_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
self.buy_tech_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
self.place_controller.handle_actions(client.actions[i], client, self.current_world_data[
'game_board'])
except IndexError:
pass

avatars: dict[Company, Avatar] = {client.avatar.company: client.avatar for client in clients}
self.current_world_data['game_board'].trap_detonation_control(avatars)

Expand Down
9 changes: 5 additions & 4 deletions game/controllers/mine_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ def handle_actions(self, action: ActionType, client: Player, world: GameBoard) -
"""

# don't mine anything if the inventory is full
if len(world.inventory_manager.get_inventory(client.avatar.company)) == 50:
if len(list(filter(lambda item: item is not None, world.inventory_manager.get_inventory(client.avatar.company)))) == 50:
return

match action:
case ActionType.MINE:
client.avatar.state = 'mining'
InteractController().handle_actions(ActionType.INTERACT_CENTER, client, world)
tile: Tile = world.game_map[client.avatar.position.y][client.avatar.position.x]
station: OreOccupiableStation = tile.occupied_by # Will return the OreOccupiableStation if it isn't

if station is None:
if station is None or station.object_type != ObjectType.ORE_OCCUPIABLE_STATION:
return

# remove the OreOccupiableStation from the game board
station.take_action(client.avatar, world.inventory_manager)

# try to remove the OreOccupiableStation from the game board
station.remove_from_game_board(tile)
case _: # default case
return
7 changes: 6 additions & 1 deletion game/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,12 @@ def tick(self):
thr.join(time_remaining)

# Go through each thread and check if they are still alive

client: Player
thr: Thread
for client, thr in zip(self.clients, threads):
# Load actions into player
client.actions = thr.result if thr.result is not None else []
# If thread is no longer alive, mark it as non-functional, preventing it from receiving future turns
if thr.is_alive():
client.functional = False
Expand Down Expand Up @@ -287,7 +292,7 @@ def shutdown(self, source=None):
if source:
output = "\n"
for client in self.clients:
if client.error != None:
if client.error is not None:
output += client.error + "\n"
print(f'\nGame has ended due to {source}: [{output}].')

Expand Down
Loading

0 comments on commit 0172abe

Please sign in to comment.