hedgewars/uAIAmmoTests.pas
author Wuzzy <Wuzzy2@mail.ru>
Sun, 13 Dec 2020 22:26:39 +0100
changeset 15796 9588bd852202
parent 15771 82fd124e6512
child 15858 f7875779d910
permissions -rw-r--r--
Add alfadur to credits for Mushroom/Teapot graves

(*
 * Hedgewars, a free turn based strategy game
 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *)

{$INCLUDE "options.inc"}

unit uAIAmmoTests;
interface
uses uConsts, uFloat, uTypes, uAIMisc;
const
    amtest_Rare            = $00000001; // check only several positions
    amtest_NoTarget        = $00000002; // each pos, but no targetting
    amtest_MultipleAttacks = $00000004; // test could result in multiple attacks, set AttacksNum
    amtest_NoTrackFall     = $00000008; // skip fall tracing.  
    amtest_LaserSight      = $00000010; // supports laser sighting
    amtest_NoVampiric      = $00000020; // don't use vampirism with this ammo
    amtest_NoInvulnerable  = $00000040; // don't use invulnerable with this with ammo
    amtest_NoLowGravity    = $00000080; // don't use low gravity with this with ammo

var aiWindSpeed: real;
    aiGravity: hwFloat;
    aiGravityf: real;
    aiLaserSighting: boolean;
    aiHogsInTeam: LongInt;

type TAttackParams = record
        Time, Bounce, AttacksNum: Longword;
        Angle, Power: LongInt;
        ExplX, ExplY, ExplR: LongInt;
        AttackPutX, AttackPutY: LongInt;
        end;

function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestRCPlane(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestDrillStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestMineStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestSineGun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestSMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestPiano(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestResurrector(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestSeduction(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestKnife(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestAirMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
function TestMinigun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;

type TAmmoTestProc = function (Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
    TAmmoTest = record
            proc: TAmmoTestProc;
            flags: Longword;
            end;

const AmmoTests: array[TAmmoType] of TAmmoTest =
            (
            (proc: nil;              flags: 0), // amNothing
            (proc: @TestGrenade;     flags: 0), // amGrenade
            (proc: @TestClusterBomb; flags: 0), // amClusterBomb
            (proc: @TestBazooka;     flags: 0), // amBazooka
            (proc: @TestBee;         flags: amtest_Rare), // amBee
            (proc: @TestShotgun;     flags: amtest_LaserSight), // amShotgun
            (proc: nil;              flags: 0), // amPickHammer
            (proc: nil;              flags: 0), // amSkip
            (proc: nil;              flags: 0), // amRope
            (proc: @TestMine;        flags: amtest_NoTarget), // amMine
            (proc: @TestDesertEagle; flags: amtest_MultipleAttacks or amtest_LaserSight), // amDEagle
            (proc: @TestDynamite;    flags: amtest_NoTarget), // amDynamite
            (proc: @TestFirePunch;   flags: amtest_NoTarget), // amFirePunch
            (proc: @TestWhip;        flags: amtest_NoTarget or amtest_NoInvulnerable), // amWhip
            (proc: @TestBaseballBat; flags: amtest_NoTarget or amtest_NoInvulnerable), // amBaseballBat
            (proc: nil;              flags: 0), // amParachute
            (proc: @TestAirAttack;   flags: amtest_Rare), // amAirAttack
            (proc: @TestMineStrike;  flags: amtest_Rare), // amMineStrike
            (proc: nil;              flags: 0), // amBlowTorch
            (proc: nil;              flags: 0), // amGirder
            (proc: nil;              flags: 0), // amTeleport
            //(proc: @TestTeleport;    flags: amtest_OnTurn), // amTeleport
            (proc: nil;              flags: 0), // amSwitch
            (proc: @TestMortar;      flags: 0), // amMortar
            (proc: @TestKamikaze;    flags: amtest_LaserSight or amtest_NoInvulnerable or amtest_NoVampiric), // amKamikaze
            (proc: @TestCake;        flags: amtest_Rare or amtest_NoTarget), // amCake
            (proc: @TestSeduction;   flags: amtest_NoTarget), // amSeduction
            (proc: @TestWatermelon;  flags: 0), // amWatermelon
            (proc: nil;              flags: 0), // amHellishBomb
            (proc: nil;              flags: 0), // amNapalm
            (proc: @TestDrillRocket; flags: 0), // amDrill
            (proc: nil;              flags: 0), // amBallgun
            (proc: @TestRCPlane;     flags: amtest_LaserSight), // amRCPlane
            (proc: nil;              flags: 0), // amLowGravity
            (proc: nil;              flags: 0), // amExtraDamage
            (proc: nil;              flags: 0), // amInvulnerable
            (proc: nil;              flags: 0), // amExtraTime
            (proc: nil;              flags: 0), // amLaserSight
            (proc: nil;              flags: 0), // amVampiric
            (proc: @TestSniperRifle; flags: 0), // amSniperRifle
            (proc: nil;              flags: 0), // amJetpack
            (proc: @TestMolotov;     flags: 0), // amMolotov
            (proc: nil;              flags: 0), // amBirdy
            (proc: nil;              flags: 0), // amPortalGun
            (proc: @TestPiano;       flags: amtest_Rare or amtest_NoInvulnerable or amtest_NoVampiric), // amPiano
            (proc: @TestGrenade;     flags: amtest_NoTrackFall), // amGasBomb
            (proc: @TestSineGun;     flags: amtest_NoVampiric), // amSineGun
            (proc: nil;              flags: 0), // amFlamethrower
            (proc: @TestSMine;       flags: 0), // amSMine
            (proc: @TestHammer;      flags: amtest_NoTarget or amtest_NoInvulnerable), // amHammer
            (proc: @TestResurrector; flags: amtest_NoTarget or amtest_NoInvulnerable or amtest_NoVampiric or amtest_NoLowGravity), // amResurrector
            (proc: @TestDrillStrike; flags: amtest_Rare), // amDrillStrike
            (proc: @TestSnowball;    flags: amtest_NoInvulnerable or amtest_NoVampiric), // amSnowball
            (proc: nil;              flags: 0), // amTardis
            (proc: nil;              flags: 0), // amLandGun
            (proc: nil;              flags: 0), // amIceGun
            (proc: @TestKnife;       flags: 0), // amKnife
            (proc: nil;              flags: 0), // amRubber
            (proc: @TestAirMine;     flags: amtest_LaserSight), // amAirMine
            (proc: nil;              flags: 0), // amCreeper
            (proc: @TestMinigun;     flags: amtest_LaserSight),  // amMinigun
            (proc: nil;              flags: 0)  // amSentry
            );

implementation
uses uVariables, uUtils, uGearsHandlers;

function Metric(x1, y1, x2, y2: LongInt): LongInt; inline;
begin
Metric:= abs(x1 - x2) + abs(y1 - y2)
end;

function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const cExtraTime = 300;
var Vx, Vy, r, mX, mY: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY: real;
    t: LongInt;
    value: LongInt;
begin
Flags:= Flags; // avoid compiler hint
mX:= hwFloat2Float(Me^.X);
mY:= hwFloat2Float(Me^.Y);
ap.Time:= 0;
rTime:= 350;
ap.ExplR:= 0;
if (WorldEdge = weWrap) then
    if (Targ.Point.X < mX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else targXWrap:= Targ.Point.X - (RightX-LeftX);
valueResult:= BadTurn;
repeat
    rTime:= rTime + 300 + Level * 50 + random(300);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= - aiWindSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime
    else Vx:= - aiWindSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime;
    Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - mY) / rTime;
    r:= sqr(Vx) + sqr(Vy);
    if not (r > 1) then
        begin
        x:= mX;
        y:= mY;
        dX:= Vx;
        dY:= -Vy;
        t:= rTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;

            y:= y + dY;
            dX:= dX + aiWindSpeed;
            //dX:= CheckBounce(x,dX);
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t < -cExtraTime);

        EX:= trunc(x);
        EY:= trunc(y);
        if t >= -cExtraTime then
            begin
                if Level = 1 then
                    value:= RateExplosion(Me, EX, EY, 101, afTrackFall or afErasesLand)
                else
                    value:= RateExplosion(Me, EX, EY, 101);
            end else
                value:= BadTurn;

        if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            if GameFlags and gfSolidLand = 0 then
                 value := 1024 - Metric(Targ.Point.X, Targ.Point.Y, EX, EY) div 64
            else value := BadTurn;

        if (valueResult < value) or ((valueResult = value) and (Level < 3)) then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9));
            ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1);
            ap.ExplR:= 100;
            ap.ExplX:= EX;
            ap.ExplY:= EY;
            valueResult:= value
            end;
        end
until rTime > 5050 - Level * 800;
TestBazooka:= valueResult
end;

function calcBeeFlight(Me: PGear; x, y, dx, dy, tX, tY: real; var eX, eY: LongInt): LongInt;
var t: Longword;
    f: boolean;
    speed, d: real;
begin
    // parabola flight before activation
    t:= 500;
    repeat
        x:= x + dx;
        y:= y + dy;
        dy:= dy + aiGravityf;
        f:= ((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
           ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5));
        dec(t)
    until (t = 0) or (y >= cWaterLine) or f;

    if f then
    begin
        eX:= trunc(x);
        eY:= trunc(y);
        exit(RateExplosion(Me, eX, eY, 101, afTrackFall or afErasesLand));
    end;


    // activated
    t:= 5000;
    speed:= sqrt(sqr(dx) + sqr(dy));

    repeat
        if (t and $F) = 0 then
        begin
            dx:= dx + 0.000064 * (tX - x);
            dy:= dy + 0.000064 * (tY - y);
            d := speed / sqrt(sqr(dx) + sqr(dy));
            dx:= dx * d;
            dy:= dy * d;
        end;

        x:= x + dx;
        y:= y + dy;
        f:= ((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
           ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5));
        dec(t)
    until (t = 0) or f;

    if f then
    begin
        eX:= trunc(x);
        eY:= trunc(y);
        exit(RateExplosion(Me, eX, eY, 101, afTrackFall or afErasesLand));
    end
    else
        calcBeeFlight:= BadTurn
end;

function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var i, j: LongInt;
    valueResult, v, a, p: LongInt;
    mX, mY: real;
    eX, eY: LongInt;
begin
Flags:= Flags; // avoid compiler hint
    if Level > 1 then
        exit(BadTurn);

    eX:= 0;
    eY:= 0;
    mX:= hwFloat2Float(Me^.X);
    mY:= hwFloat2Float(Me^.Y);
    valueResult:= BadTurn;
    for i:= 0 to 8 do
        for j:= 0 to 1 do
            begin
            a:= i * 120;
            p:= random(cMaxPower - 200) + 180;

            if j = 0 then
                a:= -a;

            v:= calcBeeFlight(Me
                    , mX
                    , mY
                    , sin(a * pi / 2048) * p / cPowerDivisor
                    , -cos(a * pi / 2048) * p / cPowerDivisor
                    , Targ.Point.X
                    , Targ.Point.Y
                    , eX
                    , eY);

            if v > valueResult then
                begin
                ap.ExplR:= 100;
                ap.ExplX:= eX;
                ap.ExplY:= eY;
                ap.Angle:= a;
                ap.Power:= p;
                valueResult:= v
                end
            end;

    ap.AttackPutX:= Targ.Point.X;
    ap.AttackPutY:= Targ.Point.Y;

    if valueResult > 0 then
        TestBee:= valueResult - 5000
    else
        TestBee:= BadTurn // no digging
end;

function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var Vx, Vy, r, mX, mY: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY: real;
    t: LongInt;
    value: LongInt;
    t2: real;
    timer: Longint;
begin
Flags:= Flags; // avoid compiler hint
    if (Level > 3) then exit(BadTurn);

    mX:= hwFloat2Float(Me^.X);
    mY:= hwFloat2Float(Me^.Y);
    ap.Time:= 0;
    rTime:= 350;
    ap.ExplR:= 0;
    valueResult:= BadTurn;
    if (WorldEdge = weWrap) then
        if (Targ.Point.X < mX) then
             targXWrap:= Targ.Point.X + (RightX-LeftX)
        else targXWrap:= Targ.Point.X - (RightX-LeftX);
    timer:= 0;
    repeat
        rTime:= rTime + 300 + Level * 50 + random(300);
        if (WorldEdge = weWrap) and (random(2)=0) then
             Vx:= - aiWindSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) - mX) / rTime
        else Vx:= - aiWindSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) - mX) / rTime;
        Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y - 35 - mY) / rTime;
        r:= sqr(Vx) + sqr(Vy);
        if not (r > 1) then
            begin
            x:= mX;
            y:= mY;
            dX:= Vx;
            dY:= -Vy;
            t:= rTime;
            repeat
                x:= CheckWrap(x);
                x:= x + dX;
                y:= y + dY;
                dX:= dX + aiWindSpeed;
                dY:= dY + aiGravityf;
                dec(t)
            until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
                   ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (y > cWaterLine);

            if TestCollExcludingObjects(trunc(x), trunc(y), 5) and (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) > 21) then
                begin
                timer := 500;
                t2 := 0.5 / sqrt(sqr(dX) + sqr(dY));
                dX := dX * t2;
                dY := dY * t2;
                repeat
                    x:= x + dX;
                    y:= y + dY;
                    dec(timer);
                until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 22)
                    or (x < 0)
                    or (y < 0)
                    or (trunc(x) > LAND_WIDTH)
                    or (trunc(y) > LAND_HEIGHT)
                    or (not TestCollExcludingObjects(trunc(x), trunc(y), 5))
                    or (timer = 0)
                end;
            EX:= trunc(x);
            EY:= trunc(y);
            // Try to prevent AI from thinking firing into water will cause a drowning
            if (EY < cWaterLine-5) and (timer > 0) and (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) > 21) then
                value:= BadTurn
            else if Level = 1 then
                value:= RateExplosion(Me, EX, EY, 101, afTrackFall or afErasesLand)
            else
                value:= RateExplosion(Me, EX, EY, 101);
            if valueResult <= value then
                begin
                ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9));
                ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1);
                ap.ExplR:= 100;
                ap.ExplX:= EX;
                ap.ExplY:= EY;
                valueResult:= value-2500 // trying to make it slightly less attractive than a bazooka, to prevent waste.  AI could use awareness of weapon count
                end;
            end
    until rTime > 5050 - Level * 800;
    TestDrillRocket:= valueResult
end;

function TestRCPlane(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const
    MIN_RANGE =  200;
var Vx, Vy, meX, meY, x, y: real;
    rx, ry, valueResult: LongInt;
    range, maxRange: integer;
begin
// This is a very simple test to let a RC plane fly in a straight line, without dropping any bombs
// TODO: Teach AI how to steer
// TODO: Teach AI how to drop bombs
// TODO: Teach AI how to predict fire behavior
Flags:= Flags; // avoid compiler hint
if Level = 5 then
    exit(BadTurn)
else if Level = 4 then
    maxRange:= 2200
else if Level = 3 then
    maxRange:= 2900
else if Level = 2 then
    maxRange:= 3500
else
    maxRange:= 3900;
TestRCPlane:= BadTurn;
ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
x:= meX;
y:= meY;
range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y);
if ( range < MIN_RANGE ) or ( range > maxRange) then
    exit(BadTurn);

Vx:= (Targ.Point.X - x) * 1 / 1024;
Vy:= (Targ.Point.Y - y) * 1 / 1024;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
repeat
    x:= x + vX;
    y:= y + vY;
    rx:= trunc(x);
    ry:= trunc(y);
    if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 8)) or
        ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 8)) then
        begin
        x:= x + vX * 8;
        y:= y + vY * 8;

        // Intentionally low rating to discourage use
        if Level = 1 then
            valueResult:= RateExplosion(Me, rx, ry, 26, afTrackFall or afErasesLand)
        else
            valueResult:= RateExplosion(Me, rx, ry, 26);

        // Check range again in case the plane collided before target
        range:= Metric(trunc(meX), trunc(meY), rx, ry);
        if ( range < MIN_RANGE ) or ( range > maxRange) then
            exit(BadTurn);

        // If impact location is close, above us and wind blows in our direction,
        // there's a risk of fire flying towards us, so fail in this case.
        if (Level < 3) and (range <= 600) and (meY >= ry) and
            (((ap.Angle < 0) and (aiWindSpeed > 0)) or ((ap.Angle > 0) and (aiWindSpeed < 0))) then
            exit(BadTurn);

        // Apply inaccuracy
        if (not aiLaserSighting) then
            inc(ap.Angle, AIrndSign(random((Level - 1) * 9)));

        if (valueResult <= 0) then
            valueResult:= BadTurn;
        exit(valueResult)
        end
until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT);

TestRCPlane:= BadTurn
end;

function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const timeLimit = 5000;
      Density : real = 0.5;
var Vx, Vy, r: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY, meX, meY: real;
    t: LongInt;
    value: LongInt;

begin
Flags:= Flags; // avoid compiler hint
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
ap.Time:= 0;
rTime:= 350;
ap.ExplR:= 0;
valueResult:= BadTurn;
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else
         targXWrap:= Targ.Point.X - (RightX-LeftX);
repeat
    rTime:= rTime + 300 + Level * 50 + random(300);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= (targXWrap - meX) / rTime
    else
         Vx:= (Targ.Point.X - meX) / rTime;
    if (GameFlags and gfMoreWind) <> 0 then
         Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx
    else
         Vx:= -aiWindSpeed * rTime * 0.5 + Vx;
    Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y - meY) / rTime;
    r:= sqr(Vx) + sqr(Vy);

    if not (r > 1) then
        begin
        x:= meX;
        y:= meY;
        dX:= Vx;
        dY:= -Vy;
        t:= rTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density
            else
                dX:= dX + aiWindSpeed;

            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 4)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 4))) or (trunc(y) > cWaterLine) or (t < -timeLimit);

        EX:= trunc(x);
        EY:= trunc(y);

        // Sanity check: Make sure we're not too close to impact location
        if (Metric(trunc(meX), trunc(meY), EX, EY) <= 40) then
            value:= BadTurn
        // Rate attack
        else if (t >= -timeLimit) and (EY <= cWaterLine) then
            // radius intentionally set to 16 for shove because lower values don't work reliably
            value:= RateShove(Me, EX, EY, 16, 0, trunc((abs(dX)+abs(dY))*20), dX, dY, afTrackFall)
        else
            value:= BadTurn;

        if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            value := BadTurn;

        if (valueResult < value) or ((valueResult = value) and (Level = 1)) then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 12));
            ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 22 + 1);
            ap.ExplX:= EX;
            ap.ExplY:= EY;
            valueResult:= value
            end;
        end
until rTime > 5050 - Level * 800;
TestSnowball:= valueResult
end;

function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const timeLimit = 50;
      Density : real = 2.0;
var Vx, Vy, r, meX, meY: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY: real;
    t: LongInt;
    value, range: LongInt;
begin
Flags:= Flags; // avoid compiler hint
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
ap.Time:= 0;
rTime:= 350;
ap.ExplR:= 0;
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else targXWrap:= Targ.Point.X - (RightX-LeftX);
valueResult:= BadTurn;
repeat
    rTime:= rTime + 300 + Level * 50 + random(300);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime
    else
         Vx:= (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime;
    if (GameFlags and gfMoreWind) <> 0 then
         Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx;
    Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime;
    r:= sqr(Vx) + sqr(Vy);

    if not (r > 1) then
        begin
        x:= meX;
        y:= meY;
        dX:= Vx;
        dY:= -Vy;
        t:= rTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density;

            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t < -timeLimit);

        EX:= trunc(x);
        EY:= trunc(y);
        range:= Metric(trunc(meX), trunc(meY), EX, EY);

        // Sanity check 1: Make sure we've hit a hedgehog or object
        if not TestCollHogsOrObjects(EX, EY, 5) then
            value:= BadTurn
        // Sanity check 2: Make sure we're not too close to impact location
        else if (range < 150) and (Level < 5) then
            value:= BadTurn
        // Sanity check 3: If impact location is close, above us and wind blows
        // towards us, there's a risk of fire flying towards us, so fail in this case.
        else if (Level < 3) and (range <= 600) and (trunc(meY) >= EX) and
            (((ap.Angle < 0) and (aiWindSpeed > 0)) or ((ap.Angle > 0) and (aiWindSpeed < 0))) then
            value:= BadTurn
        // Timeout
        else if t < -timeLimit then
            value:= BadTurn
        else
        // Valid hit!
            // Weapon does not actually explode, so this rating is an approximation
            value:= RateExplosion(Me, EX, EY, 97); // average of 17 attempts, most good, but some failing spectacularly

        if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            value := BadTurn;

        if (valueResult < value) or ((valueResult = value) and (Level < 3)) then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9));
            ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1);
            ap.ExplR:= 100;
            ap.ExplX:= EX;
            ap.ExplY:= EY;
            valueResult:= value
            end;
        end
until rTime > 5050 - Level * 800;
TestMolotov:= valueResult
end;

function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const tDelta = 24;
      Density : real = 1.5;
var Vx, Vy, r: real;
    Score, EX, EY, valueResult: LongInt;
    TestTime: LongInt;
    targXWrap, x, y, meX, meY, dX, dY: real;
    t: LongInt;
begin
valueResult:= BadTurn;
TestTime:= 0;
ap.Bounce:= 0;
ap.ExplR:= 0;
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else targXWrap:= Targ.Point.X - (RightX-LeftX);
repeat
    inc(TestTime, 1000);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= (targXWrap + AIrndOffset(Targ, Level) - meX) / (TestTime + tDelta)
    else
         Vx:= (Targ.Point.X + AIrndOffset(Targ, Level) - meX) / (TestTime + tDelta);
    if (GameFlags and gfMoreWind) <> 0 then
         Vx:= -(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5 + Vx;
    Vy:= aiGravityf * ((TestTime + tDelta) div 2) - (Targ.Point.Y - meY) / (TestTime + tDelta);
    r:= sqr(Vx) + sqr(Vy);
    if not (r > 1) then
        begin
        x:= meX;
        y:= meY;
        dX:= Vx;
        dY:= -Vy;
        t:= TestTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density;
            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t = 0);
    EX:= trunc(x);
    EY:= trunc(y);
    if t < 50 then
        if (Level = 1) and (Flags and amtest_NoTrackFall = 0) then
            Score:= RateExplosion(Me, EX, EY, 101, afTrackFall or afErasesLand)
        else Score:= RateExplosion(Me, EX, EY, 101)
    else
        Score:= BadTurn;

    if (valueResult < Score) and (Score > 0) then
        begin
        ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random(Level * 3));
        ap.Power:= trunc(sqrt(r) * cMaxPower) + AIrndSign(random(Level) * 20);
        ap.Time:= TestTime;
        ap.ExplR:= 100;
        ap.ExplX:= EX;
        ap.ExplY:= EY;
        valueResult:= Score
        end;
    end
//until (Score > 204800) or (TestTime > 4000);
until TestTime > 4500 - Level * 512;
TestGrenade:= valueResult
end;

function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const tDelta = 24;
      Density : real = 1.5;
var Vx, Vy, r: real;
    Score, EX, EY, valueResult: LongInt;
    TestTime: Longword;
    x, y, dX, dY, meX, meY: real;
    t: LongInt;
begin
Flags:= Flags; // avoid compiler hint
valueResult:= BadTurn;
TestTime:= 500;
ap.Bounce:= 0;
ap.ExplR:= 0;
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
repeat
    inc(TestTime, 900);
    if (GameFlags and gfMoreWind) <> 0 then
        Vx:= (-(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5) + ((Targ.Point.X - meX) / (TestTime + tDelta))
    // Try to overshoot slightly, seems to pay slightly better dividends in terms of hitting cluster
    else if meX<Targ.Point.X then
        Vx:= ((Targ.Point.X+10) - meX) / (TestTime + tDelta)
    else
        Vx:= ((Targ.Point.X-10) - meX) / (TestTime + tDelta);
    Vy:= aiGravityf * ((TestTime + tDelta) div 2) - ((Targ.Point.Y-50) - meY) / (TestTime + tDelta);
    r:= sqr(Vx)+sqr(Vy);
    if not (r > 1) then
        begin
        x:= meX;
        dX:= Vx;
        y:= meY;
        dY:= -Vy;
        t:= TestTime;
    repeat
        x:= x + dX;
        if (GameFlags and gfMoreWind) <> 0 then
            dX:= dX + aiWindSpeed / Density;
        y:= y + dY;
        dY:= dY + aiGravityf;
        dec(t)
    until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or
           ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t = 0);
    EX:= trunc(x);
    EY:= trunc(y);
    if t < 50 then
        Score:= RateExplosion(Me, EX, EY, 41)
    else
        Score:= BadTurn;

     if Score > 0 then
        begin
        ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random(Level * 2));
        ap.Power:= trunc(sqrt(r) * cMaxPower) + AIrndSign(random(Level) * 15);
        ap.Time:= TestTime div 1000 * 1000;
        ap.ExplR:= 90;
        ap.ExplX:= EX;
        ap.ExplY:= EY;
        valueResult:= Score
        end;
     end
until (TestTime = 4100);
TestClusterBomb:= valueResult
end;

function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const tDelta = 24;
      Density : real = 2.0;
var Vx, Vy, r: real;
    Score, EX, EY, valueResult: LongInt;
    TestTime: Longword;
    targXWrap, x, y, dX, dY, meX, meY: real;
    t: LongInt;
begin
Flags:= Flags; // avoid compiler hint
valueResult:= BadTurn;
TestTime:= 500;
ap.ExplR:= 0;
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else targXWrap:= Targ.Point.X - (RightX-LeftX);
repeat
    inc(TestTime, 900);
    if (WorldEdge = weWrap) and (random(2)=0) then
        Vx:= (targXWrap - meX) / (TestTime + tDelta)
    else
        Vx:= (Targ.Point.X - meX) / (TestTime + tDelta);
    if (GameFlags and gfMoreWind) <> 0 then
        Vx:= -(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5 + Vx;

    Vy:= aiGravityf * ((TestTime + tDelta) div 2) - ((Targ.Point.Y-50) - meY) / (TestTime + tDelta);
    r:= sqr(Vx)+sqr(Vy);
    if not (r > 1) then
        begin
        x:= meX;
        dX:= Vx;
        y:= meY;
        dY:= -Vy;
        t:= TestTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                 dX:= dX + aiWindSpeed / Density;
            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
       until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 6)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 6))) or (t = 0);

        EX:= trunc(x);
        EY:= trunc(y);
        if t < 50 then
            Score:= RateExplosion(Me, EX, EY, 200) + RateExplosion(Me, EX, EY + 120, 200)
        else
            Score:= BadTurn;

        if valueResult < Score then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random(Level));
            ap.Power:= trunc(sqrt(r) * cMaxPower) + AIrndSign(random(Level) * 15);
            ap.Time:= TestTime div 1000 * 1000;
            ap.ExplR:= 300;
            ap.ExplX:= EX;
            ap.ExplY:= EY;
            valueResult:= Score
            end;
        end
until (TestTime = 4100);
TestWatermelon:= valueResult
end;


    function Solve(TX, TY, MX, MY: LongInt): LongWord;
    var A, B, D, T: real;
        C: LongInt;
    begin
        A:= sqr(aiGravityf);
        B:= - aiGravityf * (TY - MY) - 1;
        C:= sqr(TY - MY) + sqr(TX - MX);
        D:= sqr(B) - A * C;
        if D >= 0 then
            begin
            D:= sqrt(D) - B;
            if D >= 0 then
                T:= sqrt(D * 2 / A)
            else
                T:= 0;
            Solve:= trunc(T)
            end
            else
                Solve:= 0
    end;

