Asteroids 22: Another defect, can you believe it???
I’ve made yet another mistake. Will the embarrassment never end?
A couple of readers on the Codea forum discovered a defect in this morning’s release: it no longer accumulates the score. Curiously enough, the program doesn’t halt on an error. Curiously also, I didn’t notice the score not adding up, even though the score line is of enormous size.
The defect is due to two mistakes. First:
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
Score = Score + inc
end
Should refer to the new score in Universe, i.e. like this:
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
U.score = U.score + inc
end
But why didn’t that give us an error? We removed the global definition of Score.
Except that we didn’t. In Main:
function setup()
U = Universe()
U:createAsteroids()
Score = 0
end
Lua defaults variables to global, so this statement served to create the global, and then the scoreAsteroid
function blithely used it, while drawScore
correctly referred to the variable in Universe, which never got incremented:
function Universe:drawScore()
local s= "000000"..tostring(self.score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
end
You’d think I’d have to be blind to miss the fact that the score wasn’t ticking up, but I did in fact miss it. I can imagine a test that would have detected it, something like this:
_:test("Asteroids increment score", function()
local a = Asteroid()
U.score = 0
scoreAsteroid(a)
_:expect(U.score).is(20)
end)
That test runs. It’ll probably have to be changed when scoreAsteroid
moves into the Asteroid
object, but that’s OK.
A more important issue is that the tests aren’t very comprehensive, and so they don’t add much to my confidence, so I don’t always run them. That’s pretty foolish but also pretty typical of people if the tests aren’t very helpful. I’d be wise to try to beef them up, make them more valuable and get in the habit of using them.
Looking at setup, we see another issue:
function setup()
U = Universe()
U:createAsteroids()
end
Why is setup creating the Asteroids? That should be one of the jobs of Universe. We have another couple of related issues before us as well. First, we pretty much have to start the whole program over to start a new game. If we go to a two-player version, we’ll have to arrange things differently. This would also come up if we build an “attract mode” that runs when the game is waiting for a player. And when the player kills all the asteroids, we need to start a new “wave”, and each wave gets more and more asteroids added, up to a limit of something like eleven.
So we’ll have some reorganizing to do.
And before that, we should arrange the draw cycle to do draw-update-check rather than the current oddly interleaved code we have now.
All that is for another day. Commit: “scoring wasn’t working”.
Here’s a version of the program without the scoring defect:
--# Main
-- Asteroids
-- RJ 20200511
Touches = {}
function setup()
U = Universe()
U:createAsteroids()
end
function draw()
U:draw()
end
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
--# TestAsteroids
-- TestAsteroids
-- RJ 20200511
function testAsteroids()
CodeaUnit.detailed = true
_:describe("Asteroids First Tests", function()
_:before(function()
-- Some setup
end)
_:after(function()
-- Some teardown
end)
_:test("Hookup", function()
_:expect( 2+1 ).is(3)
end)
_:test("Random", function()
local min = 100
local max = 0
for i = 0,1000 do
local rand = math.random()*2*math.pi
if rand < min then min = rand end
if rand > max then max = rand end
end
_:expect(min < 0.01).is(true)
_:expect(max > 6.2).is(true)
end)
_:test("Rotated Length", function()
for i = 0, 1000 do
local rand = math.random()*2*math.pi
local v = vec2(1.5,0):rotate(rand)
local d = v:len()
_:expect(d > 1.495).is(true)
_:expect(d < 1.505).is(true)
end
end)
_:test("Some rotates go down", function()
local angle = math.rad(-45)
local v = vec2(1,0):rotate(angle)
local rvx = v.x*1000//1
local rvy = v.y*1000//1
_:expect(rvx).is(707)
_:expect(rvy).is(-708)
end)
_:test("Bounds function", function()
_:expect(U:keepInBounds(100,1000)).is(100)
_:expect(U:keepInBounds(1000,1000)).is(0)
_:expect(U:keepInBounds(1001,1000)).is(1)
_:expect(U:keepInBounds(-1,1000)).is(999)
end)
_:test("Missile fired at rest", function()
local ship = Ship()
local missile = Missile(ship)
_:expect(missile.step).is(U.missileVelocity)
end)
_:test("Missile fired north", function()
local ship = Ship()
ship.radians = math.pi/2
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(0, 0.001)
_:expect(my).is(U.missileVelocity.x, 0.001)
end)
_:test("Missile fired from moving ship", function()
local ship = Ship()
ship.step = vec2(1,2)
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(U.missileVelocity.x + 1, 0.001)
_:expect(my).is(U.missileVelocity.y + 2, 0.001)
end)
_:test("Asteroids increment score", function()
local a = Asteroid()
U.score = 0
scoreAsteroid(a)
_:expect(U.score).is(20)
end)
end)
end
--# Shapes
RR1 = {
vec4(0.000000, 2.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 3.000000, 0.000000),
vec4(3.000000, 0.000000, 4.000000, -2.000000),
vec4(4.000000, -2.000000, 1.000000, -4.000000),
vec4(1.000000, -4.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, -4.000000, -2.000000),
vec4(-4.000000, -2.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}
RR2 = {
vec4(2.000000, 1.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, 0.000000, 3.000000),
vec4(0.000000, 3.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -3.000000, 0.000000),
vec4(-3.000000, 0.000000, -4.000000, -2.000000),
vec4(-4.000000, -2.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, -1.000000, -3.000000),
vec4(-1.000000, -3.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -1.000000),
vec4(4.000000, -1.000000, 2.000000, 1.000000)
}
RR3 = {
vec4(-2.000000, 0.000000, -4.000000, -1.000000),
vec4(-4.000000, -1.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, 0.000000, -1.000000),
vec4(0.000000, -1.000000, 0.000000, -4.000000),
vec4(0.000000, -4.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -1.000000),
vec4(4.000000, -1.000000, 4.000000, 1.000000),
vec4(4.000000, 1.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, -1.000000, 4.000000),
vec4(-1.000000, 4.000000, -4.000000, 1.000000),
vec4(-4.000000, 1.000000, -2.000000, 0.000000)
}
RR4 = {
vec4(1.000000, 0.000000, 4.000000, 1.000000),
vec4(4.000000, 1.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 1.000000, 4.000000),
vec4(1.000000, 4.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, -1.000000, 2.000000),
vec4(-1.000000, 2.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -4.000000, -1.000000),
vec4(-4.000000, -1.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, 1.000000, -3.000000),
vec4(1.000000, -3.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -2.000000),
vec4(4.000000, -2.000000, 1.000000, 0.000000)
}
Rocks = {RR1,RR2,RR3,RR4}
--# Ship
-- Ship
-- RJ 20200520
Ship = class()
local rotationStep = math.rad(1) -- one degree in radians
function Ship:init()
self.pos = vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
end
function Ship:draw()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
function Ship:move()
if U.button.left then self.radians = self.radians + rotationStep end
if U.button.right then self.radians = self.radians - rotationStep end
if U.button.fire then if not self.holdFire then self:fireMissile() end end
if not U.button.fire then self.holdFire = false end
self:actualShipMove()
end
function Ship:actualShipMove()
if U.button.go then
local accel = vec2(0.015,0):rotate(self.radians)
self.step = self.step + accel
self.step = maximize(self.step, 3)
end
self:finallyMove()
end
function Ship:finallyMove()
U:moveObject(self)
end
function maximize(vec, size)
local s = vec:len()
if s <= size then
return vec
else
return vec*size/s
end
end
function Ship:fireMissile()
self.holdFire = true
Missile(self)
end
--# Button
-- Button
-- RJ 20200520
local Buttons = {}
function createButtons()
local dx=50
local dy=200
table.insert(Buttons, {x=dx, y=dy, name="left"})
table.insert(Buttons, {x=dy, y=dx, name="right"})
table.insert(Buttons, {x=WIDTH-dx, y=dy, name="fire"})
table.insert(Buttons, {x=WIDTH-dy, y=dx, name = "go"})
end
function checkButtons()
U.button.left = false
U.button.right = false
U.button.go = false
U.button.fire = false
for id,touch in pairs(Touches) do
for i,button in ipairs(Buttons) do
if touch.pos:dist(vec2(button.x,button.y)) < 50 then
U.button[button.name]=true
end
end
end
end
function drawButtons()
pushStyle()
ellipseMode(RADIUS)
textMode(CENTER)
stroke(255)
strokeWidth(1)
for i,b in ipairs(Buttons) do
pushMatrix()
pushStyle()
translate(b.x,b.y)
if U.button[b.name] then
fill(128,0,0)
else
fill(128,128,128,128)
end
ellipse(0,0, 50)
fill(255)
fontSize(30)
text(b.name,0,0)
popStyle()
popMatrix()
end
popStyle()
end
--# Asteroid
-- Asteroid
-- RJ 20200520
local DeadAsteroids = {}
local Vel = 1.5
Asteroid = class()
function Asteroid:init()
self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
self.shape = Rocks[math.random(1,4)]
self.scale = 16
local angle = math.random()*2*math.pi
self.step = vec2(Vel,0):rotate(angle)
end
function Asteroid:killDist()
local s = self.scale
if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end
function killDeadAsteroids(asteroids)
for k,a in pairs(DeadAsteroids) do
asteroids[a] = nil
end
DeadAsteroids = {}
end
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
U.score = U.score + inc
end
function splitAsteroid(asteroid, asteroids)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
return
end
asteroid.scale = asteroid.scale//2
asteroid.angle = math.random()*2*math.pi
local new = Asteroid()
new.pos = asteroid.pos
new.scale = asteroid.scale
asteroids[new] = new
Splat(asteroid.pos)
end
function Asteroid:draw()
pushMatrix()
pushStyle()
translate(self.pos.x, self.pos.y)
scale(self.scale)
strokeWidth(1/self.scale)
for i,l in ipairs(self.shape) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
end
function Asteroid:move()
U:moveObject(self)
end
--# Splat
-- Splat
-- RJ 20200521
local Splats = {}
local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}
function drawSplats()
for k, splat in pairs(Splats) do
splat:draw()
end
end
Splat = class()
function Splat:init(pos)
local die = function()
Splats[self] = nil
end
self.pos = pos
Splats[self] = self
self.size = 2
self.diameter = 6
self.rot = math.random(0,359)
tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
stroke(255)
rotate(self.rot)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, self.diameter)
end
popMatrix()
popStyle()
end
--# Missile
-- Missile
-- RJ 20200522
Missile = class()
function Missile:init(ship)
function die()
self:die()
end
self.pos = ship.pos
self.step = U.missileVelocity:rotate(ship.radians) + ship.step
U.missiles[self] = self
tween(3, self, {}, tween.easing.linear, die)
end
function Missile:die()
U.missiles[self] = nil
end
function Missile:draw()
ellipse(self.pos.x, self.pos.y, 6)
end
function Missile:move()
U:moveObject(self)
end
--# Universe
-- Universe
-- RJ 20200523
Universe = class()
local MissileSpeed = 2.0
function Universe:init()
self.score = 0
self.missileVelocity = vec2(MissileSpeed,0)
self.button = {}
createButtons()
self.ship = Ship()
self.processorRatio = 1.0
self.asteroids = {}
self.missiles = {}
self.explosions = {}
end
function Universe:draw()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
self.processorRatio = DeltaTime/0.0083333
self:drawAsteroids()
self:drawExplosions()
checkButtons()
drawButtons()
if self.ship then self.ship:draw() end
if self.ship then self.ship:move() end
self:drawMissiles()
drawSplats()
U:drawScore()
popStyle()
U:findCollisions()
end
function Universe:createAsteroids()
for i = 1,4 do
local a = Asteroid()
self.asteroids[a] = a
end
end
function Universe:findCollisions()
for i,a in pairs(self.asteroids) do
self:checkMissileCollisions(a)
if self.ship then self:checkShipCollision(a) end
end
end
function Universe:checkShipCollision(asteroid)
if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
scoreAsteroid(asteroid)
splitAsteroid(asteroid, self.asteroids)
self:killShip()
end
end
function Universe:checkMissileCollisions(asteroid)
for k,m in pairs(self.missiles) do
if m.pos:dist(asteroid.pos) < asteroid:killDist() then
scoreAsteroid(asteroid)
splitAsteroid(asteroid, self.asteroids)
m:die()
end
end
end
function Universe:killShip()
local f = function()
self.ship = Ship()
end
Explosion(U.ship)
U.ship = nil
tween(6, self, {}, tween.easing.linear, f)
end
function Universe:moveObject(anObject)
local pos = anObject.pos + self.processorRatio*anObject.step
anObject.pos = vec2(self:keepInBounds(pos.x, WIDTH), self:keepInBounds(pos.y, HEIGHT))
end
function Universe:keepInBounds(value, bound)
return (value+bound)%bound
end
function Universe:drawExplosions()
for k,e in pairs(self.explosions) do
e:draw()
end
end
function Universe:drawMissiles()
pushStyle()
pushMatrix()
fill(255)
stroke(255)
for k, missile in pairs(self.missiles) do
missile:draw()
end
popMatrix()
popStyle()
for k, missile in pairs(self.missiles) do
missile:move()
end
end
function Universe:drawAsteroids()
pushStyle()
stroke(255)
fill(0,0,0, 0)
strokeWidth(2)
rectMode(CENTER)
for i,asteroid in pairs(self.asteroids) do
asteroid:draw()
asteroid:move()
end
popStyle()
killDeadAsteroids(self.asteroids)
end
function Universe:drawScore()
local s= "000000"..tostring(self.score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
end
--# Explosion
Explosion = class()
function Explosion:init(ship)
local f = function()
U.explosions[self] = nil
end
self.pos = ship.pos
self.step = vec2(0,0)
U.explosions[self] = self
tween(4, self, {}, tween.easing.linear, f)
end
function Explosion:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fontSize(30)
text("BLAMMO", 0, 0)
popMatrix()
popStyle()
end