Python - Infinite loop in runSimulation function - python

This is an assignment where we are simulating those roomba vacuum cleaner robots. Anyway I would really appreciate some help with the runSimulation function. It launches into an infinite loop at the while loop. As far as I know the values it calculates at the condition of the while loops are correct but are they initialised each time the while loop is tested? I don't think so but could be wrong. So this simulation function instantiates a room of width and height and robots of num_robots and the simulation runs until the floor is cleaned until min_coverage is reached. The result from runSimulation is the average time steps it takes to reach min_coverage. Thanks in advance!
Simulating robots
import math
import random
import ps2_visualize
import pylab
from ps2_verify_movement27 import testRobotMovement
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: number 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()
angle = float(angle)
# 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 = old_x + delta_x
new_y = old_y + delta_y
return Position(new_x, new_y)
def __str__(self):
return "(%0.2f, %0.2f)" % (self.x, self.y)
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.width = width
self.height = height
self.tiles = [[False] * self.height for i in range(self.width)]
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 - pos is a tuple (x, y)
"""
(x_tile, y_tile) = (int(math.floor(pos.getX())), int(math.floor(pos.getY())))
#print (x_tile, y_tile)
self.tiles[x_tile][y_tile] = True
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
"""
self.m = m
self.n = n
if self.tiles[self.m][self.n] == True:
return True
else:
return False
def getNumTiles(self):
"""
Return the total number of tiles in the room.
returns: an integer
"""
return self.width*self.height
def getNumCleanedTiles(self):
"""
Return the total number of clean tiles in the room.
returns: an integer
"""
numCleanTiles = 0
for row in range(self.width):
for column in range(self.height):
if self.tiles[row][column] == True:
numCleanTiles +=1
return numCleanTiles
def getRandomPosition(self):
"""
Return a random position inside the room.
returns: a Position object.
"""
#
return Position(random.randrange(0, self.width), random.randrange(0, self.height))
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 0 <= pos.getX() < self.width and 0 <= pos.getY() < self.height:
return True
else:
return False
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, 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)
"""
#raise NotImplementedError
self.room = room
self.speed = speed
self.position = self.room.getRandomPosition()
self.direction = random.randint(0, 359)
self.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.
"""
d = self.direction
return d
def setRobotPosition(self, position):
"""
Set the position of the robot to POSITION.
position: a Position object.
"""
self.position = position
return self.position
def setRobotDirection(self, direction):
"""
Set the direction of the robot to DIRECTION.
direction: integer representing an angle in degrees
"""
self.direction = direction
return self.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.
"""
raise NotImplementedError # don't change this!
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 would hit a wall, it *instead* chooses a new direction
randomly.
"""
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.
"""
newPosition = self.position.getNewPosition(self.direction, self.speed)
if 0 <= newPosition.getX() <= self.room.width and 0 <= newPosition.getY() <= self.room.height:
self.setRobotPosition(newPosition)
self.room.cleanTileAtPosition(self.position)
else:
self.setRobotDirection(random.randint(0, 359))
# Uncomment this line to see your implementation of StandardRobot in action!
#testRobotMovement(StandardRobot, RectangularRoom)
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. StandardRobot or
RandomWalkRobot)
"""
numTimeStepsList = []
for i in range(num_trials):
room1 = RectangularRoom(width, height)
numTimeSteps = 0
robot = []
for n in range(num_robots-1):
robot[n] = robot_type(room1, speed)
while (1.0 - float(room1.getNumCleanedTiles())/float(room1.getNumTiles())) >= min_coverage:
for n in range(num_robots-1):
robot[n].updatePositionAndClean()
numTimeSteps += 1
#print numTimeSteps
numTimeStepsList.append(numTimeSteps)
#print numTimeStepsList
return sum(numTimeStepsList)/len(numTimeStepsList)
#raise NotImplementedError
# Uncomment this line to see how much your simulation takes on average
print runSimulation(1, 1.0, 10, 10, 0.75, 30, StandardRobot)
class RandomWalkRobot(Robot):
"""
A RandomWalkRobot is a robot with the "random walk" movement strategy: it
chooses a new direction at random at the end of 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.
"""
raise NotImplementedError
def showPlot1(title, x_label, y_label):
"""
What information does the plot produced by this function tell you?
"""
num_robot_range = range(1, 11)
times1 = []
times2 = []
for num_robots in num_robot_range:
print "Plotting", num_robots, "robots..."
times1.append(runSimulation(num_robots, 1.0, 20, 20, 0.8, 20, StandardRobot))
times2.append(runSimulation(num_robots, 1.0, 20, 20, 0.8, 20, RandomWalkRobot))
pylab.plot(num_robot_range, times1)
pylab.plot(num_robot_range, times2)
pylab.title(title)
pylab.legend(('StandardRobot', 'RandomWalkRobot'))
pylab.xlabel(x_label)
pylab.ylabel(y_label)
pylab.show()
def showPlot2(title, x_label, y_label):
"""
What information does the plot produced by this function tell you?
"""
aspect_ratios = []
times1 = []
times2 = []
for width in [10, 20, 25, 50]:
height = 300/width
print "Plotting cleaning time for a room of width:", width, "by height:", height
aspect_ratios.append(float(width) / height)
times1.append(runSimulation(2, 1.0, width, height, 0.8, 200, StandardRobot))
times2.append(runSimulation(2, 1.0, width, height, 0.8, 200, RandomWalkRobot))
pylab.plot(aspect_ratios, times1)
pylab.plot(aspect_ratios, times2)
pylab.title(title)
pylab.legend(('StandardRobot', 'RandomWalkRobot'))
pylab.xlabel(x_label)
pylab.ylabel(y_label)
pylab.show()

