Thread Runner (game)

Here is a game I wrote while watching a couple of football games on Sunday. I wasn’t satisfied with the way it worked, so I re-wrote it on Monday and made other updates today. I’m not sure what reminded me of the game, but I just started to write it. It’s based on a game my daughter and I played many, many, many years ago on her Commodore 64. It’s called Beam Rider. If you’re old enough you may have played it, if not you can do a search for it to see the original. I played this game serval times, but I’m not sure if I have all the bugs worked out. If you find any, let me know. To see the instructions, you can either load the game or just scroll down to the end and read the instructions there. This game is posted in several chunks.


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT)

-- copyright dave1707 2012 - all rights reserved

function setup()
    
    --to clear the high scores, uncomment clearLocalData().
    --run the game, stop it, then comment clearLocalData().
    --clearLocalData()
    
    bx={ 35, 85,135,185,235,285,335,385,435,485,535,585,635,685,735}
    tx={280,295,310,325,340,355,370,385,400,415,430,445,460,475,490}
    
    msx={}
    enx={}
    calcMsxEnx()
    init()
    showInstructions=true
end

function init()
    misslesTab={ {},{},{},{},{},{},{},{},{},{},{},{},{},{},{} }
    enemyTab={ {},{},{},{},{},{},{},{},{},{},{},{},{},{},{} }
    explosionTab={}
    enemyHit=0
    misslesFired=0
    objectsLeft=0
    total=0
    sx=WIDTH/2
    ang=0
    shoot=0
    hit=true
    enemyEt=0
    shootEt=0
    shieldStrength=10
    showingHighScore=false
end

function draw()
    background(40, 40, 50)
    if showInstructions then
        instructions()
        return 
    end
    drawLines()
    drawObjects()
    removeObjects()
    createEnemy()
    showScore()
    endGame()
    who()
end

function showScore()
    fill(39, 176, 93, 255)
    fontSize(30)
    fill(0,0,255)
    text("Thread Runner",WIDTH/2,830)
    fill(0,255,0)
    str=string.format("Enemy ships hit %4d",enemyHit)
    text(str,150,1000)
    str=string.format("Objects left %3d",objectsLeft)
    text(str,150,960)
    str=string.format("Shield strength %3d%% ",shieldStrength*10)
    text(str,375,100)
    str=string.format("Missles fired %4d",misslesFired)
    text(str,600,1000)
    fontSize(80)
    total=enemyHit*50-misslesFired*2-objectsLeft*10
    str=string.format("%d",total)
    text(str,375,900)
end

function createEnemy()
    if shieldStrength>0 then
        if ElapsedTime-enemyEt>10 then
            offset=math.random(1,15)
            table.insert(enemyTab[offset],enemy(tx[offset],800,enx[offset]))
            enemyEt=ElapsedTime
        end
        if hit then
            offset=math.random(1,15)
            table.insert(enemyTab[offset],enemy(tx[offset],800,enx[offset]))
        end
        hit=false
    end
end

function endGame()
    local z; local y; local count
    if shieldStrength==0 then
        for z=1,#enemyTab do
            for y=1,#enemyTab[z] do
                if enemyTab[z][y].y==200 then
                    table.insert(explosionTab,explosion(enemyTab[z][y].x,enemyTab[z][y].y))
                    enemyTab[z][y].removeE=true
                    return
                end
            end
        end
        count=0
        for z=1,#misslesTab do 
            count=count+#misslesTab[z] 
        end
        for z=1,#enemyTab do 
            count=count+#enemyTab[z] 
        end
        count=count+#explosionTab
        if count==0 then
            showHighScore()
            fontSize(40)
            fill(255,0,0)
            text("Double tap screen to restart",WIDTH/2,220)
        end
    end      
end

function drawLines()
    local z
    local alpha=100
    if shieldStrength>0 then 
        alpha=255 
    end
    stroke(255,255,255,alpha)
    strokeWidth(4)
    for z=1,#bx do 
        line(bx[z],200,tx[z],800) 
    end
    line(117,400,653,400)
    line(178,550,592,550)
    line(219,650,551,650)
    line(249,725,520,725)
    line(268,770,502,770)
    strokeWidth(6)
    line(0,200,WIDTH,200)
    line(0,800,WIDTH,800)
end

function drawObjects()
    local z; local y
    if shieldStrength>0 then 
        drawShip() 
    end
    for z=1,#misslesTab do
        for y=1,#misslesTab[z] do
            misslesTab[z][y]:draw()
            if y==1 then misslesTab[z][y]:checkHit() end
        end
    end
    for z=1,#enemyTab do
        for y=1,#enemyTab[z] do 
            enemyTab[z][y]:draw() 
            enemyTab[z][y]:checkHit()
        end
    end
    for z=1,#explosionTab do 
        explosionTab[z]:draw() 
    end
end

function who()
    fontSize(17)
    fill(100,100,100,220)
    text("dave1707  (c)   12/26/2012",600,30)
end

function touched(t)
    local z
    if t.state==BEGAN then 
        if t.tapCount==2 then
            if showingHighScore then 
                init() return 
            end
            if showInstructions then 
                showInstructions=false 
                return 
            end
        end
        if t.y>800 and ElapsedTime-shootEt>.1 then
            shootEt=ElapsedTime
            for z=1,#bx do
                if sx>bx[z]-15 and sx<bx[z]+15 then
                    shoot=1
                    table.insert(misslesTab[z],missles(bx[z],200,msx[z],z))
                    misslesFired = misslesFired + 1
                    sound(SOUND_SHOOT, 49730)
                    return
                end
            end
            return
        end
    end
    if t.state==MOVING and shieldStrength>0 then  
        sx=sx+t.deltaY
        ang=-(WIDTH/2-sx)/15
        if sx<35 then 
            sx=35
        elseif sx>735 then
            sx=735
        end
    end
end

Thread Runner continued


function calcMsxEnx()
    for z=1,#bx do
        msx[z]=(tx[z]-bx[z])/600 
        enx[z]=(bx[z]-tx[z])/600 
    end
end

function drawShip()
    pushMatrix()
    translate(sx,200)
    rotate(ang)
    sprite("Tyrian Remastered:Evil Head",0,0)
    if shoot>0 then 
        sprite("Tyrian Remastered:Flame 1",0,50) 
        shoot = 0 
    end
    popMatrix()
end

function removeObjects()
    local z; local y
    for z=1,#misslesTab do
        for y=1,#misslesTab[z] do
            if misslesTab[z][y]:remove(z,y) then 
                return 
            end
        end
    end
    for z=1,#enemyTab do
        for y=1,#enemyTab[z] do
            if enemyTab[z][y]:remove(z,y) then 
                return 
            end
        end
    end
    for z=1,#explosionTab do
        if explosionTab[z]:remove(z) then 
            return 
        end
    end
end

explosion = class()

function explosion:init(x,y)
    self.x=x
    self.y=y
    self.explode=true
end

function explosion:draw()
    if self.explode then
        sound(SOUND_EXPLODE, 49737)
        sprite("Tyrian Remastered:Explosion Huge",self.x,self.y)
        self.explode=false
    end
end

function explosion:remove(offset)
    if not self.explode then
        if table.remove(explosionTab,offset) then 
            return true 
        end
    end
    return false
end

enemy = class()

function enemy:init(x,y,dxdy)
    self.x=x
    self.y=y
    self.dxdy=dxdy
    self.endx=math.random(-1,1)
    self.removeE=false
    self.fuel=false
    if math.random(1,15)==5 then 
        self.fuel=true 
    end
end

function enemy:draw()
    if self.fuel then 
        sprite("Tyrian Remastered:Eye Green",self.x,self.y)
    else
        sprite("Tyrian Remastered:Enemy Ship C",self.x,self.y)
    end
    if self.y==200 then
        self.x=self.x+self.endx
        if self.x>WIDTH or self.x<0 then 
            self.removeE=true hit=true 
        end
        if shieldStrength==0 then
            self.removeE=true 
            objectsLeft = objectsLeft + 1
            table.insert(explosionTab,explosion(self.x,self.y))
        end
    else
        self.y = self.y - 1 self.x = self.x + self.dxdy
    end
end

function enemy:checkHit()
    if self.y==200 then
        if math.abs(self.x-sx)<10 then
            self.removeE=true
            if not self.fuel then
                table.insert(explosionTab,explosion(sx,200))
                shieldStrength = shieldStrength - 1
                hit=true
                if shieldStrength==0 then
                    table.insert(explosionTab,explosion(sx,200))
                    sx=-100
                end
            else
                shieldStrength = shieldStrength + 1
                if shieldStrength>10 then
                    shieldStrength=8
                    hit=true
                    sound(SOUND_POWERUP, 5330)
                else
                    sound(SOUND_JUMP, 49717)
                end
            end
        end
    end
end

function enemy:remove(off1,off2)
    if self.removeE then 
        table.remove(enemyTab[off1],off2) 
        return true 
    end
    return false
