6
\$\begingroup\$

Use these links to understand the code I'm posting below since although it looks like PHP it is not:


I have been making a programming language for some time now as a good learning experience etc and improving my programming skills.

I'm making a full clone of the original Pengo. I'm currently doing the AI there is a few Ghosts.

Here is a video of the game I'm making Pengo Demo Video.

Source code to the game: Pengo Source Code

Here is the website for the game and it's download Pengo wiki which will be helpful in seeing how the AI works etc in order to help me improve it. Also i wouldn't mind improving any aspect let me know your opinions.

  • Yellow ghost will just cruise around the map going clockwise when it hits a wall however it will randomly decide to move in another direction.
  • Blue ghost is similar to Red but if it seeds the player it will begin moving towards the player.
  • Green ghost is similar to Blue but if it gets within 3 blocks of the player it will go frenzy and chase player non-stop.
  • Pink ghost uses A* pathfinder to immediately chase the player and hunt them down no matter where they are.
  • Red ghost is one I'm having problems with I need it to avoid the player at all costs even move out of the way of pushed obstacles.

This post will focus on the AI for the Ghosts improvements and the best way to do it also I want to add an avoid AI for the Red ghost so it will avoid players and even dodge blocks pushed at it not sure how making that work perfect.

Forgive the lengthy post; I just wanted to make sure you had all the AI that is needed available

I don't really see a way to get the AI posted since if I leave out any parts it might cause more confusion

This is the constructor for the Green ghost AI this is how it creates it's AI and sets it up:

// AI
$AI = new AI($this); // Creates a CORE AI class
$AI->$Enabled = false;
my $AIWanderAStar = new AIWanderAStar($this); // Uses a AIWanderAStar
$AIWanderAStar->$Enabled = true;        
my $AIChase = new AIChase($this); // Uses an AIChase
$AIChase->$Enabled = true;
$AIChase->$TryChasePlayer = true;
$AIChase->$TryChasePlayerDist = 3;
$AIChase->$TryChasePlayerLock = true;       
my $AIMoveDelay = new AIMoveDelay($this); // Uses an AIMoveDelay
$AIMoveDelay->$MoveFor = 200;
$AIMoveDelay->$StopFor = 10;
$AIMoveDelay->$Enabled = true;
$AI->$AIList[] = $AIMoveDelay;
$AI->$AIList[] = $AIChase;
$AI->$AIList[] = $AIWanderAStar;

AI.spk

This is the BASE AI class that all AI's inherit:

Class AI
{
    Static $AIEnabled = true;
    my $Entity;
    my $Enabled;
    my $AIList;
    Function __Construct(Entity $E)
    {
        $Entity = $E;
        $Enabled = false;
        $AIList = array();
    }
    Function __Destruct()
    {
        Dispose();
    }
    Function Dispose()
    {
        if($Disposed)
            return;
        $Disposed = true;
        $Enabled = false;
        $Entity = null;
    }
    Function Update()
    {
        if($Disposed)
            return;
        foreach($AIList as my $AICls)
            if($AICls->DoUpdate())
                return;
    }
};

AIChase.spk

This is the Chase AI it is responsible for making AIs go after the player:

Class AIChase extends AI
{
    my $Path;
    my $AIWanderAStar;
    my $Chasing;
    my $ChaseCount;
    my $TryChasePlayer;
    my $TryChasePlayerDist;
    my $TryChasePlayerLock;
    Function __Construct(Entity $E)
    {
        parent::AI::__construct($E);
        // Create a wander ai to use when not chasing a player
        // Act like Slime when not chasing a player
        $AIWanderAStar = new AIWanderAStar($E);
        $AIWanderAStar->$Enabled = true;
        // A few chase things
        $Chasing = false;
        $ChaseCount = 0;
        // Keep the path in class for ref access
        $Path = array();
        // Saves having to resolve a ton of ->
        $PathFinder = $Entity->$Game->$Map->$PathFinder;
        $Map = $Entity->$Game->$Map;
        // Try chase?
        $TryChasePlayer = false;
        $TryChasePlayerDist = 0;
        $TryChasePlayerLock = false;
    }
    Function DoUpdate( )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        // Find a player to chase after
        my $Player = $Entity->FindClosestPlayer(true);
        if($TryChasePlayer) // Try chase a player that got too close first
        {
            if($Player != null)
                return TryChasePlayer($Player, $TryChasePlayerDist, $TryChasePlayerLock);
            else
                return false;
        }
        if($Player == null)
            // If we have nothing to attack just behave like a Wander
            return $AIWanderAStar->ASTarWanderAround();
        return ChasePlayer($Player, true);
    }
    Function ChasePlayer( $Player, $AllowWander )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        // If its possible to move begin organizing the chase
        if($Entity->Stopped())
        {
            // Get our current position
            my List($X, $Y) = $Entity->GetPosition();           
            // Get player position
            my List($PX, $PY) = $Player->GetPosition();
            // Plot a path to the players position
            $Path = array();
            $PathFinder->FindPath($Path, $X, $Y, $PX, $PY);
            my $Pos = $Path[0];
            if($Pos ~~ Array && count($Pos) == 2)
            {
                // Turn to face player
                if($X > $PX)
                    $Entity->$Facing = Facing->$Left;
                if($X < $PX)
                    $Entity->$Facing = Facing->$Right;
                // Get the postion we must move to
                my $MX = $Pos[0];
                my $MY = $Pos[1];
                // Move to new postion
                my $Moved = false;
                if(!$Moved && $MX > $X)
                    $Moved = $Entity->TryMove(Direction->$Right);
                if(!$Moved && $MX < $X)
                    $Moved = $Entity->TryMove(Direction->$Left);
                if(!$Moved && $MY > $Y)
                    $Moved = $Entity->TryMove(Direction->$Down);
                if(!$Moved && $MY < $Y)
                    $Moved = $Entity->TryMove(Direction->$Up);
                if(!$Moved && $AllowWander)
                    return $AIWanderAStar->ASTarWanderAround();
            }
            else if($AllowWander)
                return $AIWanderAStar->ASTarWanderAround();
        }
        return false;
    }
    Function TryChasePlayer($Player, $ChaseRange, $UseChaseLocker = false)
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        // We should chase the player?
        if($Entity->Stopped())
        {
            if($AIChase == null)
            {
                $AIChase = new AIChase($Entity);
                $AIChase->$Enabled = true;
            }
            my List($X, $Y) = $Entity->GetPosition();
            my List ($PX, $PY) = $Player->GetPosition();
            my $Dist = Utils::Distance2D($X, $Y, $PX, $PY);
            if($Dist <= $ChaseRange || $Chasing)
            {
                if(!$Chasing)
                {
                    $Chasing = true;
                    $ChaseCount = $ChaseRange + 1;
                }
                else
                    $ChaseCount--;
                if($ChaseCount > 0)
                {
                    if($Dist <= $ChaseRange && $ChaseCount == 1)
                        $ChaseCount++; // Keep up the chase
                    $AIChase->ChasePlayer($Player, false);
                    return !$Entity->Stopped(); // Stopped means chase AI didnt move us
                }
                else
                {
                    $ChaseCount = 0;
                    $Chasing = false;
                }
            }
        }
        return false;
    }
};

AIMoveDelay.spk

This tries to make the AIs freeze for a few hundred milliseconds every so often to give the player a chance to escape an AI in hot pursuit this Delay is only used on proper Chase AIs:

Class AIMoveDelay extends AI
{
    my $MoveFor;
    my $StopFor;
    my $MoveCount;
    my $Move;
    Function __Construct(Entity $E)
    {
        parent::AI::__construct($E);
        $Move = false;
        $MoveCount = 0;
        $MoveFor = 0;
        $StopFor = 0;
    }
    Function DoUpdate( )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        if(($Move && $MoveCount == $StopFor) || (!$Move && $MoveCount == $MoveFor))
        {
            $MoveCount = 0;
            $Move = !$Move;
        }
        $MoveCount++;
        if(!$Move)
            return false;
        return true;
    }
};

AIAttackPlayersInSight.spk

This should cause an AI to immediately start moving towards a player if it gets a clear line of sight:

Class AIAttackPlayersInSight extends AI
{
    Function __Construct(Entity $E)
    {
        parent::AI::__construct($E);
    }
    Function DoUpdate( )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        return AttackPlayersInSight();
    }
    Function AttackPlayersInSight()
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        if($Entity->Stopped())
        {
            my List($X, $Y) = $Entity->GetPosition();
            foreach($Entity->$Game->$Map->Players() as my $V)
            {
                if(!$V->$BeenHit && !$V->$Immortal)
                {
                    my List ($PX, $PY) = $V->GetPosition();
                    // Horizontal check
                    if($Y == $PY)
                    {
                        if(!$Entity->$Game->$Map->IsPathXBlocked($Y, $X, $PX))
                        {
                            my $moved = false;
                            if($X > $PX)
                                $moved = $Entity->TryMove(Direction->$Left);
                            else
                                $moved = $Entity->TryMove(Direction->$Right);
                            return $moved;
                        }
                    }
                    // Verticle check
                    if($X == $PX)
                    {
                        if(!$Entity->$Game->$Map->IsPathYBlocked($X, $Y, $PY))
                        {
                            my $moved = false;
                            if($Y > $PY)
                                $moved = $Entity->TryMove(Direction->$Up);
                            else
                                $moved = $Entity->TryMove(Direction->$Down);
                            return $moved;
                        }
                    }
                }
            }
        }
        return false;
    }
};