When you call runSimulation with a num_robots argument of 1, it doesn't actually make any robots at all, so nothing ever gets cleaned.
The culprit is this loop:
for n in range(num_robots-1):
robot[n] = robot_type(room1, speed)
That's almost all wrong. You can't assign to any index in the robot list because it starts empty. And you are always looping one fewer time than you should (zero times if num_robots is 1). You probably want something more like:
for _ in range(num_robots):
robot.append(robot_type(room1, speed))

Related

Class angle value is reset after running move function

I am working on a little project to have autonomous cells move around and eventually be a little game of life simulation. Currently I'm having an issue with randomizing the cells movement. I have the cell as a class and set the starting angle in the init then in a move function the angle is updated. For some reason the updated angle is reset the next time the move function is called. To handle the simulation window and physics I'm using Python Arcade with pymunk physics.
Cell Class
import arcade
import random
import math
from dice import Dice
cell_types = ["Plant", "Animal"] # add fungus and virus later
d20 = Dice(20, 1)
count = 0
class Cell(arcade.SpriteCircle):
""" Cell Sprite """
def __init__(self, radius, color, soft, mass, x, y):
""" Init """
# initialize SpriteCircle parent class
super().__init__(radius, color, soft)
# body
self.mass = radius * mass
self.speed = radius
self.center_x = x
self.center_y = y
self.angle = random.randint(0, 360)
self.hit_box_algorithm = "Simple"
# characteristics
self.type = random.choice(cell_types)
def move(self):
# roll a d20
roll = d20.roll()
print(f"roll: {roll}")
# if d20 is 15 or more turn right
# if d20 is 5 or less turn left
print(f"old angle: {self.angle}")
if roll >= 15:
self.angle -= 90
elif roll <= 5:
self.angle += 90
print(f"new angle: {self.angle}")
# convert angle to radians
angle_rad = math.radians(self.angle)
# find next coordinates and save as a tuple
print(f"old x pos: {self.center_x}")
print(f"old y pos: {self.center_y}")
self.center_x += self.speed * math.cos(angle_rad)
self.center_y += self.speed * math.sin(angle_rad)
print(f"new x pos: {self.center_x}")
print(f"new y pos: {self.center_y}")
# return the tuple for apply force function
movement_vector = (self.center_x, self.center_y)
return movement_vector
Dice class for reference, it's just a way to have a randrange as an object instead of typing out the function each time and does function as expected otherwise the results later in the post would not have any variance between the old and new angles.
import random
class Dice:
""" Create a die specifying sides and how many dice"""
def __init__(self, sides, count=1):
self.sides = sides
self.count = count
def roll(self):
""" Roll the set of dice"""
total = 0
for i in range(self.count):
total += random.randrange(1, self.sides)
return total
The class is initialized as a "new_cell" and added into a spritelist and when the on_update function runs, the move function is called.
Relevant code for main.py using arcades boilerplate window template, segments of boilerplate not in use have been cut.
https://api.arcade.academy/en/stable/examples/starting_template.html#starting-template
import arcade
import random
from typing import Optional
from cell import Cell
from dice import Dice
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Autonomous Cells"
STARTING_CELL_COUNT = 1
SPRITE_SIZE = 32
SPRITE_SCALING = .15
CELL_SIZE = SPRITE_SIZE * SPRITE_SCALING
CELL_SIZE_MIN_MULTIPLIER = 1
CELL_SIZE_MAX_MULTIPLIER = 5
DEFAULT_DAMPING = .5
CELL_DAMPING = 0.4
CELL_FRICTION = 0.5
DEFAULT_CELL_MASS = 1.0
CELL_MAX_SPEED = 50
class MyGame(arcade.Window):
"""
Main application class.
NOTE: Go ahead and delete the methods you don't need.
If you do need a method, delete the 'pass' and replace it
with your own code. Don't leave 'pass' in this program.
"""
def __init__(self, width, height, title):
super().__init__(width, height, title)
arcade.set_background_color(arcade.color.DARK_BLUE_GRAY)
# If you have sprite lists, you should create them here,
# and set them to None
self.cell_sprite_list = None
# physics engine
self.physics_engine = Optional[arcade.PymunkPhysicsEngine]
def setup(self):
""" Set up the game variables. Call to re-start the game. """
# Create your sprites and sprite lists here
self.cell_sprite_list = arcade.SpriteList()
for i in range(STARTING_CELL_COUNT):
new_color = (random.randrange(256),
random.randrange(256),
random.randrange(256)
)
new_cell = Cell(radius=(random.randint(CELL_SIZE_MIN_MULTIPLIER,
CELL_SIZE_MAX_MULTIPLIER
) * int(CELL_SIZE)),
color=new_color,
soft=False,
mass=DEFAULT_CELL_MASS,
x=SCREEN_WIDTH / 2 + random.randint(3, 10),
y=SCREEN_HEIGHT / 2 + random.randint(3, 10)
)
self.cell_sprite_list.append(new_cell)
# physics engine setup
damping = DEFAULT_DAMPING
self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
gravity=(0, 0))
# add cell sprites to physics engine
for cell in self.cell_sprite_list:
self.physics_engine.add_sprite(cell,
friction=CELL_FRICTION,
collision_type="Cell",
damping=CELL_DAMPING,
max_velocity=CELL_MAX_SPEED)
def on_draw(self):
"""
Render the screen.
"""
# This command should happen before we start drawing. It will clear
# the screen to the background color, and erase what we drew last frame.
self.clear()
self.cell_sprite_list.draw()
# Call draw() on all your sprite lists below
def on_update(self, delta_time):
"""
All the logic to move, and the game logic goes here.
Normally, you'll call update() on the sprite lists that
need it.
"""
for cell in self.cell_sprite_list:
self.physics_engine.apply_force(cell, cell.move())
self.physics_engine.step()
def main():
""" Main function """
game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
game.setup()
arcade.run()
if __name__ == "__main__":
main()
move function called for each cell in the sprite list after this loop finishes the cells initial angle resets to the value at instantiation rather than retaining the newly applied value from the move function.
The function seems to work but after the for loop exits each cells angle variable is reset to its original state. Console log output shows that while in the for loop the angle is updated but after exiting the loop and running the next time the angle has been reset to the original value.
Angle before move called: 303.0
roll: 17
old angle: 303.0
new angle: 213.0
old x pos: 450.6516108053191
old y pos: 288.2189227316175
new x pos: 437.2328817181923
new y pos: 279.5046981713771
angle after move called: 213.0
Angle before move called: 303.0
roll: 3
old angle: 303.0
new angle: 393.0
old x pos: 451.40957273618153
old y pos: 287.87260184601035
new x pos: 464.8283018233083
new y pos: 296.5868264062508
angle after move called: 393.0
I have tried reworking the movement function and calling the movement function multiple times per update outside of the for loop. when called consecutively the angle does carry over to the next call but is reset to the original value by the time the next on_update function runs.
I was expecting the self.angle of the instanced cell in the cell_sprite_list to update to the new angle generated by the move function.
If you just need to move circle randomly, you can directly update its coordinates:
import arcade
import random
class Game(arcade.Window):
def __init__(self):
super().__init__()
self.x = 400
self.y = 300
self.x_direction = random.choice([-1, 1])
self.y_direction = random.choice([-1, 1])
def on_draw(self):
self.clear()
arcade.draw_circle_filled(self.x, self.y, 30, arcade.color.RED)
def on_update(self, delta_time):
self.x += 3 * self.x_direction
self.y += 3 * self.y_direction
if random.random() < 0.1:
self.x_direction = random.choice([-1, 1])
self.y_direction = random.choice([-1, 1])
Game()
arcade.run()
Output:

