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)