Playdate 9

In day 8 I added an enemy ghost, but one with no brain – it just bumbled randomly around the maze:

Time for some very simple AI.

In real Pacman the three ghosts have different chasing strategies, but I thought I’d go for something very simple, from first principles – a ghost that chases straight towards the player, wherever he/she is.

First, I had to rework something: my enemy ghost was making turn decisions too often, when they should, normally, only even consider changing direction at a junction or collision:

-- check for collision with wall on current path
if checkForOpenPath(mapRefX,mapRefY,v) == false then
    enemyDirections[i] = chooseDirectionChange(i,v,mapRefX,mapRefY,true)
    hasChanged = true
else 
    if checkForJunction(mapRefX,mapRefY,v) then
        enemyDirections[i] = chooseDirectionChange(i,v,mapRefX,mapRefY,false)
    end   
end   

The new checkForJunction function called there just tests if either of the paths at a 90 degree angle to the current trajectory is open.

The chooseDirectionChange function, though, is the one that has work to do. First we calculate the distance from the player on the X and Y axes:

-- calculate divergence from player sprite position
    local divergenceX = ((playerSprite.x-topLeft)/16)+1 - mapRefX
    local divergenceY = (playerSprite.y/16)+1 - mapRefY

.. them use those to make an ordered array* of directions, in preference order based on comparing the absolute values of both (eg, if divergenceX is 5 and divergenceY is -7, heading up should be the priority (7>5) and the array will look like up, right, left, down.

Finally, we iterate through that array, test to see if each direction in turn is possible, and if so, take it:

for i, v in ipairs(directionBestFirst) do
-- checking direction v

    if v == currentDir and mandatory == true then 
        -- current direction, and turning due to collision, so ignore
    else     
        -- is it possible to go that way?
        if checkForOpenPath(mapRefX,mapRefY,v) then
            -- yes this direction is open
            return v
        end 
    end    
    
end  

With this done, our enemy does actually head towards the player – and will quite successfully chase them, too. So – very basic enemy AI, done!

Two’s Company

When adding the first enemy I tried to make it extensible by defining the enemy as the first in an array. Time to put that to the test:

-- in the head, define two enemy starting directions:
local enemyDirections = {"l","r"}

-- and then in initEnemy:
function initEnemy(ghostTable)
    -- Set up the enemy sprite 
    enemySprite = AnimatedSprite.new(ghostTable)
    enemySprite:playAnimation()
    enemySprite:setCenter(0, 0)
    enemySprite:moveTo( (topLeft + (9*16)), (7*16) )  
    enemySprite:setCollideRect( 0, 0, 16, 16 )
    enemySprite:add()
    table.insert(enemySprites,enemySprite)

    -- try another one as well?
    enemySprite2 = AnimatedSprite.new(ghostTable)
    enemySprite2:playAnimation()
    enemySprite2:setCenter(0, 0)
    enemySprite2:moveTo( (topLeft + (7*16)), (7*16) )  
    enemySprite2:setCollideRect( 0, 0, 16, 16 )
    enemySprite2:add()
    table.insert(enemySprites,enemySprite2)

end  

Happily, this worked first time – up pops another ghost, with a slightly different starting position but the same laser-like focus in chasing down our player.

Also note in that code that I added a collision detection rectangle to each – each sprite needs one.

Game Over

And finally, what about when our enemy ghosts catch the player?

function checkCollisions()
    -- v basic for now
    local collisions = playerSprite:overlappingSprites()
    if #collisions > 0 then
        print ('died!')
        return true
    end    
    return false
end    

This is called from the loop and, when it returns TRUE, the game (for now) ends. Here’s how it looks:

Next up:

  • Change the player sprite, because with three ghosts in the maze this is all getting a bit silly?
  • The “Ghost House” should be no-entry
  • Collision detection needs a bit of work
  • Lives, reset, game over states

Full code for this working demo here; my entire Playdate series is here.

* I know Lua calls them tables, but I just can’t. Sorry. Close enough!


Posted

in

,

by