how to make two particles collide in tkinter python 3?

I'm currently trying to make two particles in a gaseous state collide, but I'm not too sure how to write a code that will make this happen, I have a rough draft of what I could do but I don't really know how I could properly implent it (lines 43-54 i.e the code after the comment #collision of i with another particle j) after the collision occurs I wanted them to go in the opposite direction with different speedssince I will be computing the kinetic energy depending on the mass of the particles. The goal of the project is to basically show the conservation of kinetic energy of multiple particles of differnet parameters(velocity,mass,direction) moving in gaz. Here's my current code, any help would be greatly appreciated!!:
from tkinter import *
from random import *
myHeight=400
myWidth=600
mySpeed=10
x1=5
y=5
radius1=20
x2=7
y2=4
radius1=20
x=60
width=40
length=10
global particules
particules = []
def initialiseBall(dx,dy,radius,color):
b = [myWidth/2,myHeight/2, dx, dy, radius]
particules.append(b)
k = myCanvas.create_oval(myWidth/2-radius, myHeight/2,\
myWidth/2+radius,myHeight/2+radius,\
width=2,fill=color)
print(k)
def updateBalls():
N = len(particules)
for i in range(N):
particules[i][0] += particules [i][2]
particules[i][1] += particules [i][3]
# collision of i with the walls
if particules[i][0]<0 or particules[i][0]>=myWidth:
particules[i][2] *= -1
if particules[i][1]<0 or particules[i][1]>=myHeight:
particules[i][3] *= -1
#collision of i with another particle j
# for j in range(N):
# if j != i:
# compare the position of i and j
# dij = ...
# if dij ... :
#if collision, compute the normal vector
#change velocities
# if particules[i][1]<=particules[i][1]:
# particules[i][2] *= -1
# r = particules[i][4]
myCanvas.coords(i+1, particules[i][0]-particules[i][4],
particules[i][1]-particules[i][4],
particules[i][0]+particules[i][4],
particules[i][1]+particules[i][4])
def animation ():
updateBalls()
myCanvas.after(mySpeed, animation)
def kineticenergy(mass, velocity):
Ec = 1/2 * mass * velocity ** 2
return Ec
# def averagetrip(number, radius):
# #
# #
# #
# #
mainWindow=Tk()
mainWindow.title('particles reservoir')
myCanvas = Canvas(mainWindow, bg = 'grey', height = myHeight, width = myWidth)
myCanvas.pack(side=TOP)
# create 2 particules
initialiseBall(-1,0, 50, 'red')
initialiseBall(1,0, 50, 'blue')
print(particules)
'''
N = 100
for n in range(N):
initialiseBalle(-1 ,0, randint(5,10), 'red')
'''
animation()
mainWindow.mainloop()
Try this:
from math import sqrt, sin, cos
import tkinter as tk
import time
# For more info about this read: https://stackoverflow.com/a/17985217/11106801
def _create_circle(self, x, y, r, **kwargs):
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs)
tk.Canvas.create_circle = _create_circle
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 500
# The coefficient of restitution
# Set to 1 for perfectly elastic collitions
e = 1
class Ball:
def __init__(self, mass:float, r:float, x:float, y:float,
vx:float=0, vy:float=0, **kwargs):
"""
This is a class that defines what a ball is and how it interacts
with other balls.
Arguments:
mass:float # The mass of the ball
------------------------------------------------------
r:float # The radius of the ball, must be >0
------------------------------------------------------
x:float # The x position of the ball
# must be >0 and <WINDOW_WIDTH
y:float # The y position of the ball
# must be >0 and <WINDOW_HEIGHT
------------------------------------------------------
vx:float # The x velocity of the ball
vy:float # The y velocity of the ball
------------------------------------------------------
**kwargs # All of the args to be passed in to `create_circle`
"""
self.m = mass
self.r = r
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.kwargs = kwargs
def display(self, canvas:tk.Canvas) -> int:
"""
Displays the ball on the screen and returns the canvas_id (which
is a normal python int).
"""
canvas_id = canvas.create_circle(self.x, self.y, self.r, **self.kwargs)
return canvas_id
def move(self, all_balls:list, dt:float) -> (float, float):
"""
This moves the ball according to `self.vx` and `self.vy`.
It also checks for collisions with other balls.
Arguments:
all_balls:list # A list of all balls that are in the
# simulation. It can include this ball.
---------------------------------------------------
dt:float # delta time - used to move the balls
Returns:
dx:float # How much the ball has moved in the x direction
dy:float # How much the ball has moved in the y direction
Note: This function isn't optimised in any way. If you optimise
it, it should run faster.
"""
# Check if there are any collitions:
for other, _ in all_balls:
# Skip is `ball` is the same as this ball
if id(other) == id(self):
continue
# Check if there is a collision:
distance_squared = (other.x - self.x)**2 + (other.y - self.y)**2
if distance_squared <= (other.r + self.r)**2:
# Now the fun part - calulating the resultant velocity.
# I am assuming you already know all of the reasoning
# behind the math (if you don't ask physics.stackexchange.com)
# First I will find the normal vector of the balls' radii.
# That is just the unit vector in the direction of the
# balls' radii
ball_radii_vector_x = other.x - self.x
ball_radii_vector_y = other.y - self.y
abs_ball_radii_vector = sqrt(ball_radii_vector_x**2 +\
ball_radii_vector_y**2)
nx = ball_radii_vector_x / abs_ball_radii_vector **2*2
ny = ball_radii_vector_y / abs_ball_radii_vector **2*2
# Now I will calculate the tangent
tx = -ny
ty = nx
""" Only for debug
print("n =", (nx, ny), "\t t =", (tx, ty))
print("u1 =", (self.vx, self.vy),
"\t u2 =", (other.vx, other.vy))
#"""
# Now I will split the balls' velocity vectors to the sum
# of 2 vectors parallel to n and t
# self_velocity = λ*n + μ*t
# other_velocity = a*n + b*t
λ = (self.vx*ty - self.vy*tx) / (nx*ty - ny*tx)
# Sometimes `tx` can be 0 if so we are going to use `ty` instead
try:
μ = (self.vx - λ*nx) / tx
except ZeroDivisionError:
μ = (self.vy - λ*ny) / ty
""" Only for debug
print("λ =", λ, "\t μ =", μ)
#"""
a = (other.vx*ty - other.vy*tx) / (nx*ty - ny*tx)
# Sometimes `tx` can be 0 if so we are going to use `ty` instead
try:
b = (other.vx - a*nx) / tx
except ZeroDivisionError:
b = (other.vy - a*ny) / ty
""" Only for debug
print("a =", a, "\t b =", b)
#"""
self_u = λ
other_u = a
sum_mass_u = self.m*self_u + other.m*other_u
sum_masses = self.m + other.m
# Taken from en.wikipedia.org/wiki/Inelastic_collision
self_v = (e*other.m*(other_u-self_u) + sum_mass_u)/sum_masses
other_v = (e*self.m*(self_u-other_u) + sum_mass_u)/sum_masses
self.vx = self_v*nx + μ*tx
self.vy = self_v*ny + μ*ty
other.vx = other_v*nx + b*tx
other.vy = other_v*ny + b*ty
print("v1 =", (self.vx, self.vy),
"\t v2 =", (other.vx, other.vy))
# Move the ball
dx = self.vx * dt
dy = self.vy * dt
self.x += dx
self.y += dy
return dx, dy
class Simulator:
def __init__(self):
self.balls = [] # Contains tuples of (<Ball>, <canvas_id>)
self.root = tk.Tk()
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=WINDOW_WIDTH,
height=WINDOW_HEIGHT)
self.canvas.pack()
def step(self, dt:float) -> None:
"""
Steps the simulation as id `dt` seconds have passed
"""
for ball, canvas_id in self.balls:
dx, dy = ball.move(self.balls, dt)
self.canvas.move(canvas_id, dx, dy)
def run(self, dt:float, total_time:float) -> None:
"""
This function keeps steping `dt` seconds until `total_time` has
elapsed.
Arguments:
dt:float # The time resolution in seconds
total_time:float # The number of seconds to simulate
Note: This function is just for proof of concept. It is badly written.
"""
for i in range(int(total_time//dt)):
self.step(dt)
self.root.update()
time.sleep(dt)
def add_ball(self, *args, **kwargs) -> None:
"""
Adds a ball by passing all of the args and keyword args to `Ball`.
It also displays the ball and appends it to the list of balls.
"""
ball = Ball(*args, **kwargs)
canvas_id = ball.display(self.canvas)
self.balls.append((ball, canvas_id))
app = Simulator()
app.add_ball(mass=1, r=10, x=20, y=250, vx=100, vy=0, fill="red")
app.add_ball(mass=1, r=10, x=240, y=250, vx=0, vy=0, fill="blue")
app.add_ball(mass=1, r=10, x=480, y=250, vx=0, vy=0, fill="yellow")
app.run(0.01, 10)
That code has a lot of comments describing how it works. If you still have any questions, ask me. Also the move method isn't optimised. The run method isn't great and can throw an error if it's still running and the user closes the window. I will try to find a better approch for that method. If you find any bugs, please let me know. I will try to fix them.

Display a certain element in a list

Right now our code creates a grid starting at the top left and filling in rows and columns from left to right, row by row. Currently, there are a bunch of images it can pick from. It is set using a handful of IF statements that picks between shapes and rareshapes. What I am trying to figure out how to do is change the code so instead of it picking a random rareshape, I can decide what rareshape spawns. Still new to Python and finding a lot of little things that make sense to me from other languages don't work in Python so its throwing me off a little.
EDIT:
Here is the full code. Credit for the base code written by cactusbin and revised by Gareth Rees.
import pygame, random, time, sys
from pygame.locals import *
import itertools
import os
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SHAPE_WIDTH = 64 # Width of each shape (pixels).
SHAPE_HEIGHT = 64 # Height of each shape (pixels).
PUZZLE_COLUMNS = 10 # Number of columns on the board.
PUZZLE_ROWS = 11 # Number of rows on the board.
MARGIN = 118 # Margin around the board (pixels).
WINDOW_WIDTH = PUZZLE_COLUMNS * SHAPE_WIDTH + 2 * MARGIN + 485
WINDOW_HEIGHT = PUZZLE_ROWS * SHAPE_HEIGHT + 2 * MARGIN - 150
FONT_SIZE = 60
TEXT_OFFSET = MARGIN + 950
# Map from number of matches to points scored.
MINIMUM_MATCH = 10
EXTRA_LENGTH_POINTS = .1
RANDOM_POINTS = .3
DELAY_PENALTY_SECONDS = 1
DELAY_PENALTY_POINTS = 0
FPS = 30
EXPLOSION_SPEED = 15 # In frames per second.
SPIN_SPEED = 15
REFILL_SPEED = 10 # In cells per second.
VERTICAL = False
class Cell(object):
"""
A cell on the board, with properties:
`image` -- a `Surface` object containing the sprite to draw here.
`offset` -- vertical offset in pixels for drawing this cell.
"""
def __init__(self, image):
self.offset = 0.0
self.image = image
def tick(self, dt):
self.offset = max(0.0, self.offset - dt * REFILL_SPEED)
class Board(object):
"""
A rectangular board of cells, with properties:
`w` -- width in cells.
`h` -- height in cells.
`size` -- total number of cells.
`board` -- list of cells.
`matches` -- list of matches, each being a list of exploding cells.
`refill` -- list of cells that are moving up to refill the board.
`score` -- score due to chain reactions.
"""
def __init__(self, width, height):
self.explosion = [pygame.image.load('images/explosion{}.png'.format(i))
for i in range(1, 7)]
self.spin = [pygame.image.load('images/powerframe{}.png'.format(i))
for i in range (1, 12)]
self.image_color = {}
self.shapes = []
self.rareshapes = []
colors = 'red blue yellow'
letters = 'acgtu'
for c in colors.split():
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.append(im)
self.image_color[im] = l
self.background = pygame.image.load("images/bg.png")
self.blank = pygame.image.load("images/blank.png")
self.x = pygame.image.load("images/x.png")
self.w = width
self.h = height
self.size = width * height
self.board = [Cell(self.blank) for _ in range(self.size)]
self.matches = []
self.refill = []
self.score = 0.0
self.spin_time = 15
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
rare_shapes = [1, 9, 23, 27, 40, 42, 50, 56, 70, 81, 90]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell(random.choice(self.shapes))
def pos(self, i, j):
"""
Return the index of the cell at position (i, j).
"""
assert(0 <= i < self.w)
assert(0 <= j < self.h)
return j * self.w + i
def busy(self):
"""
Return `True` if the board is busy animating an explosion or a
refill and so no further swaps should be permitted.
"""
return self.refill or self.matches
def tick(self, dt):
"""
Advance the board by `dt` seconds: move rising blocks (if
any); otherwise animate explosions for the matches (if any);
otherwise check for matches.
"""
if self.refill:
for c in self.refill:
c.tick(dt)
self.refill = [c for c in self.refill if c.offset > 0]
if self.refill:
return
elif self.matches:
self.explosion_time += dt
f = int(self.explosion_time * EXPLOSION_SPEED)
if f < len(self.explosion):
self.update_matches(self.explosion[f])
return
self.update_matches(self.blank)
self.refill = list(self.refill_columns())
self.explosion_time = 0
self.matches = self.find_matches()
def draw(self, display):
"""
Draw the board on the pygame surface `display`.
"""
display.blit(self.background, (0, 0))
for i, c in enumerate(self.board):
display.blit(c.image,
(MARGIN + SHAPE_WIDTH * (i % self.w),
MARGIN + SHAPE_HEIGHT * (i // self.w - c.offset) - 68))
display.blit(self.x, (995, 735))
display.blit(self.x, (1112, 735))
display.blit(self.x, (1228, 735))
def swap(self, cursor):
"""
Swap the two board cells covered by `cursor` and update the
matches.
"""
i = self.pos(*cursor)
b = self.board
b[i], b[i+1] = b[i+1], b[i]
self.matches = self.find_matches()
def find_matches(self):
"""
Search for matches (lines of cells with identical images) and
return a list of them, each match being represented as a list
of board positions.
"""
def lines():
for j in range(self.h):
yield range(j * self.w, (j + 1) * self.w)
for i in range(self.w):
yield range(i, self.size, self.w)
def key(i):
return self.image_color.get(self.board[i].image)
def matches():
for line in lines():
for _, group in itertools.groupby(line, key):
match = list(group)
if len(match) >= MINIMUM_MATCH:
yield match
self.score = self.score + 1
return list(matches())
def update_matches(self, image):
"""
Replace all the cells in any of the matches with `image`.
"""
for match in self.matches:
for position in match:
self.board[position].image = image
def refill_columns(self):
"""
Move cells downwards in columns to fill blank cells, and
create new cells as necessary so that each column is full. Set
appropriate offsets for the cells to animate into place.
"""
for i in range(self.w):
target = self.size - i - 1
for pos in range(target, -1, -self.w):
if self.board[pos].image != self.blank:
c = self.board[target]
c.image = self.board[pos].image
c.offset = (target - pos) // self.w
target -= self.w
yield c
offset = 1 + (target - pos) // self.w
for pos in range(target, -1, -self.w):
c = self.board[pos]
c.image = random.choice(self.shapes)
c.offset = offset
yield c
class Game(object):
"""
The state of the game, with properties:
`clock` -- the pygame clock.
`display` -- the window to draw into.
`font` -- a font for drawing the score.
`board` -- the board of cells.
`cursor` -- the current position of the (left half of) the cursor.
`score` -- the player's score.
`last_swap_ticks` --
`swap_time` -- time since last swap (in seconds).
"""
def __init__(self):
pygame.init()
pygame.display.set_caption("Nucleotide")
self.clock = pygame.time.Clock()
self.display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT),
DOUBLEBUF)
self.board = Board(PUZZLE_COLUMNS, PUZZLE_ROWS)
self.font = pygame.font.Font(None, FONT_SIZE)
def start(self):
"""
Start a new game with a random board.
"""
self.board.randomize()
self.cursor = [0, 0]
self.score = 0.0
self.swap_time = 125
def quit(self):
"""
Quit the game and exit the program.
"""
pygame.quit()
sys.exit()
def play(self):
"""
Play a game: repeatedly tick, draw and respond to input until
the QUIT event is received.
"""
self.start()
while True:
self.draw()
dt = min(self.clock.tick(FPS) / 1000, 1 / FPS)
self.swap_time -= dt
for event in pygame.event.get():
if event.type == KEYUP:
self.input(event.key)
elif event.type == QUIT:
self.quit()
elif self.swap_time == 0:
self.quit()
self.board.tick(dt)
def input(self, key):
"""
Respond to the player pressing `key`.
"""
if key == K_q:
self.quit()
elif key == K_RIGHT and self.cursor[0] < self.board.w - 2:
self.cursor[0] += 1
elif key == K_LEFT and self.cursor[0] > 0:
self.cursor[0] -= 1
elif key == K_DOWN and self.cursor[1] < self.board.h - 1:
self.cursor[1] += 1
elif key == K_UP and self.cursor[1] > 0:
self.cursor[1] -= 1
elif key == K_SPACE and not self.board.busy():
self.swap()
def swap(self):
"""
Swap the two cells under the cursor and update the player's score.
"""
self.board.swap(self.cursor)
def draw(self):
self.board.draw(self.display)
self.draw_score()
self.draw_time()
if VERTICAL == False:
self.draw_cursor()
elif VERTICAL == True:
self.draw_cursor2()
pygame.display.update()
def draw_time(self):
s = int(self.swap_time)
text = self.font.render(str(int(s/60)) + ":" + str(s%60).zfill(2),
True, BLACK)
self.display.blit(text, (TEXT_OFFSET, WINDOW_HEIGHT - 170))
def draw_score(self):
total_score = self.score
def draw_cursor(self):
topLeft = (MARGIN + self.cursor[0] * SHAPE_WIDTH,
MARGIN + self.cursor[1] * SHAPE_HEIGHT - 68)
topRight = (topLeft[0] + SHAPE_WIDTH * 2, topLeft[1])
bottomLeft = (topLeft[0], topLeft[1] + SHAPE_HEIGHT)
bottomRight = (topRight[0], topRight[1] + SHAPE_HEIGHT)
pygame.draw.lines(self.display, WHITE, True,
[topLeft, topRight, bottomRight, bottomLeft], 3)
if __name__ == '__main__':
Game().play()
If what you are asking for is a way to more easily specify at which rareshapecount intervals you should place a rare shape instead of a normal shape, the following approach is more readable:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
# locations we want to place a rare shape
rare_shapes = [9, 23, 27]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell (random.choice(self.shapes))
Optionally, you could randomly populate rare_shapes if you don't feel like hardcoding the intervals each time, making for a more varied experience (i.e., if you're designing a game or something similar).
What you mean by "I can decide what rareshape spawns instead of it picking a random rareshape" is unclear to me. Would you care to give more explanations ? Like how you would tell the program which rareshape to use ?
In the meantime, here's a somewhat more pythonic version of your code:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
specials = dict((x, self.rareshapes) for x in (9, 23, 27))
get_shape_source = lambda x: specials.get(x, self.shapes)
for i in xrange(min(self.size, 41)):
self.board[i] = Cell(random.choice(get_shape_source(i)))
Note that this would break if len(self.board) < min(self.size, 41) but well, that's still basically what your current code do.
edit: given your comment, the obvious way to explicitly choose which rareshape goes where is to explicitly associate images with spots. Now what's the best way to do so / the best place ton configure this really depends on your whole code or at least on more than what you posted. As a very simple and minimal exemple, you could just have this:
from collections import ordereddict
def load_images(self)
self.image_color = {}
self.shapes = []
self.rareshapes = ordereddict()
colors = 'red', 'blue', 'yellow'
letters = 'acgtu'
for c in colors:
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.[(c, l)] = im
self.image_color[im] = l
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
raremap = {
# spot index : rareshape
9: ('red', 'a')],
23: ('blue', 'u'),
27: ('yellow', 'g')
}
for i in xrange(self.size):
if i in raremap:
im = self.rareshapes[raremap[i]]
else:
im = random.choice(self.shapes)
self.board[i] = Cell(im)
But it will be just unmaintainable in the long run - too much hardcoded stuff, and too much knowledge leaking from one method to another. I don't know what 'self' is an instance of, but you should considered splitting the responsabilities so you have the invariant parts in one class and the "configration" (images to load, spots / rareshapes mapping etc) in another. Some design patterns that come to mind are TemplateMethod (where you have an abstract base class with the invariant parts and concrete subclasses implementing the "configuration" part), Builder, and of course Strategy (in your case the "Strategy" class would take care of the configuration).

position += heading*distance_moved TypeError: can only concatenate tuple (not "Vector") to tuple

*although all parts of that operation are tuples python seems to think that in this instance one of them is not. This is my first time trying to make a vector class in python. my intentions are to move my simple mouse image to where i click on the screen by adding increments to it of a speed * vector to its position until it reaches the target distance*
import math
class Vector(object):
#defaults are set at 0.0 for x and y
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
#allows us to return a string for print
def __str__(self):
return "(%s, %s)"%(self.x, self.y)
# from_points generates a vector between 2 pairs of (x,y) coordinates
#classmethod
def from_points(cls, P1, P2):
return cls(P2[0] - P1[0], P2[1] - P1[1])
#calculate magnitude(distance of the line from points a to points b
def get_magnitude(self):
return math.sqrt(self.x**2+self.y**2)
#normalizes the vector (divides it by a magnitude and finds the direction)
def normalize(self):
magnitude = self.get_magnitude()
self.x/= magnitude
self.y/= magnitude
#adds two vectors and returns the results(a new line from start of line ab to end of line bc)
def __add__(self, rhs):
return Vector(self.x +rhs.x, self.y+rhs.y)
#subtracts two vectors
def __sub__(self, rhs):
return Vector(self.x - rhs.x, self.y-rhs.y)
#negates or returns a vector back in the opposite direction
def __neg__(self):
return Vector(-self.x, -self.y)
#multiply the vector (scales its size) multiplying by negative reverses the direction
def __mul__(self, scalar):
return Vector(self.x*scalar, self.y*scalar)
#divides the vector (scales its size down)
def __div__(self, scalar):
return Vector(self.x/scalar, self.y/scalar)
def points(self):
return (self.x, self.y)
#The simple mouse move game by Ramon Cabral
#imports
import pygame, sys, Vector
from pygame.locals import *
from Vector import *
#game init
pygame.init()
#screen
screen = pygame.display.set_mode((800,600),0,32)
#images
mouse_file = 'mouse.png'
MOUSE = pygame.image.load(mouse_file).convert_alpha()
#variables
bgcolor = (255,255,255)
position = (100.0, 100.0)
heading = Vector(0, 0)
#clock and speed
clock = pygame.time.Clock()
speed = 250.0
#main game function
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
if event.type == MOUSEBUTTONDOWN:
destination = pygame.mouse.get_pos()
heading = Vector.from_points(position, destination)
heading.normalize()
screen.fill(bgcolor)
screen.blit(MOUSE, position)
time_passed = clock.tick(30.)
time_passed_seconds = time_passed/1000.0
distance_moved = time_passed_seconds*speed
position += heading*distance_moved
pygame.display.update()
You have to define the getitem and setitem method to support indexing in your Vector Class.
It looks like you're passing Vector.from_points a Vector object, when it wants a tuple of numbers. Have you tried something like this?
position_points = (position.x, position.y)
heading = Vector.from_points(position_points, destination)
I wouldn't recommend making Vector support indexing. That's usually reserved for list-like objects. It's not super clear what Vector()[0] and Vector()[1] should be. Vector().x is Vector().y are much clearer for me.
If you find that you frequently (read: "more than once") need to treat a vector as a tuple of points, you could make an instance method to do that:
class Vector(object):
# ...
def points(self):
return (self.x, self.y)
# ...

re: position += heading*distance_moved TypeError: can only concatenate tuple (not “Vector”) to tuple

*although all parts of that operation are tuples python seems to think that in this instance one of them is not. This is my first time trying to make a vector class in python. my intentions are to move my simple mouse image to where i click on the screen by adding increments to it of a speed * vector to its position until it reaches the target distance*
import math
class Vector(object):
#defaults are set at 0.0 for x and y
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
#allows us to return a string for print
def __str__(self):
return "(%s, %s)"%(self.x, self.y)
# from_points generates a vector between 2 pairs of (x,y) coordinates
#classmethod
def from_points(cls, P1, P2):
return cls(P2[0] - P1[0], P2[1] - P1[1])
#calculate magnitude(distance of the line from points a to points b
def get_magnitude(self):
return math.sqrt(self.x**2+self.y**2)
#normalizes the vector (divides it by a magnitude and finds the direction)
def normalize(self):
magnitude = self.get_magnitude()
self.x/= magnitude
self.y/= magnitude
#adds two vectors and returns the results(a new line from start of line ab to end of line bc)
def __add__(self, rhs):
return Vector(self.x +rhs.x, self.y+rhs.y)
#subtracts two vectors
def __sub__(self, rhs):
return Vector(self.x - rhs.x, self.y-rhs.y)
#negates or returns a vector back in the opposite direction
def __neg__(self):
return Vector(-self.x, -self.y)
#multiply the vector (scales its size) multiplying by negative reverses the direction
def __mul__(self, scalar):
return Vector(self.x*scalar, self.y*scalar)
#divides the vector (scales its size down)
def __div__(self, scalar):
return Vector(self.x/scalar, self.y/scalar)
def points(self):
return (self.x, self.y)
#imports
import pygame, sys, Vector
from pygame.locals import *
from Vector import *
#game init
pygame.init()
#screen
screen = pygame.display.set_mode((800,600),0,32)
#images
mouse_file = 'mouse.png'
MOUSE = pygame.image.load(mouse_file).convert_alpha()
#variables
bgcolor = (255,255,255)
position = (100.0, 100.0)
heading = Vector(0, 0)
#clock and speed
clock = pygame.time.Clock()
speed = 250.0
#main game function
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
if event.type == MOUSEBUTTONDOWN:
destination = pygame.mouse.get_pos()
heading = Vector.from_points(position, destination)
heading.normalize()
screen.fill(bgcolor)
screen.blit(MOUSE, position)
time_passed = clock.tick(30.)
time_passed_seconds = time_passed/1000.0
distance_moved = time_passed_seconds*speed
position += heading*distance_moved
pygame.display.update()
Here's a problem:
position = (100.0, 100.0)
You need position = Vector(100.0, 100.0) in order to do position += heading * distance_moved, since otherwise you will be invoking the tuple +=.
(Some folks don't like storing "points" in Vector classes, so you may want a second Point class if you are of that inclination.)

Categories