# Problem Set 6: Simulating robots

import math
import random

import ps6_visualize as visualize
import pylab
import numpy

class Position(object):
    """
    A Position represents a location in a two-dimensional room.
    """
    def __init__(self, x, y):
        """
        Initializes a position with coordinates (x, y).
        """
        self.x = x
        self.y = y
    def getX(self):
        return self.x
    def getY(self):
        return self.y
    def getNewPosition(self, angle, speed):
        """
        Computes and returns the new Position after a single clock-tick has
        passed, with this object as the current position, and with the
        specified angle and speed.

        Does NOT test whether the returned position fits inside the room.

        angle: float representing angle in degrees, 0 <= angle < 360
        speed: positive float representing speed

        Returns: a Position object representing the new position.
        """
        old_x, old_y = self.getX(), self.getY()
        # Compute the change in position
        delta_y = speed * math.cos(math.radians(angle))
        delta_x = speed * math.sin(math.radians(angle))
        # Add that to the existing position
        new_x = round((old_x + delta_x),2)
        new_y = round((old_y + delta_y),2)
        return Position(new_x, new_y)

# === Problems 1

class RectangularRoom(object):
    """
    A RectangularRoom represents a rectangular region containing clean or dirty
    tiles.

    A room has a width and a height and contains (width * height) tiles. At any
    particular time, each of these tiles is either clean or dirty.
    """
    def __init__(self, width, height):
        """
        Initializes a rectangular room with the specified width and height.

        Initially, no tiles in the room have been cleaned.

        width: an integer > 0
        height: an integer > 0
        """
        self.robots=[]
        self.width=width
        self.height=height
        self.floordictionary={}
        for x in range(width):
            for y in range(height):
                self.floordictionary[x,y]=1
    
    def cleanTileAtPosition(self, pos):
        """
        Mark the tile under the position POS as cleaned.

        Assumes that POS represents a valid position inside this room.

        pos: a Position
        """
        self.floordictionary[int(pos.x),int(pos.y)]=0      

    def isTileCleaned(self, m, n):
        """
        Return True if the tile (m, n) has been cleaned.

        Assumes that (m, n) represents a valid tile inside the room.

        m: an integer
        n: an integer
        returns: True if (m, n) is cleaned, False otherwise
        """
        return self.floordictionary[m,n]==0
    
    def getNumTiles(self):
        """
        Return the total number of tiles in the room.

        returns: an integer
        """
        return len(self.floordictionary)

    def getNumCleanedTiles(self):
        """
        Return the total number of clean tiles in the room.

        returns: an integer
        """
        cleaned=[]
        for k in self.floordictionary.keys():
            if self.floordictionary[k]==0:
                cleaned.append(k)
        return len(cleaned)
    
    def getRandomPosition(self):
        """
        Return a random position inside the room.

        returns: a Position object.
        """
        return Position(float((random.randrange(self.width)+ random.randrange(11)*.1)),
                         float((random.randrange(self.height)+ random.randrange(11)*.1)))
        
    def isPositionInRoom(self, pos):
        """
        Return True if pos is inside the room.

        pos: a Position object.
        returns: True if pos is in the room, False otherwise.
        """
        if pos.x>=self.width or pos.x<=0:
            return False
        elif pos.y>=self.height or pos.y<=0:
            return False
        else:
            return True

class Robot(object):
    """
    Represents a robot cleaning a particular room.

    At all times the robot has a particular position and direction in the room.
    The robot also has a fixed speed.

    Subclasses of Robot should provide movement strategies by implementing
    updatePositionAndClean(), which simulates a single time-step.
    """
    def __init__(self, name, room, speed):
        """
        Initializes a Robot with the given speed in the specified room. The
        robot initially has a random direction and a random position in the
        room. The robot cleans the tile it is on.

        room:  a RectangularRoom object.
        speed: a float (speed > 0)
        """
        self.name=name
        room.robots.append(self)
        self.room=room
        self.position=room.getRandomPosition()
        self.speed=speed
        self.direction=random.randrange(360)
        room.cleanTileAtPosition(self.position)
        
    def getRobotPosition(self):
        """
        Return the position of the robot.

        returns: a Position object giving the robot's position.
        """
        return self.position
    
    def getRobotDirection(self):
        """
        Return the direction of the robot.

        returns: an integer d giving the direction of the robot as an angle in
        degrees, 0 <= d < 360.
        """
        return self.direction

    def setRobotPosition(self, position):
        """
        Set the position of the robot to POSITION.

        position: a Position object.
        """
        try:
            self.position=position
        except:
            print("not a valid position")

    def setRobotDirection(self, direction):
        """
        Set the direction of the robot to DIRECTION.

        direction: integer representing an angle in degrees
        """
        self.direction=direction

    def updatePositionAndClean(self):
        """
        Simulate the raise passage of a single time-step.

        Move the robot to a new position and mark the tile it is on as having
        been cleaned.
        """
        testPos=self.position.getNewPosition(self.direction, self.speed)
        if not self.room.isPositionInRoom(testPos):
            self.setRobotDirection(random.randrange(1,361,1))
            print(self.name + " bounces " + str(self.direction)+" degrees")
            self.updatePositionAndClean()
            
        else:
            self.position=self.position.getNewPosition(self.direction, self.speed)
            self.room.cleanTileAtPosition(self.position)
        
# === Problem 2 
# Fixed issues in problem 1, now uses float->int conversion to identify panel to be cleaned in room
# Idea = write a recursive element so it tests validitiy on each
# Validiated to work up to problem 3. method updatepostion copied to class, remove out of range if needed

