From 343eb23cff217ddb8d717962f65c67c8e6f90ac5 Mon Sep 17 00:00:00 2001 From: arumpf Date: Thu, 12 Nov 2020 23:29:22 -0500 Subject: [PATCH 1/7] Beginning work on grid turtle. --- ai/grid.py | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 ai/grid.py diff --git a/ai/grid.py b/ai/grid.py new file mode 100644 index 0000000..cbc0c6a --- /dev/null +++ b/ai/grid.py @@ -0,0 +1,169 @@ +# Built-In Example AI + +# Title: GridTurtle +# Author: Adam Rumpf +# Version: 1.0.0 +# Date: 11/12/2020 + +import game.tcturtle + +class CombatTurtle(game.tcturtle.TurtleParent): + """Grid combat turtle. + + A turtle capable of navigating around obstacles by converting the arena + into a grid graph and finding shortest paths between nodes. Directly + pursues the opponent when there is line of sight. + + This submodule defines its own lightweight Graph class for use in + representing the arena and determining shortest paths. + + For general information about mathematical graphs, see: + https://mathworld.wolfram.com/Graph.html + """ + + #------------------------------------------------------------------------- + + def class_name(): + """CombatTurtle.class_name() -> str + Static method to return the name of the Combat Turtle AI. + """ + + return "GridTurtle" + + #------------------------------------------------------------------------- + + def class_desc(): + """CombatTurtle.class_desc() -> str + Static method to return a description of the Combat Turtle AI. + """ + + return "Navigates around obstacles and pursues when close." + + #------------------------------------------------------------------------- + + 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. + """ + + # Initialize a graph representation of the arena + self.ArenaGraph = Graph() + + #------------------------------------------------------------------------- + + def step(self): + """CombatTurtle.setup() -> None + Step event code for Combat Turtle. + """ + + pass + +############################################################################## + +class Graph(): + """A rudimentary graph class. + + Defines a grid graph representation of the arena. A graph is made up of + vertices which represent evenly-spaced free coordinates within the arena. + Edges connect vertices which have a clear path between them. + + This class used a Vertex class (see below) as a container to store + vertex-level attributes and adjacency lists. Edges are not stored + explicitly, and are instead inferred from the vertex adjacency lists. + """ + + #------------------------------------------------------------------------- + + def __init__(self, rows=20, columns=20): + """Graph([rows], [columns]) -> Graph + Graph object constructor. + + Accepts the following optional keyword arguments: + rows (int) [20] -- number of rows in the grid graph + columns (int) [20] -- number of columns in the grid graph + + Initializes the vertex set of a grid graph with a specified number of + rows and columns. The rows and columns are chosen to correspond to + arena coordinates spaces as evenly as possible to cover the entire + arena in a rectangular grid. + + A vertex is defined for each grid intersection that does not collide + with a block, and an edge is defined between adjacent grid + intersections (both rectilinearly and diagonally) as long as no + obstacles fall between them. + """ + + # Initialize vertex list as an empty dictionary + self.v = dict() + + # Scan arena and create vertices at the clear coordinates + for i in range(columns): + for j in range(rows): + pass + + #------------------------------------------------------------------------ + + def __del__(self): + """Graph.__del__() -> None + Graph destructor deletes all vertex objects. + """ + + self.v.clear() + + #------------------------------------------------------------------------ + + def nearest_vertex(self, coord): + """Graph.nearest_vertex(coord) -> int + Returns the integer ID of the vertex nearest a given coordinate. + """ + + pass + +############################################################################## + +class Vertex: + """Represents a graph vertex. + + A container class which stores the arena coordinates that this vertex + represents as well as a set of the vertices adjacent to this vertex. + """ + + #------------------------------------------------------------------------- + + def __init__(self, coord): + """Vertex(coord) -> Vertex + Vertex object constructor. + + Requires the following positional arguments: + coord (tuple) -- arena coordinate that this vertex represents + """ + + # Initialize coordinate attribute + self.coord = coord From 33e163bb2cadb883a7ffc01ee79295b92d1f9d86 Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Thu, 26 Nov 2020 15:25:50 -0500 Subject: [PATCH 2/7] Adding a placeholder for a wall-hugging AI. --- ai/grid.py | 4 +-- ai/wall.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 ai/wall.py diff --git a/ai/grid.py b/ai/grid.py index cbc0c6a..9a2e8ed 100644 --- a/ai/grid.py +++ b/ai/grid.py @@ -3,7 +3,7 @@ # Title: GridTurtle # Author: Adam Rumpf # Version: 1.0.0 -# Date: 11/12/2020 +# Date: 11/26/2020 import game.tcturtle @@ -64,7 +64,7 @@ def class_shape(): east. """ - return 2 + return 4 #========================================================================= diff --git a/ai/wall.py b/ai/wall.py new file mode 100644 index 0000000..fdbe0f8 --- /dev/null +++ b/ai/wall.py @@ -0,0 +1,83 @@ +# Built-In Example AI + +# Title: WallTurtle +# Author: Adam Rumpf +# Version: 1.0.0 +# Date: 11/26/2020 + +import game.tcturtle + +class CombatTurtle(game.tcturtle.TurtleParent): + """Wall-hugging combat turtle. + + A turtle that attempts to navigate around obstacles by hugging walls using + a left-hand rule. + + 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 wraps around the wall by keeping its left side against + it. + """ + + #------------------------------------------------------------------------- + + 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. + """ + + pass + + #------------------------------------------------------------------------- + + def step(self): + """CombatTurtle.setup() -> None + Step event code for Combat Turtle. + """ + + pass From ed993e6c50d6671783a3ed16414dee7c274db1de Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Fri, 1 Jan 2021 20:59:36 -0500 Subject: [PATCH 3/7] Updating license year. --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 13945d3..e1f580e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Adam Rumpf +Copyright (c) 2021 Adam Rumpf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7520ce9..ae7b786 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ In an effort to maintain portability it uses only modules from the [Python Stand ## Credits Combat Turtles -Copyright (c) 2020 Adam Rumpf +Copyright (c) 2021 Adam Rumpf [adam-rumpf.github.io](https://adam-rumpf.github.io/) Source code released under MIT License From 9f74f9a6c313710f6f273a1a91550912975313a1 Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Sun, 3 Jan 2021 19:53:33 -0500 Subject: [PATCH 4/7] Reorganizing priorities for initial release. --- ai/grid.py | 169 ----------------------------------------------------- ai/wall.py | 8 +++ 2 files changed, 8 insertions(+), 169 deletions(-) delete mode 100644 ai/grid.py diff --git a/ai/grid.py b/ai/grid.py deleted file mode 100644 index 9a2e8ed..0000000 --- a/ai/grid.py +++ /dev/null @@ -1,169 +0,0 @@ -# Built-In Example AI - -# Title: GridTurtle -# Author: Adam Rumpf -# Version: 1.0.0 -# Date: 11/26/2020 - -import game.tcturtle - -class CombatTurtle(game.tcturtle.TurtleParent): - """Grid combat turtle. - - A turtle capable of navigating around obstacles by converting the arena - into a grid graph and finding shortest paths between nodes. Directly - pursues the opponent when there is line of sight. - - This submodule defines its own lightweight Graph class for use in - representing the arena and determining shortest paths. - - For general information about mathematical graphs, see: - https://mathworld.wolfram.com/Graph.html - """ - - #------------------------------------------------------------------------- - - def class_name(): - """CombatTurtle.class_name() -> str - Static method to return the name of the Combat Turtle AI. - """ - - return "GridTurtle" - - #------------------------------------------------------------------------- - - def class_desc(): - """CombatTurtle.class_desc() -> str - Static method to return a description of the Combat Turtle AI. - """ - - return "Navigates around obstacles and pursues when close." - - #------------------------------------------------------------------------- - - 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 4 - - #========================================================================= - - def setup(self): - """CombatTurtle.setup() -> None - Initialization code for Combat Turtle. - """ - - # Initialize a graph representation of the arena - self.ArenaGraph = Graph() - - #------------------------------------------------------------------------- - - def step(self): - """CombatTurtle.setup() -> None - Step event code for Combat Turtle. - """ - - pass - -############################################################################## - -class Graph(): - """A rudimentary graph class. - - Defines a grid graph representation of the arena. A graph is made up of - vertices which represent evenly-spaced free coordinates within the arena. - Edges connect vertices which have a clear path between them. - - This class used a Vertex class (see below) as a container to store - vertex-level attributes and adjacency lists. Edges are not stored - explicitly, and are instead inferred from the vertex adjacency lists. - """ - - #------------------------------------------------------------------------- - - def __init__(self, rows=20, columns=20): - """Graph([rows], [columns]) -> Graph - Graph object constructor. - - Accepts the following optional keyword arguments: - rows (int) [20] -- number of rows in the grid graph - columns (int) [20] -- number of columns in the grid graph - - Initializes the vertex set of a grid graph with a specified number of - rows and columns. The rows and columns are chosen to correspond to - arena coordinates spaces as evenly as possible to cover the entire - arena in a rectangular grid. - - A vertex is defined for each grid intersection that does not collide - with a block, and an edge is defined between adjacent grid - intersections (both rectilinearly and diagonally) as long as no - obstacles fall between them. - """ - - # Initialize vertex list as an empty dictionary - self.v = dict() - - # Scan arena and create vertices at the clear coordinates - for i in range(columns): - for j in range(rows): - pass - - #------------------------------------------------------------------------ - - def __del__(self): - """Graph.__del__() -> None - Graph destructor deletes all vertex objects. - """ - - self.v.clear() - - #------------------------------------------------------------------------ - - def nearest_vertex(self, coord): - """Graph.nearest_vertex(coord) -> int - Returns the integer ID of the vertex nearest a given coordinate. - """ - - pass - -############################################################################## - -class Vertex: - """Represents a graph vertex. - - A container class which stores the arena coordinates that this vertex - represents as well as a set of the vertices adjacent to this vertex. - """ - - #------------------------------------------------------------------------- - - def __init__(self, coord): - """Vertex(coord) -> Vertex - Vertex object constructor. - - Requires the following positional arguments: - coord (tuple) -- arena coordinate that this vertex represents - """ - - # Initialize coordinate attribute - self.coord = coord diff --git a/ai/wall.py b/ai/wall.py index fdbe0f8..5f08dec 100644 --- a/ai/wall.py +++ b/ai/wall.py @@ -79,5 +79,13 @@ def step(self): """CombatTurtle.setup() -> None Step event code for Combat Turtle. """ + + # Outline: + # Turn and move towards the opponent as long as there's line of sight. + # Otherwise, attempt to move towards opponent while scanning ahead of self. + # If there's an obstacle, begin wall hugging behavior, which is based on scanning a point to left of self. + # As long as that point is free, turn right while moving forward until it becomes blocked for the first time. + # As long as that point is blocked, move forward. + # After sticking to a wall, turn left whenever the point to the turtle's left is unblocked. pass From de8614b5c8d6879bab4177c08b7ddc6d8c4e7ea1 Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Tue, 5 Jan 2021 21:11:26 -0500 Subject: [PATCH 5/7] Updating documentation for final release. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae7b786..3a11720 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,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 added to 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 From 36d5919b430ebc0bcd1ba2bcbe741e73a1e851a2 Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Tue, 5 Jan 2021 22:35:53 -0500 Subject: [PATCH 6/7] Adding a random arena layout. --- game/obj/arena.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++- game/obj/block.py | 2 +- 2 files changed, 63 insertions(+), 2 deletions(-) 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) From c1fa239b9836caa3ab59763c10f516d5b0e84fa3 Mon Sep 17 00:00:00 2001 From: adam-rumpf Date: Tue, 5 Jan 2021 23:08:40 -0500 Subject: [PATCH 7/7] Implementing a wall-hugging AI. --- ai/wall.py | 67 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/ai/wall.py b/ai/wall.py index 5f08dec..e38cc9d 100644 --- a/ai/wall.py +++ b/ai/wall.py @@ -3,20 +3,20 @@ # Title: WallTurtle # Author: Adam Rumpf # Version: 1.0.0 -# Date: 11/26/2020 +# 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 hugging walls using - a left-hand rule. + 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 wraps around the wall by keeping its left side against - it. + at which point it attempts to turn so that the way ahead is free. """ #------------------------------------------------------------------------- @@ -71,7 +71,9 @@ def setup(self): Initialization code for Combat Turtle. """ - pass + # 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 #------------------------------------------------------------------------- @@ -80,12 +82,47 @@ def step(self): Step event code for Combat Turtle. """ - # Outline: - # Turn and move towards the opponent as long as there's line of sight. - # Otherwise, attempt to move towards opponent while scanning ahead of self. - # If there's an obstacle, begin wall hugging behavior, which is based on scanning a point to left of self. - # As long as that point is free, turn right while moving forward until it becomes blocked for the first time. - # As long as that point is blocked, move forward. - # After sticking to a wall, turn left whenever the point to the turtle's left is unblocked. - - pass + # 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()