# Source code for model

'''
The Model in the Model-View-Controller paradigm.
'''

# FILE VERSION: released 5/7/2017 @ 16:14

import math
from PyQt4 import QtCore, QtGui

import constants
from view.actors import Actor, CitizenPacActor, GhostActor, Food
from view.display import randomColor

[docs]def generateFoodGrid(width, height):
'''
This method returns a list of tuples containing all of the coordinates and
colors for the :class:view.actors.Food actors in the scene, given the specified
width and height of the game board in conjunction with some of the constants
defined in :mod:constants.  **No** Food **objects are to be instantiated directly
in this method**.  Rather, this method is queried from elsewhere in the framework
when it is the "appropriate" time to instantiate the Food objects.  That is,
given the width and height of the game board, this method computes

1. The total number of Food objects that can fit in both the horizontal and
vertical directions.

- This is done using width, height, and the constants
:data:constants.FOOD_RADIUS and :data:constants.FOOD_SPARSITY
- You are advised to **change** the values of these constants to test your code,
the default values do not accommodate for all corner cases.  For example, what
should your code do when you set :data:constants.FOOD_SPARSITY to 1.0?
If you set it to 2.0?  What if you set :data:constants.FOOD_RADIUS to
be 23.32?
- The total number of Food items that can fit in the :math:x and :math:y
directions are nx and ny, respectively.  That is, as a sanity check,
the total length of the list you return at the end of the method should
be exactly nx * ny, representing :math:N = n_x \cdot n_y total Food
objects based on the constraints of the problem.

2. Computes the offset variables for the starting locations of each Food object,
taking into account when :data:constants.FOOD_RADIUS does not evenly divide
width or height respectively.

- The variables dx and dy in the code correspond to the change in
position :math:\Delta x and :math:\Delta y, respectively.
- The variables tx and ty in the code correspond to the global
translation :math:t_x and :math:t_y, respectively.

.. todo::

3. Using the variables from (1) and (2), compute all Food coordinate *centers*,
and create a random color using :func:view.display.randomColor.  At the stage
that this method is called in the framework, this is all that can be computed.

- **Make sure you set** :data:constants.FULL_GAME_MODE **to** True **,
otherwise your hard work will never even be called by the framework!!!**

.. warning::

As a reminder, you should **not** create *any* Food objects in this method!

:Parameters:
width (float)
The total width of the game board at the start of the game (before any
resizing by the user.)

height (float)
The total height of the game board at the start of the game (before any
resizing by the user.)

:Preconditions:
*Size Constraints*
width and height are both positive, and are both (individually)
strictly greater than 2.0 * constants.FOOD_RADIUS *
constants.FOOD_SPARSITY.

:Return:
list
A length :math:N = n_x \cdot n_y list of **tuples** representing each
Food object.  Ensure that the order is exactly correct.  Each element of
the list should be a tuple of length 3, where the following is true:

+-------+------------------------------+---------------------------------------------+
| Index | Type                         | Value                                       |
+=======+==============================+=============================================+
| 0 | float                    | The center :math:x coordinate :math:c_x.|
+-------+------------------------------+---------------------------------------------+
| 1 | float                    | The center :math:y coordinate :math:c_y.|
+-------+------------------------------+---------------------------------------------+
| 2 | :class:PyQt4.QtGui.QColor  | A random  color. You don't need to construct|
|       |                              | one on your own, simply call the function   |
|       |                              | :func:view.display.randomColor *already*  |
|       |                              | imported for you at the top of the file.    |
+-------+------------------------------+---------------------------------------------+

.. warning::

The method calling this function (:func:model.Scene.generate) is going
to loop through what you return similar to

.. code-block:: py

lst = model.generateFoodGrid(w, h)
for cx, cy, color in lst:
# ... create the actual Food object ...

So if the code crashes on a line similar to that one, you should inspect
what you append to the list all_food and make sure it adheres to
the above specification.
'''
# Initial setup
all_food    = []
half_width  = width * 0.5
half_height = height * 0.5
food_fill   = diam * constants.FOOD_SPARSITY
half_fill   = food_fill * 0.5

# 1. Compute the x and y scaling factors
nx = float(int(width)  / int(food_fill))
ny = float(int(height) / int(food_fill))
# constraints: at least 4 food always
nx = max(nx, 2.0)  # Must be >= 2 since we need to divide by
ny = max(ny, 2.0)  # nx - 1.0 in the next step!

# 2. Compute the change in position for x / y directions, and coordinate transforms
dx = (width  - food_fill) / (nx - 1.0)
dy = (height - food_fill) / (ny - 1.0)
# coordinate transformation
tx = -half_width  + half_fill
ty = -half_height + half_fill