class StandardRobot(Robot):
    """
    A StandardRobot is a Robot with the standard movement strategy.

    At each time-step, a StandardRobot attempts to move in its current direction; when
    it hits a wall, it chooses a new direction randomly.
    """
    def updatePositionAndClean(self):
        """
        Simulate the passage of a single time-step.

        Move the robot to a new position and mark the tile it is on as having
        been cleaned.
        """
        testPos=self.position.getNewPosition(self.direction, self.speed)
        if not self.room.isPositionInRoom(testPos):
            self.setRobotDirection(random.randrange(361))
##            print(self.name + " bounces " + str(self.direction)+" degrees")
            self.updatePositionAndClean()
            
        else:
            self.position=self.position.getNewPosition(self.direction, self.speed)
            self.room.cleanTileAtPosition(self.position)
                   

# === Problem 3
# How to approach? First get multiple robots going all from simulation function - check
# figure out how to generate and record data + coverage - check
# iterate over a number of simulations and figure out how to mean data - check
## This is fully operational, like a Death Star ready to destroy the rebel fleet
## 

def runSimulation(num_robots, speed, width, height, min_coverage, num_trials,
                  robot_type):
    """
    Runs NUM_TRIALS trials of the simulation and returns the mean number of
    time-steps needed to clean the fraction MIN_COVERAGE of the room.

    The simulation is run with NUM_ROBOTS robots of type ROBOT_TYPE, each with
    speed SPEED, in a room of dimensions WIDTH x HEIGHT.

    num_robots: an int (num_robots > 0)
    speed: a float (speed > 0)
    width: an int (width > 0)
    height: an int (height > 0)
    min_coverage: a float (0 <= min_coverage <= 1.0)
    num_trials: an int (num_trials > 0)
    robot_type: class of robot to be instantiated (e.g. Robot or
                RandomWalkRobot)
    """
    resultDict={}
    totalArray=0
    for trial in range(num_trials):
##        anim=visualize.RobotVisualization(num_robots,width,height)
        room=RectangularRoom(width,height)
        counter=0
        for n in range(num_robots):
            n=robot_type("robot-"+str(n),room,speed)
        while (room.getNumCleanedTiles()/len(room.floordictionary) < min_coverage):
            for r in room.robots:
                r.updatePositionAndClean()
##            anim.update(room,room.robots)
            counter+=1
        resultDict[trial]=counter
##        print(str(counter) + " seconds")
##        print(str(room.getNumCleanedTiles())+ " / " + str(len(room.floordictionary)))
    for x,y in resultDict.items():
        totalArray+=y
    return totalArray / len(resultDict)  


# === Problem 4
#
# 1) How long does it take to clean 80% of a 20×20 room with each of 1-10 robots?
#
# 2) How long does it take two robots to clean 80% of rooms with dimensions 
#	 20×20, 25×16, 40×10, 50×8, 80×5, and 100×4?


def showPlot1():
    """
    Produces a plot showing dependence of cleaning time on number of robots.
    """
    ydata,robotarray=[],[]
    for robots in range(1,11):  
        ydata+=[runSimulation(robots, 1, 20, 20, 0.8, 20, StandardRobot)]          
        robotarray+=[robots]    

    print(ydata)
    pylab.plot(robotarray,ydata)
    pylab.title('Seconds to Clean Room by Robots Used')
    pylab.xlabel('Robots')
    pylab.ylabel('Seconds')
    pylab.show()


def showPlot2():
    """
    Produces a plot showing dependence of cleaning time on room shape.
    """
    
    dimensions=[(20,20),(25,16),(40,10),(50,8),(80,5),(100,4)]
    ydata,ratio=[],[]

    for t in dimensions:  
        ydata+=[runSimulation(2, 1, t[0], t[1], 0.8, 20, StandardRobot)]          
        ratio+=[t[0]/t[1]]

    print(ydata)
    pylab.plot(ratio,ydata)
    pylab.title('Seconds to Clean Room by Room Dimensions Ratio')
    pylab.xlabel('Room Dimension Ratio(width/height)')
    pylab.ylabel('Seconds')
    pylab.show()


### === Problem 5
##

    
class RandomWalkRobot(Robot):
    """
    A RandomWalkRobot is a robot with the "random walk" movement strategy: it
    chooses a new direction at random after each time-step.
    """
    def updatePositionAndClean(self):
        """
        Simulate the passage of a single time-step.

        Move the robot to a new position and mark the tile it is on as having
        been cleaned.
        """
        
        testPos=self.position.getNewPosition(random.randrange(361), self.speed)
        if not self.room.isPositionInRoom(testPos):
            self.updatePositionAndClean()
            
        else:
            self.position=testPos
            self.room.cleanTileAtPosition(self.position)
                   

### === Problem 6
##
### For the parameters tested below (cleaning 80% of a 20x20 square room),
### RandomWalkRobots take approximately twice as long to clean the same room as
### StandardRobots do.

            
def showPlot3():
    """
    Produces a plot comparing the two robot strategies.
    """
    ydata,robotarray,zdata=[],[],[]
    for robots in range(1,11):  
        ydata+=[runSimulation(robots, 1, 20, 20, 0.8, 20, StandardRobot)]
        zdata+=[runSimulation(robots, 1, 20, 20, 0.8, 20, RandomWalkRobot)]
        robotarray+=[robots]    


    pylab.plot(robotarray,ydata,linewidth=3.0,color="red", label="Standard")
    pylab.plot(robotarray,zdata,linewidth=3.0,color="black", label="Random")    
    pylab.title('Seconds to Clean Room by Robots Used')
    pylab.xlabel('Robots')
    pylab.ylabel('Seconds')
    pylab.show()

### 
### Get some - problem vanquished


