diff --git a/README.md b/README.md index 55435a5..85714c9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ _Combat Turtles_ is meant as a learning tool for intermediate-level [Python](htt The player can create their own turtle AIs by extending the `TurtleParent` class and overwriting a few key methods. The game is run using discrete step events (at a rate of approximately 30 steps/second), with each turtle defining its actions on a per-step basis. Custom AI submodules (in the form of standalone `.py` files) can be dropped into the `ai/` directory to import the player's AI into the game. Several example and template subclasses are included in this directory to get the player started. See also the [documentation below](#instructions) for a detailed guide to writing custom AIs. Python students might enjoy competing against each other to see whom can come up with the best AI, while Python instructors might consider running a class tournament to encourage students to learn more about object-oriented programming. -**This is a work in progress.** I am still in the process of adding features and fixing bugs, but if you are interested in playing with the latest public beta, please see the [releases](https://github.com/adam-rumpf/combat-turtles/releases) page. +The latest release can be found on this project's [itch.io](https://adam-rumpf.itch.io/combat-turtles) page or on the [releases](https://github.com/adam-rumpf/combat-turtles/releases) page. ## Game Overview diff --git a/ai/wall.py b/ai/wall.py new file mode 100644 index 0000000..e38cc9d --- /dev/null +++ b/ai/wall.py @@ -0,0 +1,128 @@ +# Built-In Example AI + +# Title: WallTurtle +# Author: Adam Rumpf +# Version: 1.0.0 +# Date: 1/5/2021 + +import math +import game.tcturtle + +class CombatTurtle(game.tcturtle.TurtleParent): + """Wall-hugging combat turtle. + + A turtle that attempts to navigate around obstacles by "feeling" the walls + around it. + + When it has direct line of sight to the opponent, it moves directly + towards it. Otherwise it moves towards the opponent until hitting a wall, + at which point it attempts to turn so that the way ahead is free. + """ + + #------------------------------------------------------------------------- + + def class_name(): + """CombatTurtle.class_name() -> str + Static method to return the name of the Combat Turtle AI. + """ + + return "WallTurtle" + + #------------------------------------------------------------------------- + + def class_desc(): + """CombatTurtle.class_desc() -> str + Static method to return a description of the Combat Turtle AI. + """ + + return "Hugs walls to get around obstacles." + + #------------------------------------------------------------------------- + + def class_shape(): + """CombatTurtle.class_shape() -> (int or tuple) + Static method to define the Combat Turtle's shape image. + + The return value can be either an integer or a tuple of tuples. + + Returning an integer index selects one of the following preset shapes: + 0 -- arrowhead (also default in case of unrecognized index) + 1 -- turtle + 2 -- plow + 3 -- triangle + 4 -- kite + 5 -- pentagon + 6 -- hexagon + 7 -- star + + A custom shape can be defined by returning a tuple of the form + (radius, angle), where radius is a tuple of radii and angle is a tuple + of angles (in radians) describing the polar coordinates of a polygon's + vertices. The shape coordinates should be given for a turtle facing + east. + """ + + return 2 + + #========================================================================= + + def setup(self): + """CombatTurtle.setup() -> None + Initialization code for Combat Turtle. + """ + + # Define the relative polar coordinates around the turtle to scan + self.nose_rel = (8, 0.0) # just ahead of turtle's front + self.hand_rel = (8, math.pi/2) # to left of turtle + + #------------------------------------------------------------------------- + + def step(self): + """CombatTurtle.setup() -> None + Step event code for Combat Turtle. + """ + + # Determine behavior based on whether there is line of sight + if (self.line_of_sight() == True): + # If there is line of sight, move directly towards opponent + + # Turn towards opponent + self.turn_towards() + + # Move towards opponent (or away if too close) + if self.distance() > 4*self.missile_radius: + self.forward() + else: + self.backward() + + # Shoot if facing opponent + if (self.can_shoot == True and + abs(self.relative_heading_towards()) <= 10): + self.shoot() + + else: + # If no line of sight, attempt to navigate around obstacles + + # Calculate Cartesian coordinates of nose and hand + nose = ((self.x + self.nose_rel[0]* + math.cos(math.radians(self.heading)+self.nose_rel[1])), + (self.y - self.nose_rel[0]* + math.sin(math.radians(self.heading)+self.nose_rel[1]))) + hand = ((self.x + self.hand_rel[0]* + math.cos(math.radians(self.heading)+self.hand_rel[1])), + (self.y - self.hand_rel[0]* + math.sin(math.radians(self.heading)+self.hand_rel[1]))) + + # Determine behavior based on whether nose and hand are clear + if self.free_space(nose) == True: + # Move forward when clear ahead + self.forward() + else: + if self.free_space(hand) == True: + # If free to left, turn left + self.left() + self.forward() + else: + # If blocked ahead and to left, turn right + self.right() + self.forward() diff --git a/game/obj/arena.py b/game/obj/arena.py index 6b09190..3138fd2 100644 --- a/game/obj/arena.py +++ b/game/obj/arena.py @@ -1,6 +1,7 @@ """Defines the arena container class.""" import math +import random from .block import Block class Arena: @@ -16,6 +17,7 @@ class Arena: 2 -- four columns near corners 3 -- wall with a central passage 4 -- plus sign + 5 -- randomized """ #------------------------------------------------------------------------- @@ -26,7 +28,7 @@ def get_names(): """ return ["Empty Arena", "Central Column Arena", "Corner Column Arena", - "Doorway Arena", "Plus-Shaped Arena"] + "Doorway Arena", "Plus-Shaped Arena", "Randomized Arena"] #------------------------------------------------------------------------- @@ -117,6 +119,9 @@ def __init__(self, game, size=(800, 800), layout=0): elif layout == 4: # Plus sign self._plus_blocks() + elif layout == 5: + # Randomized + self._random_blocks() #------------------------------------------------------------------------- @@ -186,6 +191,62 @@ def _plus_blocks(self): self._blocks.append(Block(self.game, math.floor(self.size[0]/3), math.ceil(2*self.size[0]/3), (self.size[1]/2)-30, (self.size[1]/2)+30)) + + #------------------------------------------------------------------------- + + def _random_blocks(self): + """Arena._random_blocks() -> None + Generates a randomized arena. + + The randomized layout is made up of 3-7 blocks, arranged to have + 2-fold rotational symmetry about the center, and prohibited from + intersecting the turtles' starting coordinates. + """ + + # Initialize a random block height and width + h = random.randrange(10, 151) + w = random.randrange(10, 151) + + # Decide whether to include a central block + if random.random() < 0.5: + + # Add central block + self._blocks.append(Block(self.game, (self.size[0]/2)-w, + (self.size[0]/2)+w, (self.size[1]/2)-h, + (self.size[1]/2)+h)) + + # Determine number of additional blocks on sides + num = random.randrange(1, 4) + + # Generate side blocks + iter = 0 # iteration counter + while iter < num: + + # Generate random dimensions and centers + h = random.randrange(10, 121) + w = random.randrange(10, 121) + cx = random.randrange(self.size[0]+1) + cy = random.randrange(self.size[1]+1) + + # Generate tentative blocks + self._blocks.append(Block(self.game, cx-w, cx+w, cy-h, cy+h)) + self._blocks.append(Block(self.game, self.size[0]-cx-w, + self.size[0]-cx+w, self.size[1]-cy-h, + self.size[1]-cy+h)) + + # Test whether the starting coordinates are free + if (self.blocked(self.get_p1_coords()) or + self.blocked(self.get_p2_coords())): + + # If not, delete the tentative blocks and retry + del self._blocks[-1] + del self._blocks[-1] + continue + + else: + + # Otherwise increment the counter + iter += 1 #------------------------------------------------------------------------- diff --git a/game/obj/block.py b/game/obj/block.py index 398204a..44f6c25 100644 --- a/game/obj/block.py +++ b/game/obj/block.py @@ -28,7 +28,7 @@ def __init__(self, game, left, right, bottom, top, col="black"): Requires the following positional arguments: game (tcgame.TurtleCombatGame) -- game driver object left (int) -- smallest x-coordinate (px) - right (int) -- lartst x-coordinate (px) + right (int) -- largest x-coordinate (px) bottom (int) -- smallest y-coordinate (px) top (int) -- largest y-coordinate (px)