# 3. TODO: write step 3!
# - You *MUST* use *TWO* while loops, one nested inside the other.
# - You *MUST* use dx, tx, dy, and ty in your computation
# - When you write your while-loops, you must document the meaning of any variables
#   that you create to use in the loops, using the notation for invariants we've
#   been going over in class.  Write this documentation as comments in the
#   appropriate places in your while-loops.

return all_food

[docs]class Scene(QtGui.QGraphicsScene):
'''
The main model of the game, responsible for creating and maintaining the state of
the actors in the game.   By inheriting from :class:PyQt4.QtGui.QGraphicsScene,
an instance of this class will be receiving the update notifications coming from the
View *indirectly*.  That is, though this class maintains the locations of the
different Actors, because it is part of the PyQt4 View framework, it receives
communication directly from the View in certain portions.

For example, the :class:controller.CitizenPac class maintains a gameTimer that
indirectly controls all of the Actors in the scene.  At the end of the constructor
for that class, the timer is connect ed to the :func:model.Scene.advance method
in this class.  In essence, this is an indirect "loop", but since the timer is
officially managed by the View, the impact is that the View technically communicates
directly with the Model (this class).  This relationship is instigated and
controlled by the Controller, but we wanted to clarify by example that the
relationships between the Model, View, and Controller in this framework have blurred
boundary lines.  If you decide to make your own Model-View-Controller one day, you
may find yourself in a similar scenario -- the limitations of the PyQt4
framework in the sense that technically the Model is "directly managed by the View,
which is indirectly managed by the Controller" are confusing.  However, though the
relationships between these items become less translucent, its benefits (for
technical reasons not explained here) are worthy.  The short version: PyQt4
has a sophisticated backend framework developed over decades by one of the industry
leaders.  The framework excels at maintaining consistent internal state, and so we
lean on these features at the expense of "blurred Model-View-Controller"
relationships.

:Attributes:
controller (:class:controller.CitizenPac)
A reference to the Controller to be able to propagate events received from
PyQt4 such as collisions back to the Controller so it can decide what
action to perform.

view (:class:PyQt4.QtGui.QGraphicsView)
A pointer to the QGraphicsView instance that this Scene is bound to.
This reference is also accessible via the parent class's parent member,
stored as view for convenience.  Every
:class:PyQt4.QtGui.QGraphicsScene instance must be bound to a
:class:PyQt4.QtGui.QGraphicsView instance --- this is how the scene is
actually displayed.

citizenPac (:class:view.actors.CitizenPacActor)
The representation of CitizenPac.

ghosts (list)
A list of :class:view.actors.GhostActor instances, representing all of the
Ghosts in the scene.

food (list)
A list of :class:view.actors.Food instances, representing where all of the
Food in the scene is.

gameRunning (bool)
A boolean representing whether or not the game is currently running.  In
particular, collision events, moving actors, etc, should not continue
if the game is not running.  See the :func:model.Scene.advance method for
how it is used.

foodEaten (int)
An integer representing how many Food collisions have been detected.  When
foodEaten == len(self.food), the controller is notified that the
Game has completed.
'''
def __init__(self, controller, view):
super(Scene, self).__init__(view)
# Parent references
self.controller  = controller
self.view        = view
# Actor references
self.citizenPac  = None
self.ghosts      = []
self.food        = []
# Game state convenience members
self.gameRunning = False
self.foodEaten   = 0

[docs]    def generate(self, width, height):
'''
Responsible for creating the initial conditions of the game, including where
CitizenPac and the Ghosts start, and the location of all the Food.  This method
calls the :func:model.generateFoodGrid method using the width and height of
the :class:PyQt4.QtGui.QGraphicsView instance that this (self) instance
is bound to.

All instances created are registered using the :func:model.Scene.registerActor
method, which is in turn responsible for storing the the generated actors in the
related attributes of this instance (i.e. self.citizenPac or self.food).

If you want to extend the game to add more types of actors, this is where you
should create them.

:Parameters:
width (float)
The starting width of the window we will be drawing to.

height (float)
The starting height of the window we will be drawing to.

.. warning::

Do **not** call this method from the constructor.  The
:class:controller.CitizenPac class is responsible for calling this method.
Because of the "blurred Model-View-Controller" relationships, the Scene
must be instantiated before the View has completed initialization.  In order
to use the *correct* width and height, initialization of the actors
in the scene must be deferred until **after** the View's Layout has been
performed (this is controlled by PyQt4).
'''
# Generate the CitizenPac and Ghost actors.  By default, they are dispersed in
# a circular pattern.  There can only be one CitizenPac
nActors = float(constants.NUM_GHOSTS + 1)  # All ghosts plus CitizenPac
two_pi  = 2.0 * math.pi
for i in range(int(nActors)):
# Create the new x and y coordinates on the circle
t  = (i * two_pi) / nActors

# The constructor arguments are the same for both, but the class is
# different.  We negative scaling for the y coordinate because the
# the Qt coordinate system is positive y down.
args = [self, cx, cy, constants.SPLINE_COORD_SCALE, -constants.SPLINE_COORD_SCALE]

# If it is the first iteration, create CitizenPac (always at the bottom)
actor = CitizenPacActor(*args) if i == 0 else GhostActor(*args)

# Make sure to register the actor!
self.registerActor(actor, cx, cy)

# Generate all of the Food
if constants.FULL_GAME_MODE:
try:
food_coords = generateFoodGrid(width, height)
for cx, cy, color in food_coords:
food = Food(self, cx, cy, color, constants.FOOD_RADIUS)
self.registerActor(food, cx, cy)
except:
self.controller.errorOut()

[docs]    def registerActor(self, actor, cx, cy):
'''
Registers an :class:view.actors.Actor with this Scene.  This method **must**
be called for the game mechanics (e.g. collisions) to be detected.  It is
assumed that the actor being registered has already been added to the
PyQt4 side of the scene by construction.  That is, the
:func:model.Scene.generate function will instantiate an actor passing self
as the first parameter.  Since each actor extends the
:class:PyQt4.QtGui.QGraphicsItem class, the item is automatically added to the
Scene.  Alternatively, you can call the addItem method (inherited from the
:class:PyQt4.QtGui.QGraphicsScene class) to associated the item with the
graphics backend.

After construction, the PyQt4.QGraphicsItem class has not had its position
set.  The setPos function is called at the end of this method, assuming it
was an instance of a class we are currently tracking.

:Parameters:
actor (:class:view.display.Actor)
An :class:view.display.Actor instance.  Currently the only classes
that are registered are

1. :class:view.display.CitizenPacActor (stored in self.citizenPac).
2. :class:view.display.GhostActor (appended to self.ghosts).
3. :class:view.display.Food (appended to self.food).

If you want to extend the game to allow more actors, declare the
appropriate variables in the constructor of this class to maintain their
state, and register them here.

cx (float)
The :math:x coordinate of the actor.

cy (float)
The :math:y coordinate of the actor.

:Preconditions:
*Size Constraints*
The parameters cx and cy are assumed to be valid coordinates
for this scene.  If they are not, the actor will simply be "displayed"
off-screen.
'''
if not actor:
raise RuntimeError("Actor was None, and therefore cannot be registered.")

# Associate the actor with the related member field.  Extending the game?  Add
# more member variables in the constructor to maintain tracking them, and check
# the type here.
#
# You will also need to update the advance method of this class to check for
# collisions with these new types of actors.
if type(actor) is CitizenPacActor:
if self.citizenPac:
raise RuntimeError("There can only be one CitizenPac per game!")
self.citizenPac = actor
elif type(actor) is Food:
self.food.append(actor)
elif type(actor) is GhostActor:
self.ghosts.append(actor)
else:
raise RuntimeError(
"Unknown actor of type [{}] cannot be registered.".format(type(actor))
)

# If we get to this point, then we know that the actor provided inherits from
# the view.actors.Actor class, and therefore will have the setPos function.
actor.setPos(cx, cy)

[docs]    def numFoodEaten(self):
'''
Returns the number of food items that have been eaten in this round of the game.

:Return:
int
The number of food eaten so far in this round of the game.
'''
return self.foodEaten

[docs]    def setRunning(self, running):
'''
Pass-through method for the Controller to toggle whether or not the game is
currently-running, used to avoid processing collisions before the game is
resumed.  Simply sets self.gameRunning to the value of running.

:Parameters:
running (bool)
Whether or not the game is currently running.
'''
self.gameRunning = running

[docs]    def reset(self):
'''
This method resets the game state for **all** actors in the scene, and ensures
that all food becomes edible again.  This method should not modify any instances
directly --- call the :func:view.actors.Actor.reset for the appropriate
entities of this instance.

.. tip::

You are encouraged to refer to the
implementation <_modules/model.html#Scene.advance>__ of
:func:model.Scene.advance, in addition to the **parent** class
documentation of :class:view.actors.Actor, to complete making food edible.
'''
# TODO: implement the reset
pass

[docs]    def wrapActor(self, actor, width, height):
'''
This method is responsible for adjusting the position of an Actor so that it
remains within the confines of the game grid.

.. tip::

Refer to the :ref:coordinates section of the writeup

:Parameters:
actor (:class:view.actors.Actor)
The actor to modify the position of, based off width and height.

width (float)
The width of the bounding rectangle that defines the coordinate system
for this scene.

height (float)
The height of the bounding rectangle that defines the coordinate system
for this scene.

:Preconditions:
**Size Constraints**
The width and height must be greater than or equal to 1.0.
'''
# Failsafe: make sure it is not None
if not actor:
return