AIWander.spk

This is the default Wander AI that makes an AI just zip around the map in a fairly clockwise way but with potential randomness:

Class AIWander extends AI
{
    my $AllowRandomMoves; // Used only for non A*
    my $GoRandomMove; // Used by $AllowRandomMoves when being called again
    Function __Construct(Entity $E)
    {
        parent::AI::__construct($E);
        $AllowRandomMoves = false;
        $GoRandomMove = false;
    }
    Function DoUpdate( )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        // If we arent wandering then we must fix it
        // Rules should be:
        // 1. Try to avoid going back the way we came
        // 2. Always try to go clockwise
        // Very simple and basic for the lowest level monster
        return WanderAround();
    }
    Function WanderAround( )
    {
        if($Entity->Stopped())
        {
            // This random change of direction is not part of "$AllowRandomMoves"
            // It is just something so the ghost can go places a bit better
            if($Entity->CanMove($Entity->$MoveDirection))
            {
                // 70% chance to continue on path
                if(Utils::ChanceOf(0, 100, 0, 70)) // 70% chance to continue on path
                    return $Entity->TryMove($Entity->$MoveDirection);
            }
            my $AvoidDir = Entity::$DirectionOpposit[$Entity->$MoveDirection];
            $GoRandomMove = false;
            if(!DoMove($AvoidDir, false))
                return DoMove($AvoidDir, true);
            else
                return true;
        }
    }
    Function DoMove( $AvoidDir, $UseAvoidDir )
    {
        if(!$Enabled)
            return false; 
        if($Disposed)
            return false;
        if($AllowRandomMoves)
        {
            if($GoRandomMove || Utils::ChanceOf(0, 100, 20, 40)) // 20% chance to make a totally random move
            {
                my $moved = false;
                $GoRandomMove = true;
                // But will still respect the avoid dir
                switch((int)Random(0, 3))
                {
                    case 0:
                        if($AvoidDir != Direction->$Up || $UseAvoidDir)
                            $moved = $Entity->TryMove(Direction->$Up);
                        break;
                    case 1:
                        if($AvoidDir != Direction->$Down || $UseAvoidDir)
                            $moved = $Entity->TryMove(Direction->$Down);
                        break;
                    case 2:
                        if($AvoidDir != Direction->$Left || $UseAvoidDir)
                            $moved = $Entity->TryMove(Direction->$Left);
                        break;
                    default:
                        if($AvoidDir != Direction->$Right || $UseAvoidDir)
                            $moved = $Entity->TryMove(Direction->$Right);
                        break;
                }
                return $moved;
            }
        }
        if(($AvoidDir != Direction->$Up || $UseAvoidDir) && $Entity->TryMove(Direction->$Up))
            return true;
        if(($AvoidDir != Direction->$Right || $UseAvoidDir) && $Entity->TryMove(Direction->$Right))
            return true;
        if(($AvoidDir != Direction->$Down || $UseAvoidDir) && $Entity->TryMove(Direction->$Down))
            return true;
        if(($AvoidDir != Direction->$Left || $UseAvoidDir) && $Entity->TryMove(Direction->$Left))
            return true;
        return false;
    }
};

AIWanderAStar.spk

This is a random AI that will move to any location on the map using A* to plot a path to get there:

