====== cs20ps23as05: Circle World Game Actor ====== ===== Goals ===== * Practice with object-oriented design in Python, implementing a class that models the state of an //actor// in a simple 2D game, with a variety of special methods and other methods. * Get a preview of one style of writing GUIs in Python, if you'd like. ----- ====== Prerequisites ====== This assignment requires familiarity with the [[:lecture materials/]] presented in class through [[:lecture materials/week 05]]. ----- ====== Background ====== ===== Game Engines and Actors ===== [[https://en.wikipedia.org/wiki/Game_engine|Quoth the arbiter of all human information]]: > A **game engine** is a software framework primarily designed for the development of video games, and generally includes relevant libraries and support programs. Many game engines are "component-based", i.e. the state of a game is made up of **//"actors"//** present somewhere in the world of the game, which are made up of //components//. For example, in the [[https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/|Unreal Engine]]: > Actors are instances of classes that derive from the ''AActor'' class; the base class of all gameplay objects that can be placed into the world. ... In general, ''Actor''s can be thought of as whole items or entities, while ''Object''s are more specialized parts. Actors often make use of ''Component''s, which are specialized ''Object''s, to define certain aspects of their functionality or hold values for a collection of properties. Take a car as an example. The car as a whole is an ''Actor'', whereas the parts of the car, like the wheels and doors, would all be ''Component''s of that ''Actor''. ===== Discrete-Event Simulations ===== Most simple video games are a form of [[https://en.wikipedia.org/wiki/Discrete-event_simulation|discrete-event simulation]] in which the state of the game's world and its actors are updated for every frame of animation. ----- ====== Assignment ====== {{:agar_io.png?nolink|Agar.io image from Google Play store}} You shall define a class named ''CircleActor'' in a module named ''circle_actor'' that models some of the basic properties and actions of an //actor// in a 2D game engine, specifically similar to a cell in the game [[https://agar.io|Agar.io]]. The state of a ''CircleActor'' could, for example, be updated for every frame of animation in the game, thus making a game like this a discrete-event simulation. The world in which ''CircleActor''s exist is a two-dimensional world with a Cartesian coordinate system, i.e. each actor exists at a particular x/y coordinate and has a certain radius, thereby covering a certain area of the world. Your class must be defined as follows in the specifications below. ===== CircleActor Class Specifications ===== Please see the following skeleton for the expected methods in class ''CircleActor''. Feel free to add other methods and import standard-library modules if you'd like. """ Module for the CircleActor class. """ __author__ = 'A student in CS 20P, someone@jeff.cis.cabrillo.edu' class CircleActor: """ Behaves as a circle in a 2D world centered on an X/Y coordinate. """ def __init__(self, name: str, radius: float, world_size: tuple[float, float], position: tuple[float, float], velocity: tuple[float, float]): """ Constructs a new CircleActor with a given name at a position and with an inherent velocity. :param name: the name of the actor, assumed to be non-empty :param radius: the initial radius of the actor :param world_size: the width and height of the world in which the actor resides :param position: the x/y coordinate of the center of the actor :param velocity: the amount in x/y dimensions by which the actor will move on each step """ # TODO def __bool__(self) -> bool: """ Returns True if this actor is still "alive", meaning its radius is small enough for the circle to fit within the world and no less than 1. """ # TODO def __contains__(self, other) -> bool: """ Returns True if another actor is "contained within" this one, i.e. whether the two actors overlap at all and this actor has a larger radius than the other. """ # TODO def __repr__(self) -> str: """ Returns a printable representation of this actor, appropriate for eval(). That is, the return value of this method should be a string that is valid code for re-constructing this actor with the same attributes. """ # TODO def __str__(self) -> str: """ Returns the name of this actor. """ # TODO def __sub__(self, other) -> float: """ Returns the distance between this actor and another, i.e. how far the two circles are from touching. This value will be negative if the two circles overlap. """ # TODO def collide(self, other): """ "Collides" this actor with another. If they overlap, the radius of the larger actor shall increase by 1 and that of the smaller will decrease by 1. """ # TODO def position(self, new_position: tuple[float, float] = None): """ Given no arguments, returns this actor's position. Given a tuple[float, float] as an argument, sets this actor's x/y position components. """ # TODO def radius(self, new_radius: float = None): """ Given no arguments, returns this actor's radius. Given a real number as an argument, sets this actor's radius. """ # TODO def step(self): """ Moves this actor in in the direction of its velocity by one unit of "time", i.e. one frame of animation or one discrete event. e.g. if position is (4, 5) and velocity is (-1, 1), the new position will be (3, 6). """ # TODO def velocity(self, new_velocity: tuple[float, float] = None): """ Given no arguments, returns this actor's velocity. Given a tuple[float, float] as an argument, sets this actor's x/y velocity components. """ # TODO ===== Coordinate System, World Boundaries, and Staying In Bounds ===== The coordinates that make up an actor's location are in an image-like coordinate system, where the origin (0, 0) is in the upper-left corner, the X dimension increases toward the right, and the Y dimension increases toward the bottom. The following image demonstrates how this would work in a 1920x1080 "world". I have also placed to circles in the image to demonstrate how you should think about the distance between two actors. The position of an actor represents the center of its circle. Finding the distance between those points is trivial. Finding the distance between the two circles simply involves subsequently incorporating the radii. {{:circle_world_coordinate_system.png?nolink|Image demonstrating the coordinate system of a 1920x1080 "world" in this assignment.}} Several of the methods in class ''CircleActor'' may result in placing an actor in a position where the actor's area extends beyond the boundaries of the world. If any part of an actor has traveled outside the world, ensure the following for each edge of the world crossed: - Modify the actor's position to "pull" it back into the world at the exact edge it had traveled over. - Reverse the horizontal or vertical component of the velocity so that the actor will "bounce" off of the edge instead of traveling over it. ===== GUI Driver/Tester and Video ===== I have also provided a GUI for this assignment that will create instances of your ''CircleActor'' class and watch them as they travel around the world and interact with other actors. Note that this GUI uses the ''[[https://docs.python.org/3/library/tk.html|tkinter]]'' library module, which offers an interface to the [[https://en.wikipedia.org/wiki/Tcl|Tcl/Tk]] toolkit. If you want to run this, you have two major options: - Run it on your own machine, assuming you have Python installed and it supports ''tkinter''. - Run it on the server, with [[https://en.wikipedia.org/wiki/X_Window_System#Remote_desktop|X11 forwarding]] enabled such that the GUI code will run on the server, but you can interact with the GUI from your own machine. - MobaXterm supports this automatically, or at least should. - Add the ''-Y'' flag when connecting using the ''ssh'' command: ''ssh -Y @USER@@jeff.cis.cabrillo.edu'' - On macOS, you will first need to install [[https://www.xquartz.org/|XQuartz]] in order for this to work. - On GNU/Linux, this should work fine without additional configuration. - This should probably work on Windows Terminal as well, if you have the appropriate Linux subsystem services installed. #!/usr/bin/env python """ Quick and dirty Circle World GUI using tkinter """ __author__ = 'Jeffrey Bergamini for CS 20P, jeffrey.bergamini@cabrillo.edu' import circle_actor import random import tkinter import time WINDOW_TITLE = 'CS 20P Circle World' WIDTH = 1280 HEIGHT = 720 FRAME_RATE = 60 STUDENTS = ('adcuthbertson', 'agbrunetto', 'ajgrushkin', 'altorresmoran', 'asmorales', 'atspencer', 'csantiago', 'djrobertson', 'ecweiler', 'fjmorales', 'gehult', 'ikhorvath', 'jebentley', 'jnchristofferso', 'kkonrad', 'lnkleijnen', 'mkowalski', 'mmozer', 'nekoehler', 'nepedrotti', 'rrojasresendiz', 'wsschaffer', 'ymoralesgalvan', 'ysmohamed') DEFAULT_RADIUS = min(WIDTH, HEIGHT) // len(STUDENTS) // 2 MAX_DEFAULT_VELOCITY_COMPONENT = 2 def create_actors(canvas): actors = [] for name in STUDENTS: radius = random.randint(DEFAULT_RADIUS // 2, DEFAULT_RADIUS * 2) actor = circle_actor.CircleActor( name, radius=radius, world_size=(WIDTH, HEIGHT), position=(random.randint(DEFAULT_RADIUS, WIDTH - DEFAULT_RADIUS), random.randint(DEFAULT_RADIUS, HEIGHT - DEFAULT_RADIUS)), velocity=(random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT, random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT)) actors.append(actor) posx, posy = actor.position() color_int = int.from_bytes(bytes(0xff - random.randint(0, 127) for _ in range(3)), byteorder='big') actor.oval = canvas.create_oval(posx - radius, posy - radius, posx + radius - 1, posy + radius - 1, fill=f'#{color_int:06x}') actor.label = canvas.create_text(posx, posy, text=str(actor), fill='black', font='Helvetica 12') return actors def animate(window, canvas, actors): while True: # Revive the dead for actor in actors: if not actor: actor.radius(random.randint(DEFAULT_RADIUS // 2, DEFAULT_RADIUS * 2)) actor.position((random.randint(DEFAULT_RADIUS, WIDTH - DEFAULT_RADIUS), random.randint(DEFAULT_RADIUS, HEIGHT - DEFAULT_RADIUS))) actor.velocity( (random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT, random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT)) # All actors move one step for actor in actors: actor.step() # They may collide with each other and respond for actor in actors: for other in actors: if actor is not other: actor.collide(other) # Sort the stacking order by size for actor in sorted(actors, key=circle_actor.CircleActor.radius, reverse=True): canvas.lift(actor.oval) canvas.lift(actor.label) # Display them for actor in actors: posx, posy = actor.position() radius = actor.radius() canvas.coords(actor.oval, round(posx - radius), round(posy - radius), round(posx + radius + 1), round(posy + radius + 1)) canvas.coords(actor.label, round(posx), round(posy)) window.update() time.sleep(1 / FRAME_RATE) if __name__ == '__main__': _window = tkinter.Tk() _window.title(WINDOW_TITLE) _window.geometry(f'{WIDTH}x{HEIGHT}') _canvas = tkinter.Canvas(_window) _canvas.configure(bg='white') _canvas.pack(fill='both', expand=True) _actors = create_actors(_canvas) animate(_window, _canvas, _actors) The following is a short video of the GUI running using my ''CircleActor'' implementation: ----- ====== Submission ====== Submit ''circle_actor.py'' via [[info:turnin]]. {{https://jeff.cis.cabrillo.edu/images/feedback-robot.png?nolink }} //**Feedback Robot**// This project has a feedback robot that will run some tests on your submission and provide you with a feedback report via email within roughly one minute. Please read the feedback carefully. ====== Due Date and Point Value ====== Due at 23:59:59 on the date listed on the [[:syllabus|syllabus]]. ''Assignment 05'' is worth 60 points. Possible point values per category: --------------------------------------- Properly implemented methods 60 (split as evenly as possible) Possible deductions: Style and practices 10–20% Possible extra credit: Submission via Git 5% ---------------------------------------