# Failsafe: make sure it is an actor we can work with.
# Exercise: why can't we use type(actor) is not Actor?
if not isinstance(actor, Actor):
return

# TODO:

################################################################################
# DO NOT MODIFY !!!                                                            #
# After you call actor.setPos(computed_x, computed_y), the update()    #
# method signals to the drawing framework that it needs to repaint this actor. #
actor.update()                                                                 #
################################################################################

[docs]    def wrapRelevantActors(self):
'''
Confines all relevant actors to remain within the bounds of the game grid
by calling :func:model.Scene.wrapActor for every actor that can move in
the game.  If you desired to have food moving, you would need to update this
method to do this as well!
'''
# Acquire the width and height of the current bounding rectangle
bounds = self.sceneRect()
width  = bounds.width()
height = bounds.height()

# Wrap the actors that can move in the game
self.wrapActor(self.citizenPac, width, height)
for ghost in self.ghosts:
self.wrapActor(ghost, width, height)

'''
This method is what the :class:controller.CitizenPac hooks its gameTimer
to.  It is responsible for processing three things:

1. Process collisions when self.gameRunning and
:data:constants.FULL_GAME_MODE are both True.
2. Confine actors to the game grid via :func:model.Scene.wrapRelevantActors.
3. Call the super class advance, which will propagate to the Actors.

The third point there is particularly important not to omit, otherwise the
animations on the food, decisions about moving, etc will not occur.
'''
if self.gameRunning and constants.FULL_GAME_MODE:
for ghost in self.ghosts:
if self.citizenPac.collidesWithItem(ghost):
self.controller.lostLife()
return

for food in self.food:
if food.isVisible() and self.citizenPac.collidesWithItem(food):
food.hide()
self.foodEaten += 1
self.controller.foodConsumed()

if self.foodEaten == len(self.food):
self.controller.gameWon()

self.wrapRelevantActors()

[docs]    def keyPressEvent(self, e):
'''
If the key pressed is one of w, s, d, or a, call the
:func:view.actors.CitizenPacActor.queueMove actor with the appropriate move
direction and True to signal that the user requested CitizenPac to move in
this direction.

If it is not one of these keys, call the super class keyPressEvent to
allow other keys to be applied elsewhere in the framework.

:Parameters:
e (:class:PyQt4.QtGui.QKeyEvent)
The key event provided by the Qt backend.
'''
key = e.key()

if key == QtCore.Qt.Key_W:
self.citizenPac.queueMove(constants.MOVE_NORTH, True)
elif key == QtCore.Qt.Key_S:
self.citizenPac.queueMove(constants.MOVE_SOUTH, True)
elif key == QtCore.Qt.Key_D:
self.citizenPac.queueMove(constants.MOVE_EAST, True)
elif key == QtCore.Qt.Key_A:
self.citizenPac.queueMove(constants.MOVE_WEST, True)
else:
super(Scene, self).keyPressEvent(e)

[docs]    def keyReleaseEvent(self, e):
'''
If the key released is one of w, s, d, or a, call the
:func:view.actors.CitizenPacActor.queueMove actor with the appropriate move
direction and False to signal that the user has stopped requesting that
CitizenPac move in this direction.

If the space key is released, then the user has requested to pause or resume
the game, call the :func:controller.CitizenPac.gameRunningSwitched function.

If the r key is released, then the :func:controller.CitizenPac.replay
method is called.  Note this method does not reset the game unless it has been
won or lost, so the r key only has an effect at the end of the game.

If it is not one of these keys, call the super class keyPressEvent to
allow other keys to be applied elsewhere in the framework.

:Parameters:
e (:class:PyQt4.QtGui.QKeyEvent)
The key event provided by the Qt backend.
'''
key = e.key()

if key == QtCore.Qt.Key_W:
self.citizenPac.queueMove(constants.MOVE_NORTH, False)
elif key == QtCore.Qt.Key_S:
self.citizenPac.queueMove(constants.MOVE_SOUTH, False)
elif key == QtCore.Qt.Key_D:
self.citizenPac.queueMove(constants.MOVE_EAST, False)
elif key == QtCore.Qt.Key_A:
self.citizenPac.queueMove(constants.MOVE_WEST, False)
elif key == QtCore.Qt.Key_Space:
self.controller.gameRunningSwitched()
elif key == QtCore.Qt.Key_R:
self.controller.replay()
else:
super(Scene, self).keyPressEvent(e)