function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const Density : real = 1.0;
var Vx, Vy: real;
    Score, EX, EY: LongInt;
    TestTime: Longword;
    x, y, dX, dY, meX, meY: real;
begin
Flags:= Flags; // avoid compiler hint
    TestMortar:= BadTurn;
    ap.ExplR:= 0;

    meX:= hwFloat2Float(Me^.X);
    meY:= hwFloat2Float(Me^.Y);

    if (Level > 2) then
        exit(BadTurn);

    TestTime:= Solve(Targ.Point.X, Targ.Point.Y, trunc(meX), trunc(meY));

    if TestTime = 0 then
        exit(BadTurn);

    Vx:= (Targ.Point.X - meX) / TestTime;
    if (GameFlags and gfMoreWind) <> 0 then
        Vx:= -(aiWindSpeed / Density) * TestTime * 0.5 + Vx;
    Vy:= aiGravityf * (TestTime div 2) - (Targ.Point.Y - meY) / TestTime;

    x:= meX;
    dX:= Vx;
    y:= meY;
    dY:= -Vy;

    repeat
        x:= x + dX;
        if (GameFlags and gfMoreWind) <> 0 then
            dX:= dX + aiWindSpeed / Density;
        y:= y + dY;
        dY:= dY + aiGravityf;
        EX:= trunc(x);
        EY:= trunc(y);
    until (((Me = CurrentHedgehog^.Gear) and TestColl(EX, EY, 4)) or
           ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, EX, EY, 4))) or (EY > cWaterLine);

    if (EY < cWaterLine) and (dY >= 0) then
        begin
        Score:= RateExplosion(Me, EX, EY, 91);
        if (Score = 0) then
            if (dY > 0.15) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
                Score:= - abs(Targ.Point.Y - EY) div 32
            else
                Score:= BadTurn
        else if (Score < 0) then
            Score:= BadTurn
        end
    else
        Score:= BadTurn;

    if Score > 0 then
        begin
        ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random(Level));
        ap.Power:= 1;
        ap.ExplR:= 100;
        ap.ExplX:= EX;
        ap.ExplY:= EY;
        TestMortar:= Score
        end;
end;

function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const
    MIN_RANGE =  80;
    MAX_RANGE = 400;
var Vx, Vy, x, y: real;
    rx, ry, valueResult: LongInt;
    range: integer;
begin
Flags:= Flags; // avoid compiler hint
TestShotgun:= BadTurn;
ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
x:= hwFloat2Float(Me^.X);
y:= hwFloat2Float(Me^.Y);
range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y);
// Range limits (laser sight can remove upper range limit)
if (range < MIN_RANGE) or ((range > MAX_RANGE) and (not aiLaserSighting) and (Level >= 4))then
    exit(BadTurn);

Vx:= (Targ.Point.X - x) * 1 / 1024;
Vy:= (Targ.Point.Y - y) * 1 / 1024;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
// Apply inaccuracy
if (not aiLaserSighting) then
    inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));
repeat
    x:= x + vX;
    y:= y + vY;
    rx:= trunc(x);
    ry:= trunc(y);
    if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 2)) or
        ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 2)) then
    begin
        x:= x + vX * 8;
        y:= y + vY * 8;
        valueResult:= RateShotgun(Me, vX, vY, rx, ry);

        if (valueResult = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            begin
            if GameFlags and gfSolidLand = 0 then
                 valueResult:= 1024 - Metric(Targ.Point.X, Targ.Point.Y, rx, ry) div 64
            else valueResult := BadTurn
            end
        else
            dec(valueResult, Level * 4000);
        // 27/20 is reuse bonus
        exit(valueResult * 27 div 20)
    end
until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT);

TestShotgun:= BadTurn
end;

function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var Vx, Vy, x, y, t: real;
    d: Longword;
    ix, iy, valueResult: LongInt;
begin
Flags:= Flags; // avoid compiler hint
if (Level > 4) or (Targ.Score < 0) or (Targ.Kind <> gtHedgehog) then exit(BadTurn);
Level:= Level; // avoid compiler hint
ap.ExplR:= 1;
ap.Time:= 0;
ap.Power:= 1;

x:= hwFloat2Float(Me^.X);
y:= hwFloat2Float(Me^.Y);

if Abs(trunc(x) - Targ.Point.X) + Abs(trunc(y) - Targ.Point.Y) < 20 then
    exit(BadTurn);

t:= 2 / sqrt(sqr(Targ.Point.X - x)+sqr(Targ.Point.Y-y));
Vx:= (Targ.Point.X - x) * t;
Vy:= (Targ.Point.Y - y) * t;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
// Apply inaccuracy
if (not aiLaserSighting) then
    inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));
d:= 0;

ix:= trunc(x);
iy:= trunc(y);

if ((ix and LAND_WIDTH_MASK) = 0) and ((iy and LAND_HEIGHT_MASK) = 0) then
    repeat
        if Land[iy, ix] <> 0 then
            inc(d);
        x:= x + vX;
        y:= y + vY;
        ix:= trunc(x);
        iy:= trunc(y);
    until (Abs(Targ.Point.X - ix) + Abs(Targ.Point.Y - iy) < 5)
        or (x < 0)
        or (y < 0)
        or (ix >= LAND_WIDTH)
        or (iy >= LAND_HEIGHT)
        or (d > 48);

if Abs(Targ.Point.X - ix) + Abs(Targ.Point.Y - iy) < 5 then
    begin
    ap.AttacksNum:= 1 + (d + 8) div 12;
    valueResult:= RateShove(Me, Targ.Point.X, Targ.Point.Y, 1, 7, 20, vX*0.125, vY*0.125, afTrackFall) - ap.AttacksNum
    end
else
    valueResult:= BadTurn;

TestDesertEagle:= valueResult
end;


function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var Vx, Vy, x, y, t, dmg: real;
    d: Longword;
    //fallDmg: LongInt;
begin
Flags:= Flags; // avoid compiler hint
if (Level > 3) or (Targ.Score < 0) or (Targ.Kind <> gtHedgehog) then exit(BadTurn);
Level:= Level; // avoid compiler hint
ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
x:= hwFloat2Float(Me^.X);
y:= hwFloat2Float(Me^.Y);
if Abs(trunc(x) - Targ.Point.X) + Abs(trunc(y) - Targ.Point.Y) < 40 then
    exit(BadTurn);

dmg:= sqrt(sqr(Targ.Point.X - x)+sqr(Targ.Point.Y-y));
t:= 1.5 / dmg;
dmg:= dmg * 0.025; // div 40
Vx:= (Targ.Point.X - x) * t;
Vy:= (Targ.Point.Y - y) * t;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
// Apply inaccuracy
inc(ap.Angle, AIrndSign(random((Level - 1) * 5)));

d:= 0;

repeat
    x:= x + vX;
    y:= y + vY;
    if ((trunc(x) and LAND_WIDTH_MASK) = 0)and((trunc(y) and LAND_HEIGHT_MASK) = 0)
    and (Land[trunc(y), trunc(x)] <> 0) then
        inc(d);
until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT)
    or (d > 22);

if Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4 then
     TestSniperRifle:= RateShove(Me, Targ.Point.X, Targ.Point.Y, 1, trunc(dmg), 20, vX*0.166, vY*0.166, afTrackFall)
else TestSniperRifle:= BadTurn;
end;


function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var valueResult, a, v1, v2: LongInt;
    x, y, trackFall: LongInt;
    dx, dy: real;
begin
Flags:= Flags; // avoid compiler hint
    Targ:= Targ; // avoid compiler hint

    if Level < 3 then trackFall:= afTrackFall
        else trackFall:= 0;

    ap.ExplR:= 0;
    ap.Time:= 0;
    ap.Power:= 1;
    x:= hwRound(Me^.X);
    y:= hwRound(Me^.Y);

    a:= cMaxAngle div 2;
    valueResult:= 0;

    while a >= 0 do
        begin
        dx:= sin(a / cMaxAngle * pi) * 0.5;
        dy:= cos(a / cMaxAngle * pi) * 0.5;

        v1:= RateShove(Me, x - 10, y + 2
                , 32, 30, 115
                , -dx, -dy, trackFall);
        v2:= RateShove(Me, x + 10, y + 2
                , 32, 30, 115
                , dx, -dy, trackFall);
        if (v1 > valueResult) or (v2 > valueResult) then
            if (v2 > v1)
                or {don't encourage turning for no gain}((v2 = v1) and (not Me^.dX.isNegative)) then
                begin
                ap.Angle:= a;
                valueResult:= v2
                end
            else
                begin
                ap.Angle:= -a;
                valueResult:= v1
                end;

        a:= a - 15 - random(cMaxAngle div 16)
        end;

    if valueResult <= 0 then
        valueResult:= BadTurn;

    TestBaseballBat:= valueResult;
end;

function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var valueResult, v1, v2, i: LongInt;
    x, y, trackFall: LongInt;
begin
Flags:= Flags; // avoid compiler hint
    Targ:= Targ; // avoid compiler hint

    if Level = 1 then trackFall:= afTrackFall
    else trackFall:= 0;

    ap.ExplR:= 0;
    ap.Time:= 0;
    ap.Power:= 1;
    x:= hwRound(Me^.X);
    y:= hwRound(Me^.Y) + 4;

    v1:= 0;
    for i:= 0 to 8 do
        begin
        v1:= v1 + RateShove(Me, x - 5, y - 10 * i
                , 19, 30, 40
                , -0.45, -0.9, trackFall or afSetSkip);
        end;
    v1:= v1 + RateShove(Me, x - 5, y - 90
            , 19, 30, 40
            , -0.45, -0.9, trackFall);


    // now try opposite direction
    v2:= 0;
    for i:= 0 to 8 do
        begin
        v2:= v2 + RateShove(Me, x + 5, y - 10 * i
                , 19, 30, 40
                , 0.45, -0.9, trackFall or afSetSkip);
        end;
    v2:= v2 + RateShove(Me, x + 5, y - 90
            , 19, 30, 40
            , 0.45, -0.9, trackFall);

    if (v2 > v1)
        or {don't encourage turning for no gain}((v2 = v1) and (not Me^.dX.isNegative)) then
        begin
        ap.Angle:= 1;
        valueResult:= v2
        end
    else
        begin
        ap.Angle:= -1;
        valueResult:= v1
        end;

    if valueResult <= 0 then
        valueResult:= BadTurn;

    TestFirePunch:= valueResult;
end;


function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var valueResult, v1, v2: LongInt;
    x, y, trackFall: LongInt;
begin
Flags:= Flags; // avoid compiler hint
    Targ:= Targ; // avoid compiler hint

    if Level = 1 then trackFall:= afTrackFall
    else trackFall:= 0;

    ap.ExplR:= 0;
    ap.Time:= 0;
    ap.Power:= 1;
    x:= hwRound(Me^.X);
    y:= hwRound(Me^.Y);

    // check left direction
    {first RateShove checks farthermost of two whip's AmmoShove attacks
    to encourage distant attacks (damaged hog is excluded from view of second
    RateShove call)}
    v1:= RateShove(Me, x - 13, y
            , 30, 30, 25
            , -1, -0.8, trackFall or afSetSkip);
    v1:= v1 +
        RateShove(Me, x - 2, y
            , 30, 30, 25
            , -1, -0.8, trackFall);
    // now try opposite direction
    v2:= RateShove(Me, x + 13, y
            , 30, 30, 25
            , 1, -0.8, trackFall or afSetSkip);
    v2:= v2 +
        RateShove(Me, x + 2, y
            , 30, 30, 25
            , 1, -0.8, trackFall);

    if (v2 > v1)
        or {don't encourage turning for no gain}((v2 = v1) and (not Me^.dX.isNegative)) then
        begin
        ap.Angle:= 1;
        valueResult:= v2
        end
    else
        begin
        ap.Angle:= -1;
        valueResult:= v1
        end;

    if valueResult <= 0 then
        valueResult:= BadTurn
    else
        inc(valueResult);

    TestWhip:= valueResult;
end;

function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const step = 8;
var valueResult, i, v, tx: LongInt;
    trackFall: LongInt;
    t, d, x, y, dx, dy, cx: real;
begin
Flags:= Flags; // avoid compiler hint
    ap.ExplR:= 0;
    ap.Time:= 0;
    ap.Power:= 1;

    if Level = 1 then
        trackFall:= afTrackFall
    else if Level = 2 then
        trackFall:= 0
    else
        exit(BadTurn);

    // Don't sacrifice last hog
    if aiHogsInTeam <= 1 then
        exit(BadTurn);

    valueResult:= 0;
    v:= 0;

    x:= hwFloat2Float(Me^.X);
    y:= hwFloat2Float(Me^.Y);
    d:= sqrt(sqr(Targ.Point.X - x) + sqr(Targ.Point.Y - y));
    if d < 10 then
        begin
        dx:= 0;
        dy:= step;
        ap.Angle:= 2048
        end
    else
        begin
        t:= step / d;
        dx:= (Targ.Point.X - x) * t;
        dy:= (Targ.Point.Y - y) * t;

        ap.Angle:= DxDy2AttackAnglef(dx, -dy);
        // Apply inaccuracy
        if (not aiLaserSighting) then
            inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));
        end;

    if dx >= 0 then cx:= 0.45 else cx:= -0.45;

    for i:= 1 to 512 div step - 2 do
        begin
        x:= x + dx;
        y:= y + dy;

        valueResult:= valueResult +
            RateShove(Me, trunc(x), trunc(y)
                , 30, 30, 25
                , cx, -0.9, trackFall or afSetSkip or afIgnoreMe);
        end;

    if (d < 10) and (dx = 0) then
        begin
        x:= hwFloat2Float(Me^.X);
        y:= hwFloat2Float(Me^.Y);
        tx:= trunc(x);
        v:= RateShove(Me, tx, trunc(y)
                , 30, 30, 25
                , -cx, -0.9, trackFall or afIgnoreMe);
        for i:= 1 to 512 div step - 2 do
            begin
            y:= y + dy;
            v:= v +
                RateShove(Me, tx, trunc(y)
                    , 30, 30, 25
                    , -cx, -0.9, trackFall or afSetSkip or afIgnoreMe);
            end
        end;

    if v > valueResult then
    begin
        cx:= -cx;
        ap.Angle:= -2048;
        valueResult:= v
        end;

    v:= RateShove(Me, trunc(x), trunc(y)
            , 30, 30, 25
            , cx, -0.9, trackFall or afIgnoreMe);
    valueResult:= valueResult + v - KillScore * friendlyfactor div 100 * 1024;

    if v < 65536 then
        inc(valueResult, RateExplosion(Me, trunc(x), trunc(y), 30, afIgnoreMe));

    TestKamikaze:= valueResult;
end;

function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var rate: LongInt;
begin
Flags:= Flags; // avoid compiler hint
Level:= Level; // avoid compiler hint
Targ:= Targ;

ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
ap.Angle:= 0;

rate:= RateHammer(Me);
if rate = 0 then
    rate:= BadTurn;
TestHammer:= rate;
end;

function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const cShift = 4;
      Density : real = 2.0;
var bombsSpeed, X, Y, dX, dY: real;
    b: array[0..9] of boolean;
    dmg: array[0..9] of LongInt;
    fexit, firstHit: boolean;
    i, t, valueResult, targetY: LongInt;
begin
Flags:= Flags; // avoid compiler hint
ap.ExplR:= 0;
ap.Time:= 0;
if (Level > 3) or (aiGravityf <= 0) then
    exit(BadTurn);

ap.Angle:= 0;
ap.AttackPutX:= Targ.Point.X;
ap.AttackPutY:= Targ.Point.Y;

bombsSpeed:= hwFloat2Float(cBombsSpeed);
X:= Targ.Point.X - 135 - cShift; // hh center - cShift
X:= X - bombsSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf);
Y:= topY - 300;

dX:= bombsSpeed;
dY:= 0;

for i:= 0 to 9 do
    begin
    b[i]:= true;
    dmg[i]:= 0
    end;
valueResult:= 0;
firstHit:= false;

repeat
    X:= X + dX;
    if (GameFlags and gfMoreWind) <> 0 then
        dX:= dX + aiWindSpeed / Density;
    Y:= Y + dY;
    dY:= dY + aiGravityf;
    fexit:= true;

    for i:= 0 to 9 do
        if b[i] then
            begin
            fexit:= false;
            if TestColl(trunc(X) + LongWord(i * 30), trunc(Y), 4) then
                begin
                b[i]:= false;
                if Level = 1 then
                    dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 58, afTrackFall or afErasesLand)
                else
                    dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 58);
                // 58 (instead of 60) for better prediction (hh moves after explosion of one of the rockets)
                if (not firstHit) then
                    begin
                    firstHit:= true;
                    // remember Y of first hit, used for target pos
                    targetY:= trunc(Y);
                    end;
                end
            end;
until fexit or (Y > cWaterLine);

for i:= 0 to 5 do
    if dmg[i] <> BadTurn then
        inc(valueResult, dmg[i]);
t:= valueResult;
ap.AttackPutX:= Targ.Point.X - 60;
if firstHit then
    // this is not strictly neccessry, it's just to make sure
    // the X is on the height of the first hit
    ap.AttackPutY:= targetY;

for i:= 0 to 3 do
    if dmg[i] <> BadTurn then
        begin
        dec(t, dmg[i]);
        inc(t, dmg[i + 6]);
        if t > valueResult then
            begin
            valueResult:= t;
            ap.AttackPutX:= Targ.Point.X - 30 - cShift + i * 30
            end
        end;

if valueResult <= 0 then
    valueResult:= BadTurn;
TestAirAttack:= valueResult;
end;

function TestResurrector(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var rate, heal: LongInt;
begin
Flags:= Flags; // avoid compiler hint
Targ:= Targ;

if (Level = 5) then
    exit(BadTurn);

if (Me^.Health <= 1) then
    exit(BadTurn);

if (Level <= 2) and (Me^.Hedgehog^.Effects[hePoisoned] > 0) then
    // Sacrifice almost all health if poisoned
    heal:= Me^.Health - 1
else
    // Sacrifice up to 10% of own health
    heal:= (Me^.Health div 10);

ap.ExplR:= 0;
ap.Time:= 0;
if (Level >= 4) then
    // slow resurrect
    ap.Power:= max(512 * heal - 512, 10)
else
    // fast resurrect
    ap.Power:= max(16 * heal - 16, 10);

// Time limit
ap.Power:= min(ap.Power, 5000);

ap.Angle:= 0;

rate:= RateResurrector(Me);
if rate <= 0 then
    rate:= BadTurn;
TestResurrector:= rate;
end;

function TestDrillStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const cShift = 4;
      Density : real = 1.0;
var bombsSpeed, X, Y, dX, dY, drillX, drillY: real;
    t2: real;
    dmg: array[0..9] of LongInt;
    collided, drilling, timerRuns, firstHit: boolean;
    i, t, value, valueResult, attackTime, drillTimer, targetX, targetY: LongInt;
begin
Flags:= Flags; // avoid compiler hint
ap.ExplR:= 0;
if (Level > 3) or (aiGravityf <= 0) then
    exit(BadTurn);

ap.Angle:= 0;
targetX:= Targ.Point.X;
ap.AttackPutY:= Targ.Point.Y;

bombsSpeed:= hwFloat2Float(cBombsSpeed);
X:= Targ.Point.X - 135 - cShift; // hh center - cShift
X:= X - bombsSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf);
Y:= topY - 300;

valueResult:= 0;

attackTime:= 6000;
while attackTime >= 0 do
    begin
    dec(attackTime, 1000);
    value:= 0;
    firstHit:= false;
    for i:= 0 to 9 do
        begin
        dmg[i]:= 0;
        drillX:= trunc(X) + LongWord(i * 30);
        drillY:= trunc(Y);
        dX:= bombsSpeed;
        dY:= 0;
        collided:= false;
        drilling:= false;
        timerRuns:= false;
        drillTimer := attackTime;

        repeat
            // Simulate in-air movement
            drillX:= drillX + dX;
            drillY:= drillY + dY;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density;
            dY:= dY + aiGravityf;

            if timerRuns then
                dec(drillTimer);

            // Collided with land ... simulate drilling
            if (drillTimer > 0) and TestCollExcludingObjects(trunc(drillX), trunc(drillY), 4) and
                (Abs(Targ.Point.X - trunc(drillX)) + Abs(Targ.Point.Y - trunc(drillY)) > 21) then
                begin
                drilling := true;
                timerRuns := true;
                t2 := 0.5 / sqrt(sqr(dX) + sqr(dY));
                dX := dX * t2;
                dY := dY * t2;
                repeat
                    drillX:= drillX + dX;
                    drillY:= drillY + dY;
                    dec(drillTimer, 10);
                    if (Abs(Targ.Point.X - drillX) + Abs(Targ.Point.Y - drillY) < 22)
                        or (drillX < -32)
                        or (drillY < -32)
                        or (trunc(drillX) > LAND_WIDTH + 32)
                        or (trunc(drillY) > cWaterLine)
                        or (drillTimer <= 0) then
                        collided:= true
                    else if not TestCollExcludingObjects(trunc(drillX), trunc(drillY), 4) then
                        drilling:= false;
                until (collided or (not drilling));
                end
            // Collided with something else ... record collision
            else if (drillTimer <= 0) or TestColl(trunc(drillX), trunc(drillY), 4) then
                collided:= true;

            // Simulate explosion
            if collided then
                begin
                if Level = 1 then
                    dmg[i]:= RateExplosion(Me, trunc(drillX), trunc(drillY), 58, afTrackFall or afErasesLand)
                else
                    dmg[i]:= RateExplosion(Me, trunc(drillX), trunc(drillY), 58);
                // 58 (instead of 60) for better prediction (hh moves after explosion of one of the rockets)
                if not firstHit then
                    begin
                    targetY:= trunc(drillY);
                    firstHit:= true;
                    end;
                end;
        until collided or (drillY > cWaterLine);
        end;

    // calculate score
    for i:= 0 to 5 do
        if dmg[i] <> BadTurn then
            inc(value, dmg[i]);
    t:= value;
    targetX:= Targ.Point.X - 60;

    for i:= 0 to 3 do
        if dmg[i] <> BadTurn then
            begin
            dec(t, dmg[i]);
            inc(t, dmg[i + 6]);
            if t > value then
                begin
                value:= t;
                targetX:= Targ.Point.X - 30 - cShift + i * 30
                end
            end;

    if value > valueResult then
        begin
        valueResult:= value;
        ap.AttackPutX:= targetX;
        if firstHit then
            ap.AttackPutY:= targetY;
        ap.Time:= attackTime;
        end;
end;

if valueResult <= 0 then
    valueResult:= BadTurn
else
    begin
    // Weaker AI has chance to get the time wrong by 1-3 seconds
    if Level = 5 then
        // +/- 3 seconds
        ap.Time:= ap.Time + (3 - random(7)) * 1000
    else if Level = 4 then
        // +/- 2 seconds
        ap.Time:= ap.Time + (2 - random(5)) * 1000
    else if Level = 3 then
        // +/- 1 second
        if (random(2) = 0) then
            ap.Time:= ap.Time + (1 - random(3)) * 1000
    else if Level = 2 then
        // 50% chance for +/- 1 second
        if (random(2) = 0) then
            ap.Time:= ap.Time + (1 - random(3)) * 1000;
    ap.Time:= Min(5000, Max(1000, ap.Time));
    end;

TestDrillStrike:= valueResult;
end;

function TestMineStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const cShift = 4;
      Density : real = 1.0;
var minesSpeed, X, Y, dX, dY: real;
    b: array[0..9] of boolean;
    dmg: array[0..9] of LongInt;
    fexit, firstHit: boolean;
    i, t, valueResult, targetY: LongInt;
begin
Flags:= Flags; // avoid compiler hint
ap.ExplR:= 0;
ap.Time:= 0;

// AI currently only supports cMinesTime = 0 because it's the most
// predictable.
// Other cMinesTime values are risky because of bouncy mines;
// so they are unsupported.
// TODO: Implement mine strike for other values of MineTime
// TODO: Teach AI to avoid hitting their own with mines
if (Level > 3) or (aiGravityf <= 0) or (cMinesTime <> 0) then
    exit(BadTurn);

ap.Angle:= 0;
ap.AttackPutX:= Targ.Point.X;
ap.AttackPutY:= Targ.Point.Y;

minesSpeed:= hwFloat2Float(cBombsSpeed);
X:= Targ.Point.X - 135 - cShift; // hh center - cShift
X:= X - minesSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf);
Y:= topY - 300;
dX:= minesSpeed;
dY:= 0;

for i:= 0 to 9 do
    begin
    b[i]:= true;
    dmg[i]:= 0
    end;
valueResult:= 0;
firstHit:= false;

repeat
    X:= X + dX;
    if (GameFlags and (gfMoreWind or gfInfAttack)) <> 0 then
        dX:= dX + aiWindSpeed / Density;
    Y:= Y + dY;
    dY:= dY + aiGravityf;
    fexit:= true;

    for i:= 0 to 9 do
        if b[i] then
            begin
            fexit:= false;
            if TestColl(trunc(X) + LongWord(i * 30), trunc(Y), 4) then
                begin
                b[i]:= false;
                if Level = 1 then
                    dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 96, afTrackFall or afErasesLand)
                else
                    dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 96);

                if (not firstHit) then
                    begin
                    targetY:= trunc(Y);
                    firstHit:= true;
                    end;
                end
            end;
until fexit or (Y > cWaterLine);

for i:= 0 to 5 do
    if dmg[i] <> BadTurn then
        inc(valueResult, dmg[i]);
t:= valueResult;
ap.AttackPutX:= Targ.Point.X - 60;
if firstHit then
    ap.AttackPutY:= targetY;

for i:= 0 to 3 do
    if dmg[i] <> BadTurn then
        begin
        dec(t, dmg[i]);
        inc(t, dmg[i + 6]);
        if t > valueResult then
            begin
            valueResult:= t;
            ap.AttackPutX:= Targ.Point.X - 30 - cShift + i * 30
            end
        end;

if valueResult <= 0 then
    valueResult:= BadTurn;
TestMineStrike:= valueResult;
end;

function TestSineGun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const
    MIN_RANGE =  40;
    MAX_RANGE = 400;
var Vx, Vy, x, y: real;
    rx, ry, value, valueResult: LongInt;
    range: integer;
    landStop: boolean;
begin
// TODO: Also simulate the sine gun's kickback and make sure the attacker
// doesn't get hurt

Flags:= Flags; // avoid compiler hint
TestSineGun:= BadTurn;
valueResult:= 0;
ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
x:= hwFloat2Float(Me^.X);
y:= hwFloat2Float(Me^.Y);
range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y);
// Range limits
if (range < MIN_RANGE) or ((range > MAX_RANGE) and (Level >= 4)) then
    exit(BadTurn);

Vx:= (Targ.Point.X - x) * 1 / 1024;
Vy:= (Targ.Point.Y - y) * 1 / 1024;

// Never shoot downwards or horizontal because it's dangerous
// and we don't have a proper check for kickback yet
if Vy >= 0 then
    exit(BadTurn);

landStop:= false;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
// Apply inaccuracy
if (not aiLaserSighting) then
    inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));
repeat
    x:= x + vX;
    y:= y + vY;
    rx:= trunc(x);
    ry:= trunc(y);
    if (GameFlags and gfSolidLand) <> 0 then
        // Stop projectile when colliding with indestructible land
        landStop:= (Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 5);

    if landStop or
       ((Me = CurrentHedgehog^.Gear) and TestCollHogsOrObjects(rx, ry, 5)) or
       ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 5)) then
    begin
        x:= x + vX * 8;
        y:= y + vY * 8;
        value:= RateShove(Me, rx, ry, 5, 35, 50, vX, vY, afTrackFall);

        if (value > 0) then
            inc(valueResult, value);
    end
until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 5)
    or (landStop)
    or (Metric(trunc(x), trunc(y), hwRound(Me^.X), hwRound(Me^.Y)) > MAX_RANGE)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT);

if valueResult <= 0 then
    valueResult:= BadTurn;
TestSineGun:= valueResult;
end;

function TestSMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const timeLimit = 50;
      Density : real = 1.6;
var Vx, Vy, r, meX, meY: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY: real;
    t: LongInt;
    value: LongInt;
begin
Flags:= Flags; // avoid compiler hint
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
ap.Time:= 0;
rTime:= 350;
ap.ExplR:= 0;
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else targXWrap:= Targ.Point.X - (RightX-LeftX);
valueResult:= BadTurn;
repeat
    rTime:= rTime + 300 + Level * 50 + random(300);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime
    else
         Vx:= (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime;
    if (GameFlags and gfMoreWind) <> 0 then
         Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx;
    Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime;
    r:= sqr(Vx) + sqr(Vy);

    if not (r > 1) then
        begin
        x:= meX;
        y:= meY;
        dX:= Vx;
        dY:= -Vy;
        t:= rTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density;

            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 2)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 2))) or (t < -timeLimit);

        EX:= trunc(x);
        EY:= trunc(y);

        if t >= -timeLimit then
            if (Level = 1) and (Flags and amtest_NoTrackFall = 0) then
                value:= RateExplosion(Me, EX, EY, 61, afTrackFall)
            else
                value:= RateExplosion(Me, EX, EY, 61)
        else
            value:= BadTurn;

        if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            value := BadTurn;

        if (valueResult < value) or ((valueResult = value) and (Level < 3)) then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9));
            ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1);
            ap.ExplR:= 60;
            ap.ExplX:= EX;
            ap.ExplY:= EY;
            valueResult:= value
            end;
        end
until rTime > 5050 - Level * 800;
if valueResult <> BadTurn then
    // 27/20 is reuse bonus
    valueResult:= valueResult * 27 div 20;
TestSMine:= valueResult
end;

function TestPiano(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
// TODO: Test all 5 bounces
const BOUNCES = 1; // we only test 1 bounce to avoid the rating getting excessively high
var X, Y: real;
    dmg: array[0..BOUNCES-1] of LongInt;
    i, e, rate, valueResult, targetY: LongInt;
    firstHit, solidBounce: boolean;
begin
Flags:= Flags; // avoid compiler hint
ap.ExplR:= 0;
ap.Time:= 0;
if (aiGravityf <= 0) then
    exit(BadTurn);

if (Level > 2) then
    exit(BadTurn);

// Don't sacrifice last hog
if aiHogsInTeam <= 1 then
    exit(BadTurn);

ap.Angle:= 0;
ap.AttackPutX:= Targ.Point.X;
ap.AttackPutY:= Targ.Point.Y;

X:= Targ.Point.X;
Y:= -1024;

for i:= 0 to BOUNCES-1 do
    dmg[i]:= 0;

i:= 1;
firstHit:= false;
solidBounce:= false;
repeat
    // Piano goes down
    if (not solidBounce) then
        Y:= Y + 11;
    if solidBounce or TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(X), trunc(Y), 32) then
        begin
        if (not firstHit) then
            targetY:= trunc(Y);
        firstHit:= true;
        if (GameFlags and gfSolidLand) <> 0 then
            // Don't change Y when indestructible land was hit
            solidBounce:= true;

        for e:= -1 to 1 do
            begin
            // TODO: RateExplosion should remember hogs that already died in the simulation;
            // because currently, dead hogs might still be counted for damage
            rate:= RateExplosion(Me, trunc(X) + 30*e, trunc(Y)+40, 161, afIgnoreMe);
            if rate <> BadTurn then
                dmg[i]:= dmg[i] + rate;
            end;

        if (i > 1) and (dmg[i] > 0) then
            dmg[i]:= dmg[i] div 2;
        inc(i);
        if (not solidBounce) then
            // Skip past the blast hole
            Y:= Y + 41
        end;
until (i > BOUNCES) or (Y > cWaterLine);

if (i = 0) and (Y > cWaterLine) then
    exit(BadTurn);

valueResult:= 0;
for i:= 0 to BOUNCES do
    if dmg[i] <= BadTurn then
        begin
        valueResult:= BadTurn;
        break;
        end
    else
        inc(valueResult, dmg[i]);
ap.AttackPutX:= Targ.Point.X;
if firstHit then
    ap.AttackPutY:= targetY;

valueResult:= valueResult - KillScore * friendlyfactor div 100 * 1024;

if valueResult <= 0 then
    valueResult:= BadTurn;
TestPiano:= valueResult;
end;

function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var
    i, failNum: longword;
    maxTop: longword;
begin
Flags:= Flags; // avoid compiler hint
    TestTeleport := BadTurn;
    exit(BadTurn);
    Level:= Level; // avoid compiler hint
    //FillBonuses(true, [gtCase]);
    if bonuses.Count = 0 then
        begin
        if Me^.Health <= 100  then
            begin
            maxTop := Targ.Point.Y - cHHRadius * 2;

            while not TestColl(Targ.Point.X, maxTop, cHHRadius) and (maxTop > topY + cHHRadius * 2 + 1) do
                dec(maxTop, cHHRadius*2);
            if not TestColl(Targ.Point.X, maxTop + cHHRadius, cHHRadius) then
                begin
                ap.AttackPutX := Targ.Point.X;
                ap.AttackPutY := maxTop + cHHRadius;
                TestTeleport := Targ.Point.Y - maxTop;
                end;
            end;
        end
    else
        begin
        failNum := 0;
        repeat
            i := random(bonuses.Count);
            inc(failNum);
        until not TestColl(bonuses.ar[i].X, bonuses.ar[i].Y - cHHRadius - bonuses.ar[i].Radius, cHHRadius)
        or (failNum = bonuses.Count*2);

        if failNum < bonuses.Count*2 then
            begin
            ap.AttackPutX := bonuses.ar[i].X;
            ap.AttackPutY := bonuses.ar[i].Y - cHHRadius - bonuses.ar[i].Radius;
            TestTeleport := 0;
            end;
        end;
end;


procedure checkCakeWalk(Me, Gear: PGear; var ap: TAttackParams);
var i: Longword;
    v: LongInt;
begin
while (not TestColl(hwRound(Gear^.X), hwRound(Gear^.Y), 6)) and (Gear^.Y.Round < LongWord(LAND_HEIGHT)) do
    Gear^.Y:= Gear^.Y + _1;

for i:= 0 to 2040 do
    begin
    cakeStep(Gear);
    v:= RateExplosion(Me, hwRound(Gear^.X), hwRound(Gear^.Y), cakeDmg * 2, afTrackFall);
    if v > ap.Power then
        begin
        ap.ExplX:= hwRound(Gear^.X);
        ap.ExplY:= hwRound(Gear^.Y);
        ap.Power:= v
        end
    end;
end;

function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var valueResult, v1, v2: LongInt;
    cake: TGear;
begin
Flags:= Flags; // avoid compiler hint
    Targ:= Targ; // avoid compiler hint

    if (Level > 2) then
        exit(BadTurn);

    ap.ExplR:= 0;
    ap.Time:= 0;
    ap.Power:= BadTurn; // use it as max score value in checkCakeWalk

    //FillChar(cake, sizeof(cake), 0);
    cake.Radius:= 7;
    cake.CollisionMask:= lfNotCurHogCrate;
    cake.Hedgehog:= Me^.Hedgehog;
    cake.Kind:= gtCake;

    // check left direction
    cake.Angle:= 3;
    cake.dX.isNegative:= true;
    cake.dX.QWordValue:= 0;
    cake.dY:= _0;
    cake.X:= Me^.X - _3;
    cake.Y:= Me^.Y;
    checkCakeWalk(Me, @cake, ap);
    v1:= ap.Power;

    // now try opposite direction
    cake.Angle:= 1;
    cake.dX.isNegative:= false;
    cake.X:= Me^.X + _3;
    cake.Y:= Me^.Y;
    checkCakeWalk(Me, @cake, ap);
    v2:= ap.Power;

    ap.Power:= 1;

    if (v2 > v1) then
        begin
        ap.Angle:= 1;
        valueResult:= v2
        end
    else
        begin
        ap.Angle:= -1;
        valueResult:= v1
        end;

    if valueResult <= 0 then
        valueResult:= BadTurn;

    TestCake:= valueResult;
end;

function TestSeduction(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
var rate: LongInt;
begin
Flags:= Flags; // avoid compiler hint
Level:= Level; // avoid compiler hint
Targ:= Targ;

if (Level = 5) then
    exit(BadTurn);

ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
ap.Angle:= 0;

rate:= RateSeduction(Me);
if rate <= 0 then
    rate:= BadTurn;
TestSeduction:= rate;
end;

function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const Density : real = 2.0;
var valueResult: LongInt;
    x, y, dx, dy: real;
    EX, EY, t: LongInt;
begin
Flags:= Flags; // avoid compiler hint
Targ:= Targ; // avoid compiler hint

x:= hwFloat2Float(Me^.X) + hwSign(Me^.dX) * 7;
y:= hwFloat2Float(Me^.Y);
dx:= hwSign(Me^.dX) * 0.03;
if (GameFlags and gfMoreWind) <> 0 then
    dx:= -(aiWindSpeed / Density) + dx;
dy:= 0;
t:= 5000;
repeat
    dec(t);
    if (GameFlags and gfMoreWind) <> 0 then
        dx:= dx + aiWindSpeed / Density;
    x:= x + dx;
    dy:= dy + aiGravityf;
    y:= y + dy;

    if TestColl(trunc(x), trunc(y), 3) then
        t:= 0;
until t = 0;

EX:= trunc(x);
EY:= trunc(y);

if Level = 1 then
    valueResult:= RateExplosion(Me, EX, EY, 76, afTrackFall or afErasesLand)
else
    valueResult:= RateExplosion(Me, EX, EY, 76);

if (valueResult > 0) then
    begin
    ap.Angle:= 0;
    ap.Power:= 1;
    ap.Time:= 0;
    ap.ExplR:= 150;
    ap.ExplX:= EX;
    ap.ExplY:= EY
    end else
    valueResult:= BadTurn;

TestDynamite:= valueResult
end;

function TestMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const Density : real = 1.0;
var valueResult: LongInt;
    x, y, dx, dy: real;
    EX, EY, t: LongInt;
begin
Flags:= Flags; // avoid compiler hint
Targ:= Targ; // avoid compiler hint

x:= hwFloat2Float(Me^.X) + hwSign(Me^.dX) * 7;
y:= hwFloat2Float(Me^.Y);
dx:= hwSign(Me^.dX) * 0.02;
if (GameFlags and gfMoreWind) <> 0 then
    dx:= -(aiWindSpeed / Density) + dx;
dy:= 0;
t:= 10000;
repeat
    dec(t);
    if (GameFlags and gfMoreWind) <> 0 then
         dx:= dx + aiWindSpeed / Density;
    x:= x + dx;
    dy:= dy + aiGravityf;
    y:= y + dy;
    if ((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 2)) or
        ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 2)) then
        t:= 0;
until t = 0;

EX:= trunc(x);
EY:= trunc(y);

if Level = 1 then
    valueResult:= RateExplosion(Me, EX, EY, 51, afTrackFall or afErasesLand)
else
    valueResult:= RateExplosion(Me, EX, EY, 51);

if (valueResult > 0) then
    begin
    ap.Angle:= 0;
    ap.Power:= 1;
    ap.Time:= 0;
    if (Level < 5) then
        // Set minimum mine bounciness for improved aim
        ap.Bounce:= 1
    else
        ap.Bounce:= 0;
    ap.ExplR:= 100;
    ap.ExplX:= EX;
    ap.ExplY:= EY
    end else
    valueResult:= BadTurn;

TestMine:= valueResult
end;

function TestKnife(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const timeLimit = 300;
      Density : real = 4.0;
var Vx, Vy, r, meX, meY: real;
    rTime: LongInt;
    EX, EY: LongInt;
    valueResult: LongInt;
    targXWrap, x, y, dX, dY: real;
    t: LongInt;
    value, range: LongInt;
begin
Flags:= Flags; // avoid compiler hint
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
ap.Time:= 0;
rTime:= 350;
ap.ExplR:= 0;
if (WorldEdge = weWrap) then
    if (Targ.Point.X < meX) then
         targXWrap:= Targ.Point.X + (RightX-LeftX)
    else
         targXWrap:= Targ.Point.X - (RightX-LeftX);
valueResult:= BadTurn;
repeat
    rTime:= rTime + 300 + Level * 50 + random(300);
    if (WorldEdge = weWrap) and (random(2)=0) then
         Vx:= (targXWrap - meX) / rTime
    else
         Vx:= (Targ.Point.X - meX) / rTime;
    if (GameFlags and gfMoreWind) <> 0 then
         Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx;
    Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime;
    r:= sqr(Vx) + sqr(Vy);

    if not (r > 1) then
        begin
        x:= meX;
        y:= meY;
        dX:= Vx;
        dY:= -Vy;
        t:= rTime;
        repeat
            x:= CheckWrap(x);
            x:= x + dX;
            if (GameFlags and gfMoreWind) <> 0 then
                dX:= dX + aiWindSpeed / Density;

            y:= y + dY;
            dY:= dY + aiGravityf;
            dec(t)
        until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 7)) or
               ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 7))) or (t < -timeLimit);

        EX:= trunc(x);
        EY:= trunc(y);

        // Sanity check: Make sure we're not too close to impact location
        range:= Metric(trunc(meX), trunc(meY), EX, EY);
        if (range <= 40) then
            value:= BadTurn
        // Timeout
        else if t < -timeLimit then
            value:= BadTurn
        else
            value:= RateShove(Me, EX, EY, 16, trunc(sqr((abs(dY)+abs(dX))*40000/10000)), 0, dX, dY, 0);

        if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            value := BadTurn;

        if (valueResult < value) or ((valueResult = value) and (Level = 1)) then
            begin
            ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 12));
            ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 22 + 1);
            valueResult:= value
            end;
        end
until rTime > 5050 - Level * 800;
TestKnife:= valueResult
end;

function TestAirMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const
    MIN_RANGE = 160;
    MAX_RANGE = 2612;
var Vx, Vy, meX, meY, x, y, r: real;
    rx, ry, valueResult: LongInt;
    range, maxRange: integer;
begin
Flags:= Flags; // avoid compiler hint
maxRange:= MAX_RANGE - ((Level - 1) * 300);
TestAirMine:= BadTurn;
ap.ExplR:= 60;
ap.Time:= 0;
meX:= hwFloat2Float(Me^.X);
meY:= hwFloat2Float(Me^.Y);
x:= meX;
y:= meY;

// Rough first range check
range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y);
if ( range < MIN_RANGE ) or ( range > maxRange ) then
    exit(BadTurn);

Vx:= (Targ.Point.X - x) * 1 / 1024;
Vy:= (Targ.Point.Y - y) * 1 / 1024;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
repeat
    x:= x + vX;
    y:= y + vY;
    rx:= trunc(x);
    ry:= trunc(y);
    if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 8)) or
        ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 8)) then
        begin
        x:= x + vX * 8;
        y:= y + vY * 8;

        if Level = 1 then
            valueResult:= RateExplosion(Me, rx, ry, 61, afTrackFall)
        else
            valueResult:= RateExplosion(Me, rx, ry, 61);

        // Precise range calculation required to calculate power;
        // The air mine is very sensitive to small changes in power.
        r:= sqr(meX - rx) + sqr(meY - ry);
        range:= trunc(sqrt(r));

        if ( range < MIN_RANGE ) or ( range > maxRange ) then
            exit(BadTurn);
        ap.Power:= ((range + cHHRadius*2) * cMaxPower) div MAX_RANGE;

        // Apply inaccuracy
        inc(ap.Power, (random(93*(Level-1)) - 31*(Level-1))); // Level 1 spread: -124 .. 248
        if (not aiLaserSighting) then
            inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));

        if (valueResult <= 0) then
            valueResult:= BadTurn;
        exit(valueResult)
        end
until (abs(Targ.Point.X - trunc(x)) + abs(Targ.Point.Y - trunc(y)) < 4)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT);

TestAirMine := BadTurn
end;

function TestMinigun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt;
const
    MAX_RANGE = 400;
var Vx, Vy, x, y: real;
    rx, ry, valueResult: LongInt;
    range: integer;
begin
// This code is still very similar to TestShotgun,
// but it's a good simple estimate.
// TODO: Simulate random bullets
// TODO: Replace RateShotgun with something else
// TODO: Teach AI to move aim during shooting
Flags:= Flags; // avoid compiler hint
TestMinigun:= BadTurn;
ap.ExplR:= 0;
ap.Time:= 0;
ap.Power:= 1;
x:= hwFloat2Float(Me^.X);
y:= hwFloat2Float(Me^.Y);
range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y);
if ( range > MAX_RANGE ) then
    exit(BadTurn);

Vx:= (Targ.Point.X - x) * 1 / 1024;
Vy:= (Targ.Point.Y - y) * 1 / 1024;
ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
// Minigun angle is limited
if (ap.Angle < Ammoz[amMinigun].minAngle) or (ap.Angle > Ammoz[amMinigun].maxAngle) then
    exit(BadTurn);

// Apply inaccuracy
if (not aiLaserSighting) then
    inc(ap.Angle, AIrndSign(random((Level - 1) * 10)));
repeat
    x:= x + vX;
    y:= y + vY;
    rx:= trunc(x);
    ry:= trunc(y);
    if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 1)) or
        ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 1)) then
    begin
        x:= x + vX * 8;
        y:= y + vY * 8;
        // TODO: Use different rating function
        valueResult:= RateShotgun(Me, vX, vY, rx, ry);

        if (valueResult = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then
            begin
            if GameFlags and gfSolidLand = 0 then
                 valueResult:= 1024 - Metric(Targ.Point.X, Targ.Point.Y, rx, ry) div 64
            else valueResult := BadTurn
            end
        else
            dec(valueResult, Level * 4000);
        exit(valueResult)
    end
until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4)
    or (x < 0)
    or (y < 0)
    or (trunc(x) > LAND_WIDTH)
    or (trunc(y) > LAND_HEIGHT);

TestMinigun:= BadTurn
end;

end.