end

missles = class()

function missles:init(x,y,dxdy,thread)
    self.x=x
    self.y=y
    self.dxdy=dxdy
    self.thread=thread
    self.removeM=false
end

function missles:draw()
    sprite("Tyrian Remastered:Powerup 3",self.x,self.y)
    self.y = self.y + 1
    self.x = self.x + self.dxdy
    if self.y>800 then
        self.removeM=true
        hit=true
    end
end

function missles:checkHit()
    if #enemyTab[self.thread]>0 then
        if math.abs(self.y-enemyTab[self.thread][1].y)<10 then
            self.removeM=true
            enemyTab[self.thread][1].removeE=true
            table.insert(explosionTab,explosion(self.x,self.y))
            enemyHit = enemyHit + 1
            hit=true
        end
    end
end

function missles:remove(off1,off2)
    if self.removeM then
        table.remove(misslesTab[off1],off2)
        return true
    end
    return false
end

function showHighScore()
    showingHighScore=true
    pushStyle()
    fill(255)
    fontSize(40)
    font("Courier")
    t=total
    for x=1,10 do
        str=string.format("threadrunner%d",x)
        h=readLocalData(str)
        if h==nil then
            saveLocalData(str,0)
            if t>0 then
                saveLocalData(str,t)
                t=0
            end
        elseif t==tonumber(h) then
                break
        elseif t>tonumber(h) then
                saveLocalData(str,t)
                t=tonumber(h)
        end
    end    
    for x=1,10 do
        str=string.format("threadrunner%d",x)
        h=readLocalData(str) 
        str=string.format("%2d) %6d",x,h)
        text(str,WIDTH/2,800-x*50)
    end
    popStyle()
end

function instructions()
    textWrapWidth(650)
    textAlign(CENTER)
    fontSize(50)
    fill(255,0,0)
    text("Thread Runner",WIDTH/2,950)
    fontSize(25)
    fill(255)
    str="The object is to destroy the enemy ships running down "
    .."the theads. To move your ship (side to side) along the "
    .."bottom line, slide or flick your finger (up or down) anywhere "
    .."on the screen below the top line. To shoot, your ship "
    .."has to be near one of the threads, then tap the screen "
    .."anywhere above the top line. Every time an enemy ship hits "
    .."yours, you lose 10% of your shield strength. You get 10% shield "
    .."strength back each time you run into a green energy sphere. If you run "
    .."into an energy sphere and your shield strength is 100%, you "
    .."lose 20% due to an overload. An enemy ship is created "
    .."every 10 seconds, if a missile reaches the top line, you "
    .."run into an energy sphere at 100% shield strength, or you "
    .."shoot an enemy ship. Once your shield strength reaches 0%, "
    .."the game is over and you lose points for each object not "
    .."destroyed. The 10 highest scores will be saved and negative "
    .."scores will be ignored. Warning: There may be several enemy "
    .."ships overlapping each other along the bottom line, so if "
    .."you run into them, your shield strength can drop quickly. "
    text(str,WIDTH/2,550)
    fontSize(30)
    fill(0,255,0)
    text("Double tap the screen to start.",WIDTH/2,100)
end


For some reason the second part posted twice, so I had to delete it from here.

Nice. Top score 1716 so far. Gets pretty hard pretty quickly and the control system takes a lot of getting used to - why not slide left to right to move the ship?

Also a few typos in the instructions - 3 loose instead of lose and 1 missle instead of missile.

Finally, some of the enemies stay at the bottom of the thread rather than move left or right. Not sure if this is meant

@West Thanks for the spell check. I corrected them, but I’m not going to correct my bad spelling (missles) in the program itself. I had a top score above 17000, but then I played it a lot while writing it. It gets easier the more you play. I move (up and down) to move the ship because I found it easier to move my finger or thumb up or down while holding the iPad while my other hand fired the missiles. After playing a few times, it will feel natural and you’ll be able to move to a thread and fire real easy. As for the enemy ships staying in one place at the bottom, that’s to force you to run into them. It’s more of an incentive for you to shoot them coming down the thread. Also, they stack up so if you don’t keep track of them, you don’t know how much shield energy you’re going to lose when you have to hit them. When you play for a long time, it’s not unusual to have maybe 100 stacked at the bottom. You kind of get trapped because you know if you have to hit them, the game will be over, so you want to keep hitting them so they don’t build up. That makes the energy balls more important.

Ah that control method is easier. Up to 3708 but nowhere near 17000!