Class AIWanderAStar extends AI
{
    my $Path;
    my $Fails;
    my $PathFinder;
    my $Map;
    Function __Construct(Entity $E)
    {
        parent::AI::__construct($E);
        // To handle random generation failing
        $Fails = 0;
        // Keep the path in class for ref access
        $Path = array();
        // Saves having to resolve a ton of ->
        $PathFinder = $Entity->$Game->$Map->$PathFinder;
        $Map = $Entity->$Game->$Map;
    }
    Function DoUpdate( )
    {
        if(!$Enabled)
            return false;
        if($Disposed)
            return false;
        if(!AI::$AIEnabled)
            return false;
        return ASTarWanderAround();
    }
    Function ASTarWanderAround( )
    {
        if($Entity->Stopped())
        {
            my List($X, $Y) = $Entity->GetPosition();
            // Generate a new path if needed
            if(!$Path)
                GenerateNewPath($X, $Y);
            // Move to next part of the path
            my $Cord = Shift($Path);
            if($Cord)
            {
                if($Cord ~~ Array && count($Cord) == 2)
                {
                    // Get the postion we must move to
                    my $MX = $Cord[0];
                    my $MY = $Cord[1];
                    if($Map->IsXYBlocked($MX, $MY, true))
                    {
                        GenerateNewPath($X, $Y);
                        return false;
                    }
                    // Move to new postion
                    my $Moved = false;
                    if(!$Moved && $MX > $X)
                        $Moved = $Entity->TryMove(Direction->$Right);
                    if(!$Moved && $MX < $X)
                        $Moved = $Entity->TryMove(Direction->$Left);
                    if(!$Moved && $MY > $Y)
                        $Moved = $Entity->TryMove(Direction->$Down);
                    if(!$Moved && $MY < $Y)
                        $Moved = $Entity->TryMove(Direction->$Up);
                    if($Moved)
                    {
                        $Fails = 0;
                        return true;
                    }
                    else
                        $Fails++;
                    if($Fails > 3)
                    {
                        GenerateNewPath($X, $Y);
                        return false;
                    }
                }
            }
        }
        return false;
    }
    Function GenerateNewPath($X, $Y)
    {
        $Path = array();
        my $Pos = $Map->FindEmptyCoordinate();
        if($Pos != null)
            $PathFinder->FindPath($Path, $X, $Y, $Pos[0], $Pos[1]);
    }
};

Utils.spk

class Utils
{
    Static Function Distance2D($dX0, $dY0, $dX1, $dY1)
    {
        return Sqrt(($dX1 - $dX0)*($dX1 - $dX0) + ($dY1 - $dY0)*($dY1 - $dY0));
    }
    Static Function ChanceOf($Min, $Max, $From, $To)
    {
        my $Chance = (int)Random($Min, $Max + 1);
        return $Chance >= $From && $Chance <= $To;
    }
};
\$\endgroup\$
2
\$\begingroup\$

I am not familiar with this language (no one is), but I will make some comments about the readability of your code. These are sort of general blanket statements that could be applied to any language, and definitely can be applied here.


The only time you ever wrap an if, else, or loop's body in curly braces is when it's more than one line. I know that these are apparently optional in this language, and I know that they're optional in many languages. However, optional curly braces aren't really that great of an idea, and some modern languages have recognized this and made them non-optional.

The problem with optional braces is that we run into constructs like this:

if(!$Move)
    return false;
return true;

And this:

foreach($AIList as my $AICls)
    if($AICls->DoUpdate())
        return;

And unless your language enforces indentation like Python, it's extraordinarily easy for these bits of code to get messed up really easily.

A confused maintainer may comment out return true;, especially if it were ever accidentally indented another level. Or someone looking to add to the foreach loop may completely break the code.

You should definitely include curly braces here.


if(Utils::ChanceOf(0, 100, 0, 70)) // 70% chance to continue on path
if($GoRandomMove || Utils::ChanceOf(0, 100, 20, 40)) // 20% chance to make a totally random move

This Utils::ChanceOf function is really confusing.

I don't see the function anywhere in the code, so I'm assuming it's a language level function. It's nice that you provided comments on what they do... but a language level function shouldn't NEED comments.

Some of understanding language level functions comes with an understanding of the language... but a function which apparently returns a bool value a given percentage of the time should probably only need to take that percentage as an argument. Why are there 4 arguments? What do they all mean?

Why can't this just be:

if (Utils::ChanceOf(70))

And

if (Utils::ChanceOf(20))

                    }
                }
            }
        }
    }
    return false;
}

This is a pretty good sign that you're nested way too deeply.

First,

    if(!$Enabled)
        return false;
    if($Disposed)
        return false;
    if(!AI::$AIEnabled)
        return false;

Why not just:

if (!$Enabled || $Disposed || !AI::$AIEnabled) {
    return false;
}

The if's within this foreach:

foreach($Entity->$Game->$Map->Players() as my $V)

Can probably be moved out into their own functions (there's repeated code here even) and that will help cut down some of the nesting.

\$\endgroup\$
  • \$\begingroup\$ "Why not just:" That is often how I do it but sometimes I don't... "if (Utils::ChanceOf(70))" Yea it can I just wanted full control over the parameters at the time. "problem with optional braces" I know it's a bad habit I tend to omit them wherever possible although I always tab my code properly at least. "nested way too deeply." I do prefer modular functions that handle them sort of tasks but in this code I felt I didn't need to do that at the time... I included the games source code in my topic and I will put it here ubersoft.org/download/PengoSrc.rar just so you can find any function \$\endgroup\$ – UberFoX Aug 13 '14 at 23:48
  • 2
    \$\begingroup\$ The problem with optional braces isn't the person who wrote it in the first place. It's the person who comes in behind the person who wrote it in the first place. \$\endgroup\$ – nhgrif Aug 13 '14 at 23:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.