Skip to content
Snippets Groups Projects
Commit 1a7f2483 authored by Will Billingsley's avatar Will Billingsley
Browse files

Created assignment from cut-down solution

parents
No related branches found
No related tags found
No related merge requests found
*.class
*.log
# Maven/sbt output directory
target
dist
# Test database
lift_example
# Mac
*.DS_Store
# IntelliJ
.idea
.idea_modules
# Eclipse
.cache
.classpath
*.swp
.project
.settings
.target
# Vagrant
.vagrant
vagrant/.bashrc
vagrant/sbt-launch.jar
tmp
README.md 0 → 100644
# Assignment 2: Mixing functional and imperative code
Previously, we've talked about how we can describe a changing system
as a series of immutable states. We're going to do this to implement
a simulation.
This is going to cause you to write:
* Functional code for how you produce the next state from the current
state
* A small number of mutable structures using imperative code for remembering the states
and controlling the simulation. (And painting it.)
This year, the simulation is *boids*.
## Boids
Boids ("bird-oids") is a simulation of flocking introduced in a paper by
Craig Reynolds in 1987. You can see an online version of how these boids
behave on the Processing site, here:
https://processing.org/examples/flocking.html
Each boid has a position and a velocity. At each step, the boid will
move according to its velocity
newPosition = position + velocity
And change velocity based on a calculated acceleration
newVelocity = velocity + acceleration
Note that there's no measure of how long the timestep is.
The acceleration is generally calculated as a mix of three forces for each
boid:
* Separation - if other boids are too close, it will try to move away from them
* Alignment - it will try to match the velocity and direction of other boids within 50 pixels
* Cohesion - it will try to steer towards the middle of its neighbours (where "neighbours" is all boids within 50 pixels)
Boids have a maximum velocity, and a maximum acceleration they can apply when steering
In our assignment 2, you're going to be implementing an augmented boids
simulation using a mixture of functional and imperative programming.
## Mixing functional and imperative programming
Your aim is to keep your code as functional as you can, but you're going
to be led into having a few places where there are imperative concepts too.
For example, your simulation is going to keep an action replay memory.
This is going to require you to store past states of the simulation as
you've played them -- something that works well with the *current state*
being an immutable `Seq` of immutable `Boids`.
But the memory itself uses a mutable data structure
(I've picked `mutable.Queue`) so that it can fill over time, and reset
when you click a button
There are also various mutable properties that we're going to add to our
simulation:
* The wind
* The ability to "startle" the boids by applying a random impulse to
each boid in the next frame
* The ability to tell the simulation that at the next frame, it should
add a boid
## What's provided
I have provided most of the classes, but I've stripped out some of their
implementation.
Fully implemented, you have:
* `Vec2` -- a class that can do vector addition and multiplication. So
that if you say `newPosition = position + velocity` it just works.
* `BoidsPanel` -- a panel that knows how to draw a `Seq[Boid]`
Partially implemented, you have:
* `Boid` -- an immutable representation of a Boid. You will need to
work functionally to produce new sequences of `Boid`
* `Simulation` -- a class for storing the simulation states. This
contains a mutable queue for the simulation memory, and some
mutable variables that let you alter what happens on the next
simulation step
* `BoidsApp` -- the runnable program, that creates a window, and adds the
`BoidsPanel` and some buttons. Note that the buttons' event handlers
have not all yet been implemented.
## What the components need to do
* The **Action Replay** button must take the simulation back to the start of
the memory buffer (typically one second) and let the simulation continue
again from there.
* The **wind** buttons should set the wind strength and direction. As the
wind is the medium the boids are flying in, we add the wind force *after*
we've limited the boid's velocity. So it is possible to fly faster downwind
than up.
* The **Startle boids** button should cause the simulation, at the next time
step, to perturb each boid by a velocity of `startleStrength` in a random
direction (a different random direction for each boid)
* The **Regenesis** button should push a frame into the queue with `numBoids` boids heading from the
centre in random direction. Note this shoud *not* clear the queue -- hitting
**Action Replay** quickly after hitting **Regenesis** should jump back
into one of the old states.
* Clicking a point on the canvas should add a new boid into the simulation,
heading in a random direction with a velocity of 1
## Note on the provided code
I've provided this code by writing a solution and then cutting back from it.
But it's possible you might not like some parts of my code. You can delete
and change *any code you like* -- your task is to write the simulation, not
to retain my code.
## Marking
The marking is aimed to be able to be done quickly, with the written feedback being
more formative and open-ended.
Functionality:
* The boids simulation works: 7
* Adding a boid by clicking the canvas works: 1
* Wind works: 1
* Startle works: 1
* Regenesis works: 1
* Action replay works: 1
Quality: 50%
* Boid uses functional rather than imperative code: 3
* Overall quality judgment (readable, tidy, concise): 5
\ No newline at end of file
lazy val root = (project in file(".")).
settings(
name := "firststeps",
version := "1.0",
scalaVersion := "2.12.1"
)
libraryDependencies += "org.scalafx" %% "scalafx" % "8.0.144-R12"
libraryDependencies += "org.typelevel" %% "squants" % "1.3.0"
libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.1"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
sbt.version=0.13.13
sbt 0 → 100755
#!/bin/bash
SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"
java $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"
File added
set SCRIPT_DIR=%~dp0
java -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -jar "%SCRIPT_DIR%sbt-launch.jar" %*
\ No newline at end of file
package cosc250.boids
/**
* A boid (bird-oid). It has a position and a velocity.
*
*
* https://processing.org/examples/flocking.html
*/
case class Boid(
position:Vec2, velocity:Vec2
) {
/**
* Calculates an acceleration vector that will cause it to maintain a minimum
* separation from its closest neighbours
* This steer is limited to maxForce
*/
def separate(others:Seq[Boid]):Vec2 = {
???
}
/**
* Calculates an acceleration vector that will cause it align its direction and
* velocity with other birds within Boid.neighbourDist
* This alignment force is limited to maxForce
*/
def align(others:Seq[Boid]):Vec2 = {
???
}
/**
* Calculates an acceleration that will steer this boid towards the target.
* The steer is limited to maxForce
*/
def seek(targetPos:Vec2):Vec2 = {
???
}
/**
* Calculates an acceleration that will keep it near its neighbours and maintain
* the flock cohesion
*/
def cohesion(others:Seq[Boid]):Vec2 = {
???
}
/**
* Calculates a flocking acceleration that is a composite of its separation,
* align, and cohesion acceleration vectors.
*/
def flock(others:Seq[Boid]):Vec2 = {
???
}
/**
* Produces a new Boid by adding the boid's velocity to its position, and adding
* the acceleration vector to the boid's velocity. Note that there is no division
* by timestep -- it's just p = p + v, and v = v + a
*
* Also note that we don't apply the limiting on maxForce in this function -- this is
* so that the startle effect can dramatically perturb the birds in a way they would
* not normally be perturbed in flight. Instead, limit maxForce in the flock function
* (or the functions it calls)
*
* We do, however, limit a boid's velocity to maxSpeed in this function. But we do it
* *before* we add the influence of the wind to the boid's velocity -- it's possible
* to fly faster downwind than upwind.
*/
def update(acceleration:Vec2, wind:Vec2):Boid = {
???
}
def wrapX(x:Double):Double = {
if (x > Boid.maxX) x - Boid.maxX else if (x < 0) x + Boid.maxX else x
}
def wrapY(y:Double):Double = {
if (y > Boid.maxY) y - Boid.maxY else if (y < 0) y + Boid.maxY else y
}
}
object Boid {
/** How far apart the boids want to be */
val desiredSeparation = 25
/** Maximum flying velocity of a boid */
val maxSpeed = 2
/** maximum accelaration of a boid */
val maxForce = 0.03
/** Other boids within this range are considered neighbours */
val neighBourDist = 50
/** Wrap width of the simulation. ie, for any Boid, 0 <= x < 640 */
def maxX:Int = Simulation.width
/** Wrap height of the simulation. ie, for any Boid, 0 <= y < 480 */
def maxY:Int = Simulation.height
}
package cosc250.boids
import java.awt.event.{MouseAdapter, MouseEvent, MouseListener}
import java.awt.{BorderLayout, Dimension, FlowLayout, GridLayout}
import javax.swing._
import scala.collection.mutable
import scala.util.Random
object BoidsApp {
/** The main window */
val window = new JFrame()
/** A panel to render our boids */
val boidsPanel = new BoidsPanel
/** Action replay button */
val replay = new JButton("Action Replay")
val nwWind = new JButton("<html>&#x2198;</html>")
val nWind = new JButton("<html>&darr;</html>")
val neWind = new JButton("<html>&#x2199;</html>")
val wWind = new JButton("<html>&rarr;</html>")
val stopWind = new JButton("<html>&times;</html>")
val eWind = new JButton("<html>&larr;</html>")
val swWind = new JButton("<html>&#x2197;</html>")
val sWind = new JButton("<html>&uarr;</html>")
val seWind = new JButton("<html>&#x2196;</html>")
val startle = new JButton("Startle boids")
val regenesis = new JButton("Regenesis")
def main(args:Array[String]):Unit = {
val container = new JPanel()
container.setLayout(new BorderLayout())
container.add(boidsPanel, BorderLayout.CENTER)
val controlsContainer = Box.createVerticalBox()
val windPanel = new JPanel()
windPanel.setLayout(new GridLayout(3, 3))
windPanel.add(nwWind)
windPanel.add(nWind)
windPanel.add(neWind)
windPanel.add(wWind)
windPanel.add(stopWind)
windPanel.add(eWind)
windPanel.add(swWind)
windPanel.add(sWind)
windPanel.add(seWind)
windPanel.setAlignmentX(0)
val windStrength = Simulation.windStrength
nwWind.addActionListener((_) => Simulation.setWindDirection(Vec2.NW))
nWind.addActionListener((_) => Simulation.setWindDirection(Vec2.N))
neWind.addActionListener((_) => Simulation.setWindDirection(Vec2.NE))
wWind.addActionListener((_) => Simulation.setWindDirection(Vec2.W))
stopWind.addActionListener((_) => Simulation.wind=None)
eWind.addActionListener((_) => Simulation.setWindDirection(Vec2.E))
swWind.addActionListener((_) => Simulation.setWindDirection(Vec2.SW))
sWind.addActionListener((_) => Simulation.setWindDirection(Vec2.S))
seWind.addActionListener((_) => Simulation.setWindDirection(Vec2.SE))
val wc = new JLabel("Wind controls")
wc.setAlignmentX(0)
controlsContainer.add(wc)
controlsContainer.add(windPanel)
controlsContainer.add(new JLabel("Actions"))
controlsContainer.add(replay)
controlsContainer.add(startle)
controlsContainer.add(regenesis)
controlsContainer.add(new JPanel())
container.add(controlsContainer, BorderLayout.EAST)
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
window.add(container)
window.setSize(container.getPreferredSize)
window.setVisible(true)
Simulation.pushState(Simulation.explosionOfBoids(Simulation.numBoids))
replay.addActionListener({ (evt) =>
Simulation.resetQueue()
})
startle.addActionListener({ (evt) =>
Simulation.oneTimeFunction = Some(Simulation.startleFunction)
})
regenesis.addActionListener({ (evt) =>
Simulation.pushState(Simulation.explosionOfBoids(Simulation.numBoids))
})
boidsPanel.addMouseListener(new MouseAdapter {
override def mouseClicked(e: MouseEvent):Unit = {
Simulation.pushBoid(Boid(Vec2(e.getX, e.getY), Vec2.randomDir(1)))
}
})
val timer = new Timer(16, (e) => {
boidsPanel.setBoids(Simulation.update())
})
timer.start()
}
}
package cosc250.boids
import java.awt._
import javax.swing.JPanel
/**
* A panel that knows how to paint a sequence of Boids
*
* Note, this is a JPanel and has methods such as addMouseListener
*/
class BoidsPanel extends JPanel {
val backgroundColour:Color = Color.DARK_GRAY
/**
* Updates the sequence of boids that will be painted, and queues a repaint to happen on the next render cycle
*/
def setBoids(bs:Seq[Boid]) = {
boids = bs
this.repaint()
}
/** Contains the boids we will be rendering on the next render cycle */
private var boids:Seq[Boid] = Seq.empty
override def getMinimumSize = new Dimension(Simulation.width, Simulation.height)
override def getPreferredSize = new Dimension(Simulation.width, Simulation.height)
override def getMaximumSize = new Dimension(Simulation.width, Simulation.height)
/** Paints our boids */
override def paintComponent(gAwt:Graphics): Unit = {
val g = gAwt.asInstanceOf[Graphics2D]
/** A boid is a triangle */
def boidShape:Shape = {
new Polygon(Array(-4, 4, -4), Array(-3, 0, 3), 3)
}
/** Paints a single boid */
def paintBoid(b:Boid):Unit = {
// Remember the old graphics transform so we can reset it
val xform = g.getTransform
// Set the transform to the boid's position, pointed in the direction of the
// boid's velocity
val Vec2(bx, by) = b.position
g.translate(bx, by)
g.rotate(b.velocity.theta)
// draw a small triangle
g.setColor(Color.WHITE)
g.draw(boidShape)
// Reset the graphics transform
g.setTransform(xform)
}
// Clear the backgound
g.setColor(backgroundColour)
g.fillRect(0, 0, getWidth, getHeight)
// Paint every boid
boids.foreach(paintBoid)
}
}
package cosc250.boids
import scala.collection.mutable
object Simulation {
/** Wrap width of the simulation. ie, for any Boid, 0 <= x < 640 */
val width = 640
/** Wrap height of the simulation. ie, for any Boid, 0 <= y < 480 */
val height = 480
/** How many frames of the simulation to hold */
val frameMemory = 60
/** How manby boids to start with in the simulation */
val numBoids = 150
/** When the wind is blowing, how strongly it blows */
val windStrength = 0.03
/** When the boids are startled, the strength of the vector that is applied to each of them */
val startleStrength:Double = Boid.maxSpeed
val startleFunction:Boid => Vec2 = {
???
}
/** A mutable queue containing the last `frameMemory frames` */
val queue:mutable.Queue[Seq[Boid]] = mutable.Queue.empty[Seq[Boid]]
/** The wind -- an optional acceleration vector */
var wind:Option[Vec2] = None
/**
* Sets a wind blowing at windStrength, at this angle.
* Note that a northerly wind blows **from** the north, so we multiply the vector by -1.
*/
def setWindDirection(theta:Double):Unit = {
???
}
/** A container that can hold a boid to add on the next frame */
var insertBoid:Option[Boid] = None
/**
* A function that will run for the next frame only over each boid in the system,
* producing an acceleration vector to add to a Boid
*/
var oneTimeFunction:Option[Boid => Vec2] = None
/**
* Resets the events that should occur one time only
*/
def resetOneTimeEvents():Unit = {
???
}
/** The current frame */
def current = ???
/** Generates boids in the centre of the simulation, moving at v=1 in a random direction */
def explosionOfBoids(i:Int):Seq[Boid] = ???
/** Pushes a state into the queue */
def pushState(boids:Seq[Boid]):Seq[Boid] = {
???
// Drops a frame from the queue if we've reached the maximum number of frames to remember
if (queue.lengthCompare(frameMemory) > 0) queue.dequeue()
boids
}
/** Called by a click to the canvas, to say that in the next frame, a boid should be inserted */
def pushBoid(b:Boid):Unit = {
insertBoid = Some(b)
}
/** Called by the Action Replay button to jump back in the memory buffer */
def resetQueue():Seq[Boid] = {
???
}
/** Generate the next frame in the simulation */
def update():Seq[Boid] = {
???
}
}
package cosc250.boids
import scala.util.Random
/**
* A Vec2 contains an x and a y value
*
* This could represent an (x,y) position, or an (vx, vy) velocity,
* or an (ax, ay) acceleration
*
* Vector addition and subtraction are implemented for you.
* As is multiplication by a scalar (by a number)
*
* Also implemented is converting to and from "r, theta" notation where
* instead of an x and y value, you have a magnitude and an angle.
*
* You might find reading the test spec useful for examples of how to call these
* functions
*
* @param x
* @param y
*/
case class Vec2(x:Double, y:Double) {
/** Vector addition -- adds this Vec2 to another */
def +(v:Vec2) = Vec2(x + v.x, y + v.y)
/** Vector subtraction -- subtracts the other vector from this one */
def -(v:Vec2) = Vec2(x - v.x, y - v.y)
/**
* Returns the angle this vector makes from the origin.
* Imagine this vector is an arrow on graph-paper from (0,0) to (x,y).
* Theta is the angle from the x axis.
*/
def theta:Double = Math.atan2(y, x)
/**
* Returns the magnitude of this vector
* Imagine this vector is an arrow on graph-paper from (0,0) to (x,y).
* magnitude is the length of the arrow
* @return
*/
def magnitude:Double = Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
/**
* Multiplication by a scalar. Multiplies the x and y values by d.
* eg, Vec2(7,9) * 2 == Vec2(14, 18)
*/
def *(d:Double) = Vec2(x * d, y * d)
/**
* Division by a scalar. Divides the x and y values by d.
* eg, Vec2(14, 18) / 2 == Vec2(7, 9)
*/
def /(d:Double):Vec2 = *(1/d)
/**
* Returns a vector that has the same angle (theta) but a magnitude (arrow length)
* of 1
* @return
*/
def normalised:Vec2 = Vec2.fromRTheta(1, theta)
/**
* If the magnitude is greater than mag, returns a vector that has the same angle
* (theta) but a magnitude (arrow length) of mag.
*
* Otherwise, if this vector is shorter than mag, just returns this vector.
* @param mag
* @return
*/
def limit(mag:Double):Vec2 = {
if (magnitude > mag) {
Vec2.fromRTheta(mag, theta)
} else this
}
}
/**
* Companion object for Vec2. Contains functions for creating Vec2s.
*/
object Vec2 {
/**
* Takes an angle (theta) and a length (r), and returns a Vec2 representing that
* arrow. Note that theta is measured in radians (there are 2 * PI radians in a
* circle rather than 360 degrees in a circle).
*
* @param r
* @param theta
* @return
*/
def fromRTheta(r:Double, theta:Double):Vec2 = {
Vec2(r * Math.cos(theta), r * Math.sin(theta))
}
/** A vector of magnitude d in a random direction */
def randomDir(d:Double):Vec2 = {
val theta = Random.nextDouble() * Math.PI * 2
Vec2.fromRTheta(d, theta)
}
val E:Double = 0
val SE:Double = Math.PI / 4
val S:Double = Math.PI / 2
val SW:Double = 3 * Math.PI / 4
val W:Double = Math.PI
val NW:Double = 5 * Math.PI / 4
val N:Double = 3 * Math.PI / 2
val NE:Double = 7 * Math.PI / 4
}
package cosc250.boids
import org.scalatest._
/**
*
*/
class Vec2Spec extends FlatSpec with Matchers {
"Vec2" should "be able to add vectors" in {
Vec2(1, 2) + Vec2(3, 4) should be (Vec2(4, 6))
}
it should "be able to subtract vectors" in {
Vec2(8, 9) - Vec2(3, 4) should be (Vec2(5, 5))
}
it should "be able to multiply a vector by a number" in {
Vec2(8, 9) * 4 should be (Vec2(32, 36))
}
it should "be able to divide a vector by a number" in {
Vec2(8, 6) / 2 should be (Vec2(4, 3))
}
it should "limit the size of a vector" in {
Vec2(10, 0).limit(5) should be (Vec2(5, 0))
}
it should "calculcate vectors from a direction and an angle in radians" in {
val Vec2(x, y) = Vec2.fromRTheta(4, Math.PI)
// There could be a rounding error -- these are doubles, so floor them as
// we know where Math.PI should go
Vec2(x.floor, y.floor) should be (Vec2(-4, 0))
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment