diff --git a/ellens-alien-game/.exercism/config.json b/ellens-alien-game/.exercism/config.json
new file mode 100644
index 0000000..111ebdc
--- /dev/null
+++ b/ellens-alien-game/.exercism/config.json
@@ -0,0 +1,24 @@
+{
+ "authors": [
+ "PaulT89",
+ "BethanyG"
+ ],
+ "contributors": [
+ "DjangoFett",
+ "kotp",
+ "IsaacG"
+ ],
+ "files": {
+ "solution": [
+ "classes.py"
+ ],
+ "test": [
+ "classes_test.py"
+ ],
+ "exemplar": [
+ ".meta/exemplar.py"
+ ]
+ },
+ "icon": "character-study",
+ "blurb": "Learn about classes by creating an Alien for Ellen's game."
+}
diff --git a/ellens-alien-game/.exercism/metadata.json b/ellens-alien-game/.exercism/metadata.json
new file mode 100644
index 0000000..76b8161
--- /dev/null
+++ b/ellens-alien-game/.exercism/metadata.json
@@ -0,0 +1 @@
+{"track":"python","exercise":"ellens-alien-game","id":"450bd5ee25d14958825d8c7a2ea06a99","url":"https://exercism.org/tracks/python/exercises/ellens-alien-game","handle":"myFirstCode","is_requester":true,"auto_approve":false}
\ No newline at end of file
diff --git a/ellens-alien-game/HELP.md b/ellens-alien-game/HELP.md
new file mode 100644
index 0000000..050813b
--- /dev/null
+++ b/ellens-alien-game/HELP.md
@@ -0,0 +1,130 @@
+# Help
+
+## Running the tests
+
+We use [pytest][pytest: Getting Started Guide] as our website test runner.
+You will need to install `pytest` on your development machine if you want to run tests for the Python track locally.
+You should also install the following `pytest` plugins:
+
+- [pytest-cache][pytest-cache]
+- [pytest-subtests][pytest-subtests]
+
+Extended information can be found in our website [Python testing guide][Python track tests page].
+
+
+### Running Tests
+
+To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_).
+Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded.
+
+Linux/MacOS
+```bash
+$ cd {path/to/exercise-folder-location}
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> cd {path\to\exercise-folder-location}
+```
+
+
+
+Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file:
+
+Linux/MacOS
+```bash
+$ python3 -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+
+### Common options
+- `-o` : override default `pytest.ini` (_you can use this to avoid marker warnings_)
+- `-v` : enable verbose output.
+- `-x` : stop running tests on first failure.
+- `--ff` : run failures from previous test before running other test cases.
+
+For additional options, use `python3 -m pytest -h` or `py -m pytest -h`.
+
+
+### Fixing warnings
+
+If you do not use `pytest -o markers=task` when invoking `pytest`, you might receive a `PytestUnknownMarkWarning` for tests that use our new syntax:
+
+```bash
+PytestUnknownMarkWarning: Unknown pytest.mark.task - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
+```
+
+To avoid typing `pytest -o markers=task` for every test you run, you can use a `pytest.ini` configuration file.
+We have made one that can be downloaded from the top level of the Python track directory: [pytest.ini][pytest.ini].
+
+You can also create your own `pytest.ini` file with the following content:
+
+```ini
+[pytest]
+markers =
+ task: A concept exercise task.
+```
+
+Placing the `pytest.ini` file in the _root_ or _working_ directory for your Python track exercises will register the marks and stop the warnings.
+More information on pytest marks can be found in the `pytest` documentation on [marking test functions][pytest: marking test functions with attributes] and the `pytest` documentation on [working with custom markers][pytest: working with custom markers].
+
+Information on customizing pytest configurations can be found in the `pytest` documentation on [configuration file formats][pytest: configuration file formats].
+
+
+### Extending your IDE or Code Editor
+
+Many IDEs and code editors have built-in support for using `pytest` and other code quality tools.
+Some community-sourced options can be found on our [Python track tools page][Python track tools page].
+
+[Pytest: Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html
+[Python track tools page]: https://exercism.org/docs/tracks/python/tools
+[Python track tests page]: https://exercism.org/docs/tracks/python/tests
+[pytest-cache]:http://pythonhosted.org/pytest-cache/
+[pytest-subtests]:https://github.com/pytest-dev/pytest-subtests
+[pytest.ini]: https://github.com/exercism/python/blob/main/pytest.ini
+[pytest: configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats
+[pytest: marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks
+[pytest: working with custom markers]: https://docs.pytest.org/en/6.2.x/example/markers.html#working-with-custom-markers
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit classes.py` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [Python track's documentation](https://exercism.org/docs/tracks/python)
+- The [Python track's programming category on the forum](https://forum.exercism.org/c/programming/python)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+Below are some resources for getting help if you run into trouble:
+
+- [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources.
+- [The Exercism Community on Discord](https://exercism.org/r/discord)
+- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community.
+- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners.
+- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done.
+- [Python Community Forums](https://discuss.python.org/)
+- [Free Code Camp Community Forums](https://forum.freecodecamp.org/)
+- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help)
+- [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually.
+
+Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already.
+ If not - you can always [ask](https://stackoverflow.com/help/how-to-ask) or [answer](https://stackoverflow.com/help/how-to-answer) someone else's question.
\ No newline at end of file
diff --git a/ellens-alien-game/HINTS.md b/ellens-alien-game/HINTS.md
new file mode 100644
index 0000000..cede607
--- /dev/null
+++ b/ellens-alien-game/HINTS.md
@@ -0,0 +1,42 @@
+# Hints
+
+## 1. Create the Alien Class
+
+- Remember that `object methods` are always passed `self` as the first parameter.
+- Remember the double underscores on _both_ sides of `__init__()`.
+- Instance variables are unique to the `class` instance (_object_) that possesses them.
+- Class variables are the same across all instances of a `class`.
+
+## 2. The `hit` Method
+
+- Remember that `object methods` are always passed `self` as the first parameter.
+- You can choose to allow the Alien's health to fall below zero, or require that it does not.
+
+## 3. The `is_alive` Method
+
+- Remember that `object methods` are always passed `self` as the first parameter.
+- 0 may not be the only 'dead' condition, depending on how `hit()` is implemented.
+
+## 4. The `teleport` Method
+
+- Remember that `object methods` are always passed `self` as the first parameter.
+- Instance attributes can be updated from a method by using `self.` = ``.
+
+## 5. The `collision_detection` Method
+
+- Remember that `object methods` are always passed `self` as the first parameter.
+- This method seems like an excellent place to use some kind of placeholder…
+
+## 6. Alien Counter
+
+- Class attributes are the same across all instances of a `class`.
+- Ideally, this counter would increment whenever someone _made an new Alien_.
+- Class attributes can be changed from an instance method by using the _class name_: `Alien.`.
+- `__init__()` is considered an instance method since it _initializes a new object_.
+
+## 7. Object Creation
+
+- A `tuple` would be a _single_ parameter.
+- The Alien constructor takes _2 parameters_.
+- Unpacking what is _inside_ the tuple would yield two parameters.
+- The standalone function is outside of the `class`
\ No newline at end of file
diff --git a/ellens-alien-game/README.md b/ellens-alien-game/README.md
new file mode 100644
index 0000000..1464768
--- /dev/null
+++ b/ellens-alien-game/README.md
@@ -0,0 +1,415 @@
+# Ellen's Alien Game
+
+Welcome to Ellen's Alien Game on Exercism's Python Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
+
+## Introduction
+
+## Object Oriented Programming in Python
+
+If you have been programming in a [functional][functional], [declarative][declarative], or [imperative][imperative] style, shifting focus to [object oriented programming][oop] (OOP) may feel a bit foreign.
+An OOP approach asks the programmer to think about modeling a problem as one or more `objects` that interact with one another, keep state, and act upon data.
+Objects can represent real world entities (_such as cars or cats_) - or more abstract concepts (_such as integers, vehicles, or mammals_).
+Each object becomes a unique instance in computer memory and represents some part of the overall model.
+
+## Classes
+
+`Classes` are the definitions of new object types, and from which new `instances` of objects are created.
+They often bundle data with code or functions that operate on that data.
+In this sense, classes are _blueprints_ or sets of instructions from which many objects of a similar type can be built and used.
+A complex program can have many classes, each building many different flavors of objects.
+The process of building an object from a class is known as `instantiation` (_or creating an instance of the class_).
+
+A class definition in Python is straightforward:
+
+```python
+class MyClass:
+ # Class body goes here
+```
+
+### Class Attributes
+
+Class fields (_otherwise known as `properties`, `attributes`, `data members`, or `variables`_) can be added to the body of the class:
+
+```python
+class MyClass:
+ number = 5
+ string = "Hello!"
+```
+
+An instance (_object_) of `MyClass` can be created and bound to a name by [_calling_][calling] the class (_in the same way a function is called_):
+
+```python
+>>> new_object = MyClass()
+
+# Class is instantiated and resulting object is bound to the "new_object" name.
+# Note: the object address 'at 0x15adc55b0' will vary by computer.
+>>> new_object
+<__main__.MyClass at 0x15adc55b0>
+```
+
+`Class attributes` are shared across all objects (_or instances_) created from a class, and can be accessed via [`dot notation`][dot notation] - a `.` placed after the object name and before the attribute name:
+
+```python
+>>> new_object = MyClass()
+
+# Accessing the class attribute "number" via dot-notation.
+>>> new_object.number
+5
+
+# Accessing the class attribute "string" via dot-notation.
+>>> new_object.string
+'Hello!'
+
+# Instantiating an additional object and binding it to the "second_new_object" name.
+>>> second_new_object = MyClass()
+
+>>> second_new_object
+# Note: the object address "at 0x15ad99970" will vary by computer.
+<__main__.MyClass at 0x15ad99970>
+
+# Second_new_object shares the same class attributes as new_object.
+>>> new_object.number == second_new_object.number
+True
+```
+
+Class attributes are defined in the body of the class itself, before any other methods.
+They are owned by the class - allowing them to be shared across instances of the class.
+Because these attributes are shared, their value can be accessed and manipulated from the class _directly_.
+Altering the value of class attributes alters the value _**for all objects instantiated from the class**_:
+
+```python
+>>> obj_one = MyClass()
+>>> obj_two = MyClass()
+
+# Accessing a class attribute from an object.
+>>> obj_two.number
+5
+
+# Accessing the class attribute from the class itself.
+>>> MyClass.number
+5
+
+# Modifying the value of the "number" class attribute.
+>>> MyClass.number = 27
+
+# Modifying the "number" class attribute changes the "number" attribute for all objects.
+>>> obj_one.number
+27
+
+>>> obj_two.number
+27
+```
+
+Having a bunch of objects with synchronized data at all times is not particularly useful.
+Fortunately, objects created from a class can be customized with their own `instance attributes` (_or instance properties, variables, or fields_).
+As their name suggests, instance attributes are unique to each object, and can be modified independently.
+
+
+## Customizing Object Instantiation with `__init__()`
+
+The special ["dunder method"][dunder] (_short for "double underscore method"_) `__init__()` is used to customize class instances, and can be used to initialize instance attributes or properties for objects.
+For its role in initializing instance attributes, `__init__()` is also referred to as a `class constructor` or `initializer`.
+`__init__()` takes one required parameter called `self`, which refers to the newly initialized or created object.
+Data for instance attributes or properties can then be passed as arguments of `__init__()`, following the `self` parameter.
+
+Below, `MyClass` now has instance attributes called `location_x` and `location_y`.
+As you can see, the two attributes have been assigned to the first and second indexes of the `location` (_a tuple_) argument that has been passed to `__init__()`.
+The `location_x` and `location_y` attributes for a class instance will now be initialized when you instantiate the class, and an object is created:
+
+```python
+class MyClass:
+ # These are class attributes, variables, or fields.
+ number = 5
+ string = "Hello!"
+
+ # This is the class "constructor", with a "location" parameter that is a tuple.
+ def __init__(self, location):
+
+ # This is an instance or object property, attribute, or variable.
+ # Note that we are unpacking the tuple argument into two separate instance variables.
+ self.location_x = location[0]
+ self.location_y = location[1]
+
+# Create a new object "new_object_one", with object property (1, 2).
+>>> new_object_one = MyClass((1, 2))
+
+# Create a second new object "new_object_two" with object property (-8, -9).
+>>> new_object_two = MyClass((-8, -9))
+
+# Note that new_object_one.location_x and new_object_two.location_x two different values.
+>>> new_object_one.location_x
+1
+
+>>> new_object_two.location_x
+-8
+```
+
+Note that you only need to pass one argument when initializing `MyClass` above -- Python takes care of passing `self` when the class is called.
+
+
+## Methods
+
+A `method` is a `function` that is bound to either the class itself (_known as a [class method][class method], which will be discussed in a later exercise_) or an _instance_ of the class (object).
+Methods that operate on an object (instance) need to be defined with `self` as the first parameter.
+You can then define the rest of the parameters as you would for a "normal" or non-bound function:
+
+```python
+class MyClass:
+ number = 5
+ string = "Hello!"
+
+ # Class constructor.
+ def __init__(self, location):
+ # Instance properties
+ self.location_x = location[0]
+ self.location_y = location[1]
+
+ # Instance method. Note "self" as first parameter.
+ def change_location(self, amount):
+ self.location_x += amount
+ self.location_y += amount
+ return self.location_x, self.location_y
+```
+
+Like attribute access, calling a method simply requires putting a `.` after the object name, and before the method name.
+The called method does not need a copy of the object as a first parameter -- Python fills in `self` automatically:
+
+```python
+class MyClass:
+ number = 5
+ string = "Hello!"
+
+ def __init__(self, location):
+ self.location_x = location[0]
+ self.location_y = location[1]
+
+ def change_location(self, amount):
+ self.location_x += amount
+ self.location_y += amount
+ return self.location_x, self.location_y
+
+# Make a new test_object with location (3,7)
+>>> test_object = MyClass((3,7))
+>>> (test_object.location_x, test_object.location_y)
+(3,7)
+
+# Call change_location to increase location_x and location_y by 7.
+>>> test_object.change_location(7)
+(10, 14)
+```
+
+Class attributes can be accessed from within instance methods in the same way that they are accessed outside of the class:
+
+```python
+class MyClass:
+ number = 5
+ string = "Hello!"
+
+ def __init__(self, location):
+ self.location_x = location[0]
+ self.location_y = location[1]
+
+ # Alter instance variable location_x and location_y
+ def change_location(self, amount):
+ self.location_x += amount
+ self.location_y += amount
+ return self.location_x, self.location_y
+
+ # Alter class variable number for all instances from within an instance.
+ def increment_number(self):
+ # Increment the 'number' class variable by 1.
+ MyClass.number += 1
+
+
+>>> test_object_one = MyClass((0,0))
+>>> test_object_one.number
+5
+
+>>> test_object_two = MyClass((13, -3))
+>>> test_object_two.increment_number()
+>>> test_object_one.number
+6
+```
+
+## Placeholding or Stubbing Implementation with Pass
+
+In previous concept exercises and practice exercise stubs, you will have seen the `pass` keyword used within the body of functions in place of actual code.
+The `pass` keyword is a syntactically valid placeholder - it prevents Python from throwing a syntax error for an empty function or class definition.
+Essentially, it is a way to say to the Python interpreter, 'Don't worry! I _will_ put code here eventually, I just haven't done it yet.'
+
+```python
+class MyClass:
+ number = 5
+ string = "Hello!"
+
+ def __init__(self, location):
+ self.location_x = location[0]
+ self.location_y = location[1]
+
+ # Alter instance variable location_x and location_y
+ def change_location(self, amount):
+ self.location_x += amount
+ self.location_y += amount
+ return self.location_x, self.location_y
+
+ # Alter class variable number for all instances
+ def increment_number(self):
+ # Increment the 'number' class variable by 1.
+ MyClass.number += 1
+
+ # This will compile and run without error, but has no current functionality.
+ def pending_functionality(self):
+ # Stubbing or placholding the body of this method.
+ pass
+```
+
+[calling]: https://www.pythonmorsels.com/topics/calling-a-function
+[class method]: https://stackoverflow.com/questions/17134653/difference-between-class-and-instance-methods
+[dunder]: https://mathspp.com/blog/pydonts/dunder-methods
+[imperative]: https://en.wikipedia.org/wiki/Imperative_programming
+[declarative]: https://en.wikipedia.org/wiki/Declarative_programming
+[oop]: https://www.digitalocean.com/community/tutorials/how-to-construct-classes-and-define-objects-in-python-3
+[functional]: https://en.wikipedia.org/wiki/Functional_programming
+[dot notation]: https://stackoverflow.com/questions/45179186/understanding-the-dot-notation-in-python
+
+## Instructions
+
+Ellen is making a game where the player has to fight aliens.
+She has just learned about Object Oriented Programming (OOP) and is eager to take advantage of what using `classes` could offer her program.
+
+To Ellen's delight, you have offered to help and she has given you the task of programming the aliens that the player has to fight.
+
+
+## 1. Create the Alien Class
+
+Define the Alien class with a constructor that accepts two parameters `` and ``, putting them into `x_coordinate` and `y_coordinate` instance variables.
+Every alien will also start off with a health level of 3, so the `health` variable should be initialized as well.
+
+```python
+>>> alien = Alien(2, 0)
+>>> alien.x_coordinate
+2
+>>> alien.y_coordinate
+0
+>>> alien.health
+3
+```
+
+Now, each alien should be able to internally track its own position and health.
+
+## 2. The `hit` Method
+
+Ellen would like the Alien `class` to have a `hit` method that decrements the health of an alien object by 1 when called.
+This way, she can simply call `.hit()` instead of having to manually change an alien's health.
+It is up to you if `hit()` takes healths points _to_ or _below_ zero.
+
+```python
+>>> alien = Alien(0, 0)
+>>> alien.health
+
+# Initialized health value.
+3
+
+# Decrements health by 1 point.
+>>> alien.hit()
+>>> alien.health
+2
+```
+
+## 3. The `is_alive` Method
+
+You realize that if the health keeps decreasing, at some point it will probably hit 0 (_or even less!_).
+It would be a good idea to add an `is_alive` method that Ellen can quickly call to check if the alien is... well... alive. 😉
+`.is_alive()` should return a boolean.
+
+```python
+>>> alien.health
+1
+>>> alien.is_alive()
+True
+>>> alien.hit()
+>>> alien.health
+0
+>>> alien.is_alive()
+False
+```
+
+## 4. The `teleport` Method
+
+In Ellen's game, the aliens have the ability to teleport!
+You will need to write a `teleport` method that takes new `x_coordinate` and `y_coordinate` values, and changes the alien's coordinates accordingly.
+
+```python
+>>> alien.teleport(5, -4)
+>>> alien.x_coordinate
+5
+>>> alien.y_coordinate
+-4
+```
+
+## 5. The `collision_detection` Method
+
+Obviously, if the aliens can be hit by something, then they need to be able to detect when such a collision has occurred.
+However, collision detection algorithms can be tricky, and you do not yet know how to implement one.
+Ellen has said that she will do it later, but she would still like the `collision_detection` method to appear in the class as a reminder to build out the functionality.
+It will need to take a variable of some kind (probably another object), but that's really all you know.
+You will need to make sure that putting the method definition into the class doesn't cause any errors when called:
+
+```python
+>>> alien.collision_detection(other_object)
+>>>
+```
+
+## 6. Alien Counter
+
+Ellen has come back with a new request for you.
+She wants to keep track of how many aliens have been created over the game's lifetime.
+She says that it's got something to do with the scoring system.
+
+For example:
+
+```python
+>>> alien_one = Alien(5, 1)
+>>> alien_one.total_aliens_created
+1
+>>> alien_two = Alien(3, 0)
+>>> alien_two.total_aliens_created
+2
+>>> alien_one.total_aliens_created
+2
+>>> Alien.total_aliens_created
+# Accessing the variable from the class directly
+2
+```
+
+## 7. Creating a List of Aliens
+
+Ellen loves what you've done so far, but she has one more favor to ask.
+She would like a standalone (_outside the `Alien()` class_) function that creates a `list` of `Alien()` objects, given a list of positions (as `tuples`).
+
+For example:
+
+```python
+>>> alien_start_positions = [(4, 7), (-1, 0)]
+>>> aliens = new_aliens_collection(alien_start_positions)
+...
+>>> for alien in aliens:
+ print(alien.x_coordinate, alien.y_coordinate)
+(4, 7)
+(-1, 0)
+```
+
+## Source
+
+### Created by
+
+- @PaulT89
+- @BethanyG
+
+### Contributed to by
+
+- @DjangoFett
+- @kotp
+- @IsaacG
\ No newline at end of file
diff --git a/ellens-alien-game/classes.py b/ellens-alien-game/classes.py
new file mode 100644
index 0000000..aaf775c
--- /dev/null
+++ b/ellens-alien-game/classes.py
@@ -0,0 +1,55 @@
+"""Solution to Ellen's Alien Game exercise."""
+
+
+class Alien:
+ """
+ Create an Alien object with location x_coordinate and y_coordinate.
+
+ Attributes
+ ----------
+ (class)
+ total_aliens_created: int
+ x_coordinate: int - Position on the x-axis.
+ y_coordinate: int - Position on the y-axis.
+ health: int - Number of health points.
+
+ Methods
+ -------
+ hit(): Decrement Alien health by one point.
+ is_alive(): Return a boolean for if Alien is alive (if health is > 0).
+ teleport(new_x_coordinate, new_y_coordinate): Move Alien object to new coordinates.
+ collision_detection(other): Implementation TBD.
+ """
+
+ total_aliens_created: int = 0
+
+ def __init__(self, x_coordinate: int, y_coordinate: int):
+ self.health: int = 3
+ self.x_coordinate = x_coordinate
+ self.y_coordinate = y_coordinate
+ Alien.total_aliens_created += 1
+
+ def is_alive(self):
+ return self.health > 0
+
+ def hit(self):
+ self.health -= 1
+
+ def teleport(self, new_x_coordinate, new_y_coordinate):
+ self.x_coordinate = new_x_coordinate
+ self.y_coordinate = new_y_coordinate
+
+ def collision_detection(self, other):
+ pass
+
+
+def new_aliens_collection(
+ alien_start_positions: list[tuple[int, int]],
+) -> list:
+ """
+ Creates a list of Alien() objects, given a list of positions (as tuples).
+
+ :param alien_start_positions: given a list of positions
+ :return: a list of Alien() objects
+ """
+ return list(Alien(pos[0], pos[1]) for pos in alien_start_positions)
diff --git a/ellens-alien-game/classes_test.py b/ellens-alien-game/classes_test.py
new file mode 100644
index 0000000..55f19be
--- /dev/null
+++ b/ellens-alien-game/classes_test.py
@@ -0,0 +1,215 @@
+# pylint: disable=C0301, C0114, C0115, C0116, R0904
+import unittest
+import pytest
+
+
+try:
+ from classes import Alien
+except ImportError as import_fail:
+ # pylint: disable=raise-missing-from
+ raise ImportError("\n\nMISSING CLASS --> We tried to import the 'Alien' class from "
+ "your classes.py file, but could not find it."
+ "Did you misname or forget to create it?") from None
+
+try:
+ from classes import new_aliens_collection
+except ImportError as err:
+ raise ImportError("\n\nMISSING FUNCTION --> We tried to import the "
+ "new_aliens_collection() function "
+ "from your classes.py file, but could not find it. "
+ "Did you misname or forget to create it?") from None
+
+
+class ClassesTest(unittest.TestCase):
+
+ @pytest.mark.task(taskno=1)
+ def test_alien_has_correct_initial_coordinates(self):
+ """Test that the Alien class gets correctly initialised."""
+
+ alien = Alien(2, -1)
+ error_message = (f'Created a new Alien by calling Alien(2, -1). '
+ f'The Alien was initialized to position '
+ f'{(alien.x_coordinate, alien.y_coordinate)}, but the tests expected '
+ f'the object to be at position (2, -1)')
+
+ self.assertEqual((2, -1), (alien.x_coordinate, alien.y_coordinate), msg=error_message)
+
+ @pytest.mark.task(taskno=1)
+ def test_alien_has_health(self):
+ alien = Alien(0, 0)
+ error_message = (f'Created a new Alien by calling Alien(0, 0). '
+ f'The new Alien has a health of {alien.health}, '
+ f'but the tests expect health = 3')
+
+ self.assertEqual(3, alien.health, msg=error_message)
+
+ @pytest.mark.task(taskno=1)
+ def test_alien_instance_variables(self):
+ """Test instance variables are unique to specific instances."""
+
+ alien_one = Alien(-8, -1)
+ alien_two = Alien(2, 5)
+
+ coord_x_error = (f'Created two new Aliens by assigning '
+ f'alien_one = Alien(-8, -1) and alien_two = Alien(2, 5). '
+ f'Both Aliens x coordinates were {alien_two.x_coordinate}, '
+ f'but the tests expect alien_one and alien_two to have '
+ f'different x positions.')
+
+ coord_y_error = (f'Created two new Aliens by assigning '
+ f'alien_one = Alien(-8, -1) and alien_two = Alien(2, 5). '
+ f'Both Aliens y coordinates were {alien_two.y_coordinate}, '
+ f'but the tests expect alien_one and alien_two to have '
+ f'different y positions.')
+
+ self.assertFalse(alien_one.x_coordinate == alien_two.x_coordinate, msg=coord_x_error)
+ self.assertFalse(alien_one.y_coordinate == alien_two.y_coordinate, msg=coord_y_error)
+
+
+ @pytest.mark.task(taskno=2)
+ def test_alien_hit_method(self):
+ """Test class methods work as specified.
+
+ There are two valid interpretations for this method/task.
+ `self.health -= 1` and `self.health = max(0, self.health - 1)`
+ The tests for this task reflect this ambiguity.
+
+ """
+
+ test_data = [1, 2, 3, 4, 5, 6]
+ result_data = [(2,), (1,), (0,), (0, -1), (0, -2), (0, -3)]
+
+ for variant, (iterations, expected) in enumerate(zip(test_data, result_data), start=1):
+ alien = Alien(2, 2)
+
+ with self.subTest(f'variation #{variant}',
+ iterations=iterations,
+ expected=expected):
+
+ for _ in range(iterations):
+ alien.hit()
+
+ error_message = (f'Called hit() {iterations} time(s) '
+ f'on a newly created Alien. The Aliens health '
+ f'is now {alien.health}, but the tests expected '
+ f'it to be in {expected} after decrementing 1 health '
+ f'point {iterations} time(s).')
+
+ self.assertIn(alien.health, expected, msg=error_message)
+
+
+ @pytest.mark.task(taskno=3)
+ def test_alien_is_alive_method(self):
+ alien = Alien(0, 1)
+
+ alive_error = ('Created a new Alien and called hit(). '
+ 'The function is_alive() is returning False (dead) '
+ 'while alien.health is greater than 0.')
+
+ dead_error = ('Created a new Alien and called hit(). '
+ 'The function is_alive() is returning True (alive) '
+ 'while alien.health is less than or equal to 0.')
+
+ for _ in range(5):
+ alien.hit()
+ if alien.health > 0:
+ self.assertTrue(alien.is_alive(), msg=alive_error)
+ else:
+ self.assertFalse(alien.is_alive(), msg=dead_error)
+
+ @pytest.mark.task(taskno=4)
+ def test_alien_teleport_method(self):
+ alien = Alien(0, 0)
+ alien.teleport(-1, -4)
+
+ error_message = ('Called alien.teleport(-1,-4) on a newly created Alien. '
+ 'The Alien was found at position '
+ f'{(alien.x_coordinate, alien.y_coordinate)}, but the '
+ 'tests expected it at position (-1, -4).')
+
+ self.assertEqual((-1, -4), (alien.x_coordinate, alien.y_coordinate), msg=error_message)
+
+ @pytest.mark.task(taskno=5)
+ def test_alien_collision_detection_method(self):
+ alien = Alien(7, 3)
+ error_message = ('Created a new Alien at (7,3) and called '
+ 'alien.collision_detection(Alien(7, 2)). '
+ f'The method returned {alien.collision_detection(Alien(7, 2))}, '
+ 'but the tests expected None. ')
+
+ self.assertIsNone(alien.collision_detection(Alien(7, 2)), msg=error_message)
+
+
+ @pytest.mark.task(taskno=6)
+ def test_alien_class_variable(self):
+ """Test class attribute/variables are identical across instances."""
+
+ alien_one, alien_two = Alien(0, 2), Alien(-6, -1)
+ Alien.health = 6
+
+ created_error_message = ('Created two new Aliens and requested the '
+ 'total_aliens_created attribute for each one. '
+ f'Received {alien_one.total_aliens_created, alien_two.total_aliens_created} '
+ f'for total_aliens_created, but the tests expect '
+ f'the class attributes for each newly created Alien to be identical. ')
+
+ health_error_message = ('Created two new Aliens and requested the '
+ f'health attribute for each one. Received {alien_one.health, alien_two.health} '
+ 'for health, but the tests expect the class '
+ 'attributes for each newly created Alien to be identical. ')
+
+ self.assertEqual(alien_two.total_aliens_created,
+ alien_one.total_aliens_created,
+ msg=created_error_message)
+
+ self.assertEqual(alien_two.health,
+ alien_one.health,
+ msg=health_error_message)
+
+ @pytest.mark.task(taskno=6)
+ def test_alien_total_aliens_created(self):
+ """
+ Test total_aliens_created class variable increments upon object instantiation.
+ """
+
+ Alien.total_aliens_created = 0
+ aliens = [Alien(-2, 6)]
+
+ error_message = ('Created a new Alien and called total_aliens_created for it. '
+ f'{aliens[0].total_aliens_created} was returned, but '
+ 'the tests expected that total_aliens_created would equal 1.')
+
+ self.assertEqual(1, aliens[0].total_aliens_created, msg=error_message)
+
+ aliens.append(Alien(3, 5))
+ aliens.append(Alien(-5, -5))
+
+ def error_text(alien, variable):
+ return ('Created two additional Aliens for the session.'
+ f"Alien number {alien}'s total_aliens_created variable "
+ f"is equal to {variable}, but the tests expected all "
+ 'total_aliens_created variables for all instances to be '
+ 'equal to number of alien instances created (i.e. 3).')
+
+ self.assertEqual(3, aliens[0].total_aliens_created, msg=error_text(1, aliens[0]))
+ self.assertEqual(3, aliens[1].total_aliens_created, msg=error_text(2, aliens[1]))
+ self.assertEqual(3, aliens[2].total_aliens_created, msg=error_text(3, aliens[2]))
+
+ @pytest.mark.task(taskno=7)
+ def test_new_aliens_collection(self):
+ """Test that the user knows how to create objects themselves."""
+
+ test_data = [(-2, 6), (1, 5), (-4, -3)]
+ actual_result = new_aliens_collection(test_data)
+
+ error_message = "new_aliens_collection() must return a list of Alien objects."
+
+ for obj in actual_result:
+ self.assertIsInstance(obj, Alien, msg=error_message)
+
+ for position, obj in zip(test_data, actual_result):
+ position_error = (f'After calling new_aliens_collection({test_data}), '
+ f'found {obj} initialized to position {(obj.x_coordinate, obj.y_coordinate)}, '
+ f'but the tests expected {obj} to be at position {position} instead.')
+
+ self.assertEqual(position, (obj.x_coordinate, obj.y_coordinate), msg=position_error)