/* * ai-clever.c -- A smarter computer player. * * Is it too clever for its own good? Too stupid? You decide. * * Written by James Cleverdon, starting from ai-moron.c by Peter Amstutz. * August 15, 1999 * * (Need usual copyleft comments and CVS ID headers around here.) * * Copyright (C) 1999 Peter Amstutz * * 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; either version 2 of *the * License, or (at your option) any later version. * * 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., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include "tcpcore.h" #include "ai.h" #include "relay.h" #include "terrain.h" #include "aihandlers.h" #include "game.h" #include "packets.h" #include "player.h" #include "log.h" #include "rnd.h" #include "ballistics.h" #include "weapons/weapon.h" #include "cfgfile.h" #include "../config.h" #define DEFAULT_PORT 8086 #ifndef ABS #define ABS(x) ((x) < 0 ? -(x) : (x)) #endif #define MAX_VELOCITY 1000 /* Max. allowed velocity */ #define MAX_ANGLE 180 /* Angle ranges from 0 to 180 degrees */ /* A good starting Y velocity component when shooting high arcs. */ #define DEF_VY 840 struct timeval ms100; /* let rlMain() wait 100 milliseconds */ Relay_rl *relay; int svrid, gm_myid = -1; ItemStock_pl *gm_curitem; Player_pl *gm_myplstruct; Gamemode_gm gm_gamemode; char gm_quit = 0; char gm_activate_shots; char gm_stuff_happening; char time_to_fire = 0; char gm_tank_damaged = 0; int gm_activeplayers; int gm_currentRound = -1; int gm_totalRounds = -1; Gametype_gm gm_gametype = SIMULTANEOUS; int gm_AS_queue[256] = { 0 }; int gm_AS_pos = 0; int gm_death_queue[256] = { 0 }; int gm_dq_pos = 0; int curShooterId=0; int gm_iAmServer = 0; /* total waste of mem */ Player_pl *gm_firing_order[10]; static double clv_angle; /* Current cannon angle and velocity */ static double clv_velocity; enum { SR_HIT = 0, SR_NEW_TARGET, SR_OVERSHOT, SR_UNDERSHOT } clv_shot_result; /* * The revenge list is a circular linked list with a permanent head. It's * player pointer is NULL so it never matches. */ static REVENGE rvg_head = { &rvg_head, &rvg_head, NULL, -999 }; static Player_pl *ai_target = NULL; /* My current target. */ static RNDLIST *clv_name_list; static char *clv_name_array[] = { "Berserker", "Big Bad John", "Bomberman", "Brasho", "DethDroid", "Droid-O-Doom", "Echelon", "Gesa 64", "HangEmHigh", "Herc-5", "Kal-El", "KillBot", "Killdozer", "Klono's Revenge", "Land Mine", "Mother 'O All Tanks", "Snaggletooth", "Terminator-4", "TinNendo", "WabbitSwayer", "XenuPhobe", }; /* * rvgFindPlayer -- Find a player's revenge struct, or NULL. */ REVENGE *rvgFindPlayer(register Player_pl * plr) { register REVENGE *rvgp; register REVENGE *tmp; for(rvgp = &rvg_head; (rvgp = rvgp->next) != &rvg_head;) { if(rvgp->plr->ready != READY || rvgp->plr->armor <= 0) { tmp = rvgp->prev; /* Oops! Died undetected, somehow. */ rvgp->next->prev = rvgp->prev; /* unlink */ rvgp->prev->next = rvgp->next; free(rvgp); rvgp = tmp; continue; } if(rvgp->plr == plr) { return (rvgp); } } return (NULL); } /* * rvgAddRevenge -- Any revenge on the given player, * malloc a new struct if none exists. */ void rvgAddRevenge(Player_pl * plr, int amt) { register REVENGE *rvgp; rvgp = rvgFindPlayer(plr); if(rvgp != NULL) { /* * Add to existing revenge. */ rvgp->amount += amt; /* * Given how the damage comes in many calls with small values, move * the struct to the head of the list to speed up future accesses. */ if(rvgp != rvg_head.next) { rvgp->next->prev = rvgp->prev; /* unlink */ rvgp->prev->next = rvgp->next; rvgp->prev = &rvg_head; /* link */ rvgp->next = rvg_head.next; rvg_head.next->prev = rvgp; rvg_head.next = rvgp; } } else { /* * Else, alloc new struct and link it to head of list. */ rvgp = (REVENGE *) malloc(sizeof(REVENGE)); assert(rvgp); rvgp->plr = plr; rvgp->amount = amt; rvgp->prev = &rvg_head; rvgp->next = rvg_head.next; rvg_head.next->prev = rvgp; rvg_head.next = rvgp; } } /* * rvgClearAllRvg -- Free all revenge structs. */ void rvgClearAllRvg(void) { register REVENGE *rvgp; register REVENGE *nextp; for(nextp = &rvg_head; (rvgp = nextp) != &rvg_head;) { nextp = rvgp->next; free(rvgp); } rvg_head.next = rvg_head.prev = &rvg_head; /* reinit list */ rvg_head.plr = NULL; rvg_head.amount = -999; } /* * rvgClearRevenge -- Find and delete any revenge for the given player, * and clear the current target if that is plr. */ void rvgClearRevenge(register Player_pl * plr) { register REVENGE *rvgp; if(ai_target == plr) { ai_target = NULL; /* DOA */ } for(rvgp = &rvg_head; (rvgp = rvgp->next) != &rvg_head;) { if(rvgp->plr == plr) { rvgp->next->prev = rvgp->prev; /* unlink */ rvgp->prev->next = rvgp->next; free(rvgp); return; } } } /* * rvgAgeRevenge -- Time heals all wounds (except in KotH). */ void rvgAgeRevenge(void) { register REVENGE *rvgp; register REVENGE *tmp; for(rvgp = &rvg_head; (rvgp = rvgp->next) != &rvg_head;) { if(rvgp->plr->ready != READY || rvgp->plr->armor <= 0) { tmp = rvgp->prev; /* Oops! Died undetected, somehow. */ rvgp->next->prev = rvgp->prev; /* unlink */ rvgp->prev->next = rvgp->next; free(rvgp); rvgp = tmp; continue; } /* * Try a decay of 1/16th per cycle. (i.e. amt *= 15/16) */ rvgp->amount = ((rvgp->amount << 4) - rvgp->amount + 7) >> 4; } } /* * rvgFindTarget -- Who do I hate the most right now? */ REVENGE *rvgFindTarget(void) { register REVENGE *rvgp; register REVENGE *pick; register int max; pick = NULL; max = 0; for(rvgp = &rvg_head; (rvgp = rvgp->next) != &rvg_head;) { if(rvgp->amount > max) { max = rvgp->amount; pick = rvgp; } } return (pick); } /* * aihDamageReport -- keep track of who's hurting me * * Could keep track of everyone vs. everyone but doesn't bother. */ void aihDamageReport(Player_pl * hit_pl, int srcid, int amt) { Player_pl *src; if(hit_pl != gm_myplstruct) { if(hit_pl == ai_target) { clv_shot_result = SR_HIT; } return; } src = plLookupPlayer(srcid); if(!src) { return; /* Already dead. */ } if(src->armor <= 0 || src->ready != READY) { rvgClearRevenge(src); /* "He's dead, Jim." */ return; } rvgAddRevenge(src, amt); } /* * aihExplosionHook -- keep track exploding bombs * * When my shot goes off, see if it was a hit, an overshot, or an undershot. */ void aihExplosionHook(Projectilepos_bal * prj) { int dx; if(!ai_target || prj->id != gm_myid) { return; /* Only care about me. */ } dx = prj->x - ai_target->x; if(gm_myplstruct->x > ai_target->x) { dx = -dx; /* On the other side of target. */ } if(dx > 0) { clv_shot_result = SR_OVERSHOT; } else if(dx < 0) { clv_shot_result = SR_UNDERSHOT; } else { clv_shot_result = SR_HIT; } } /* * aihFindRndPlayer -- find a live, random future victim of aggression. */ Player_pl *aihFindRndPlayer(void) { register Player_pl *plr; register int idx; Player_pl *list[MAX_TANKS]; idx = 0; for(plr = pl_begin; plr != NULL; plr = plr->next) { if(plr == gm_myplstruct) { continue; /* Skip me. */ } if(plr->armor <= 0) { continue; /* Skip stiffs. */ } if(plr->ready != READY) { continue; /* Not playing. */ } list[idx++] = plr; } if(idx > 0) { return (list[RND(idx)]); } return (NULL); } /* * clvSetReadiness -- Change a player's readiness status * * On death, clears revenge and maybe targeting info. */ void clvSetReadiness(Relay_rl * rl, int id, char *pkt, int pktlen) { register Player_pl *pl; struct ChangeReady_pkt chpkt; pktUnpackChangeReady(&chpkt, pkt); pl = plLookupPlayer(chpkt.id); assert(pl); pl->ready = chpkt.r; if(pl->ready != READY) { rvgClearRevenge(pl); } } /* * clvRemovePlayer -- Removes a player from the local list that has left. * Clears revenge and maybe targeting info. */ void clvRemovePlayer(Relay_rl * rl, int id, char *pkt, int pktlen) { register Player_pl *pcur; struct PlayerID_pkt pid; pktUnpackPlayerID(&pid, pkt); pcur = plLookupPlayer(pid.id); assert(pcur); if(pl_begin == pcur) { pl_begin = pcur->next; } if(pl_end == pcur) { pl_end = pcur->prev; } if(pcur->prev) { pcur->prev->next = pcur->next; } if(pcur->next) { pcur->next->prev = pcur->prev; } rvgClearRevenge(pcur); free(pcur); } void clvInitialize(int argc, char **argv) { char buf[548]; int sock; char *server = "localhost"; int go; int port = DEFAULT_PORT; char *name = NULL; const char *helptext = " Options:\n \ -sSTR server to connect to (default localhost)\n \ -pINT server port\n \ -nSTR name to join with (default randomly chosen)\n \ -lSTR logging level"; char sset = 0, pset = 0, nset = 0, lset = 0; const char *log_str[] = { "critical", "interesting", "debug", "spam", NULL }; const Levels_log log_val[] = { CRITICAL, INTERESTING, DEBUG, SPAM }; logPrintf(INTERESTING, "King of the Hill (KOTH) Slayer AI version %s\n", VERSION); logPrintf(INTERESTING, "Copyright (C) 1999 Peter Amstutz\n"); logPrintf(INTERESTING, "Copyright (C) 2002, 2003 Allan Douglas\n"); logPrintf(INTERESTING, "KOTH comes with ABSOLUTELY NO WARRANTY\n"); logPrintf(INTERESTING, "This is free software, and you are welcome to redistribute it\n"); logPrintf(INTERESTING, "under the conditions of the GNU GPL\n"); while((go = getopt(argc, argv, "hs:n:p:l:")) > 0) { switch (go) { case 's': server = strdup(optarg); sset = 1; break; case 'n': name = strdup(optarg); nset = 1; break; case 'p': port = atoi(optarg); pset = 1; break; case 'l': lset = 1; if(strcmp(optarg, "critical") == 0) log_level = CRITICAL; else if(strcmp(optarg, "interesting") == 0) log_level = INTERESTING; else if(strcmp(optarg, "debug") == 0) log_level = DEBUG; else if(strcmp(optarg, "spam") == 0) log_level = SPAM; else { lset = 0; logPrintf(CRITICAL, "Bad log level %s\n", optarg); } break; case 'h': logPrintf(CRITICAL, "%s\n", helptext); exit(0); break; default: logPrintf(CRITICAL, "%s\n", helptext); exit(-1); break; } } cfg_configuration = cfgReadConfiguration(DEFAULT_CONFIGFILE); if(cfg_configuration != NULL) { if(!sset) { if(!cfgLoadConfigItemStr(cfg_configuration, "server.host", &server)) { logPrintf(CRITICAL, "No server name found in config file,\n" "trying default %s\n", server); } } if(!pset) { if(!cfgLoadConfigItemInt(cfg_configuration, "server.port", &port)) { logPrintf(CRITICAL, "No port number found in config file,\n" "trying default %d\n", port); } } if(!lset) cfgLoadConfigItemOption(cfg_configuration, "client.logging", log_str, (int *) log_val, (int *) &log_level); } sock = tcpDial(server, port); if(sock < 0) { logPrintf(CRITICAL, "Server not responding\n"); exit(-1); } if(sset) free(server); if((relay = rlInit(-1)) == NULL) { logPrintf(CRITICAL, "Error initializing relay code"); exit(-1); } svrid = rlAddConnection(relay, sock); rlSend(relay, svrid, "NP", 2); memset(ter_data, 0, sizeof(ter_data)); rlRegisterHandler(relay, "UT", aihUpdateTerrain); rlRegisterHandler(relay, "NT", aihNewTerrain); rlRegisterHandler(relay, "GM", aihSetGameMode); rlRegisterHandler(relay, "MS", aihMessage); rlRegisterHandler(relay, "MC", aihMessage); rlRegisterHandler(relay, "UR", aihGetMyID); rlRegisterHandler(relay, "NP", aihNewPlayer); rlRegisterHandler(relay, "ST", aihSetTank); rlRegisterHandler(relay, "SD", aihSetTank); rlRegisterHandler(relay, "SF", aihShotFired); rlRegisterHandler(relay, "CR", clvSetReadiness); rlRegisterHandler(relay, "SN", aihSetName); rlRegisterHandler(relay, "RP", clvRemovePlayer); rlRegisterHandler(relay, "AS", aihActivateShots); rlRegisterHandler(relay, "BW", aihBuyWeapon); rlRegisterHandler(relay, "SW", aihSellWeapon); rlRegisterHandler(relay, "SM", aihSetMoney); rlRegisterHandler(relay, "PV", aihCheckProtocolVersion); rlRegisterHandler(relay, "UF", aiUpdateFireInfo); rlSetDisconnectFunc(relay, aihQuit); wepInit(); rndInit(); clv_name_list = rndNewList(NEL(clv_name_array), (void **) clv_name_array); if(!name) { name = rndList(clv_name_list); } pl_tankwidth = ter_sizex / TANKSCREENRATIO_X; pl_tankheight = ter_sizey / TANKSCREENRATIO_Y; rlSend(relay, svrid, buf, sprintf(buf, "SN%s", name) + 1); if(nset) free(name); } /* * clvBuyWeapons -- buy some weapons from the server */ void clvBuyWeapons(Weapon_wep * weap) { struct BuyWeapon_pkt bw; char buf[512]; bw.type[0] = 'B'; bw.type[1] = 'W'; bw.count = weap->count; strcpy(bw.weapontype, weap->name); rlSend(relay, svrid, buf, pktPackBuyWeapon(buf, &bw)); aih_weaponbuylock = 1; while(aih_weaponbuylock) { ms100.tv_sec = 0; ms100.tv_usec = 100000; rlMain(relay, &ms100); } } void clvPregame(void) { char buf[256]; struct ChangeReady_pkt cr; Weapon_wep *weap; int i; ai_target = NULL; clv_shot_result = SR_NEW_TARGET; rvgClearAllRvg(); /* ** get rid of any weapons if the game is over ** ** check for either current round is 1 or total rounds to cover ** both possible cases of a race condition */ if(((gm_currentRound > gm_totalRounds) || (gm_currentRound == 1)) && gm_totalRounds>0) { plClearAllWeapon(gm_myid); } /* * Lots of Large Shells -- all I can afford or 10 sets. */ weap = wepLookupWeapon("Large Shell"); for(i = 10; --i >= 0 && gm_myplstruct->money >= weap->cost;) { clvBuyWeapons(weap); } sleep(3); cr.type[0] = 'C'; cr.type[1] = 'R'; cr.id = gm_myid; cr.r = (ubyte_pkt)READY; rlSend(relay, svrid, buf, pktPackChangeReady(buf, &cr)); while(gm_gamemode == PREGAME && !gm_quit) { rlMain(relay, NULL); } } void clvTimeToFire(int x) { time_to_fire = 1; } /* * clvSelectWeapon -- Pick the named weapon, or anything for NULL. * * Returns non-zero if the weapon exists and has rounds left, else 0. */ int clvSelectWeapon(char *name) { ItemStock_pl *head; head = gm_myplstruct->itemstock; gm_curitem = head->next; if(name) { for(; gm_curitem != head; gm_curitem = gm_curitem->next) { if(!strcmp(name, ((Weapon_wep *) gm_curitem->info)->name)) { return (gm_curitem->count > 0); /* Gotcha! */ } } /* Last item to check (gm_curitem == head) */ if(!strcmp(name, ((Weapon_wep *) gm_curitem->info)->name)) { return (gm_curitem->count > 0); } } else { for(; gm_curitem != head; gm_curitem = gm_curitem->next) { if(gm_curitem->count > 0) { return (1); /* Anything. */ } } /* Last item to check (gm_curitem == head) */ if(gm_curitem->count > 0) { return (1); } } return (0); /* Failure. */ } /* * clvCalcTrajectory -- Calculate a trajectory to ai_target. * * Aim high to avoid obstacles. */ void clvCalcTrajectory(void) { int dx; /** int dy; ***/ double vx; if(!ai_target) { clv_shot_result = SR_NEW_TARGET; clv_velocity = 1000; clv_angle = 88; /* Take a wild shot in the air. */ return; } dx = ai_target->x - gm_myplstruct->x; /** dy = ai_target->y - gm_myplstruct->y; ** Ignore height differences for now ***/ #if 1 /* * It takes an extra factor of 8 to make this work. Is there some kind * of scaling that I haven't accounted for here? */ vx = (4.0 * dx * bal_grav) / (DEF_VY); #else vx = (dx * bal_grav) / (2 * DEF_VY); #endif assert(ABS(vx) >= 1.0); clv_velocity = sqrt(vx * vx + DEF_VY * DEF_VY); /* The output of atan2 is in radians. */ clv_angle = (180.0 / M_PI) * atan2((double) DEF_VY, vx); assert(clv_angle >= 0 && clv_angle <= 180 && clv_angle != 90); clv_shot_result = SR_UNDERSHOT; } /* * clvGetFireAngle -- Find a target and pick the angle that will hit it. * * Calculates the first angle/velocity, then adjusts the shot according to * feedback. */ void clvGetFireAngle(void) { REVENGE *rvgp; REVENGE *rp; if(ai_target != NULL) { if(ai_target->ready != READY || ai_target->armor <= 0) { /* * Dead. Retarget. */ ai_target = NULL; clv_shot_result = SR_NEW_TARGET; } else if(clv_shot_result == SR_HIT) { /* * Dialed in. Shoot Large Shells, if you've got any. */ if(!clvSelectWeapon("Large Shell")) { (void) clvSelectWeapon("Basic Shell"); /* Should never fail. */ } return; } } if(ai_target != NULL) { /* * Do I hate someone lots more than my current target? * If so, switch to them. */ rvgp = rvgFindTarget(); rp = rvgFindPlayer(ai_target); if(rvgp && (!rp || rvgp->amount > rp->amount * 2)) { ai_target = rvgp->plr; clv_shot_result = SR_NEW_TARGET; } } else { /* * Find somebody new. */ clv_shot_result = SR_NEW_TARGET; rvgp = rvgFindTarget(); if(rvgp) { ai_target = rvgp->plr; } else { ai_target = aihFindRndPlayer(); } } switch (clv_shot_result) { case SR_NEW_TARGET: clvCalcTrajectory(); break; case SR_OVERSHOT: /* * Overshot -- reduce power. If too small, reduce the angle. */ clv_velocity -= 10; if(clv_velocity < 700) { clv_velocity = 750; if(clv_angle > 90) { clv_angle -= 1; } else { clv_angle += 1; } } break; case SR_UNDERSHOT: /* * Undershot -- increase power. If too big, increase the angle. */ clv_velocity += 10; if(clv_velocity > 980) { clv_velocity = 930; if(clv_angle > 90) { clv_angle += 1; } else { clv_angle -= 1; } } break; default: assert(0); /* shouldn't get here */ break; } /* * Switch to Basic Shells until we get a hit. */ (void) clvSelectWeapon("Basic Shell"); /* Should never fail. */ return; } /* * clvFireShot -- fire the cannon */ void clvFireShot(double angle, double velocity) { struct FireCmd_pkt sht; char buf[512]; clvGetFireAngle(); sht.type[0] = 'F'; sht.type[1] = 'S'; gm_myplstruct->fire_angle = angle; gm_myplstruct->fire_velocity = velocity; sht.a = gm_myplstruct->fire_angle; sht.v = gm_myplstruct->fire_velocity; strcpy(sht.shottype, ((Weapon_wep *) gm_curitem->info)->name); rlSend(relay, svrid, buf, pktPackFireCmd(buf, &sht)); time_to_fire = 0; logPrintf(INTERESTING, "Firing at %.0f, %.0f\n", gm_myplstruct->fire_angle, gm_myplstruct->fire_velocity); } void clvPlaygame(void) { int i; int tcount; struct Projectilelist_bal *prj; struct itimerval it; tcount = 0; rvgAgeRevenge(); clvFireShot(clv_angle, clv_velocity); while((gm_gamemode == INGAME || (gm_gamemode == PREGAME && bal_Projectiles)) && !gm_quit) { ms100.tv_sec = 0; ms100.tv_usec = 100000; rlMain(relay, &ms100); if(time_to_fire && (gm_gametype == SIMULTANEOUS || curShooterId == gm_myid)) { clvFireShot(clv_angle, clv_velocity); } if(gm_AS_pos > 0) { for(prj = bal_Projectiles; prj; prj = prj->next) { if(prj->stat == HOLDING) { prj->stat = INITSHOT(prj); } } tcount = 0; do { ++tcount; gm_stuff_happening = terCalcDirtFall(); if(gm_stuff_happening) { plCalcTankFall(); } else { gm_stuff_happening = plCalcTankFall(); } if(gm_stuff_happening) { balAdvanceProjectiles(); } else { gm_stuff_happening = balAdvanceProjectiles(); } } while(gm_stuff_happening); signal(SIGALRM, clvTimeToFire); it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 0; it.it_value.tv_sec = 0; it.it_value.tv_usec = tcount * 100000; setitimer(ITIMER_REAL, &it, NULL); --gm_AS_pos; for(prj = bal_Projectiles; prj; prj = prj->next) { if(prj->stat == HOLDING && prj->gen == gm_AS_queue[0]) prj->stat = INITSHOT(prj); } for(i = 0; i < gm_AS_pos; i++) { gm_AS_queue[i] = gm_AS_queue[i + 1]; } } } usleep(tcount * 100000); } /* post game */ void clvPostgame() { char buf[256]; struct ChangeReady_pkt cr; if(gm_currentRound >= gm_totalRounds && gm_totalRounds > 0) { /* the game is over.... get rid of all the weapons */ plClearAllWeapon(gm_myid); } cr.type[0] = 'C'; cr.type[1] = 'R'; cr.id = gm_myid; cr.r = (ubyte_pkt)READY; rlSend(relay, svrid, buf, pktPackChangeReady(buf, &cr)); while(gm_gamemode == POSTGAME) { logPrintf(DEBUG, "AI in postgame\n"); /* check for the server to change the gamemode */ rlMain(relay, NULL); } } void clvDriverloop() { while(gm_gamemode == NOTPLAYING || gm_myid == -1) { ms100.tv_sec = 0; ms100.tv_usec = 100000; rlMain(relay, &ms100); } gm_myplstruct = plLookupPlayer(gm_myid); while(!gm_quit) { if(gm_gamemode == PREGAME) { clvPregame(); } if(gm_gamemode == INGAME && !gm_quit) { clvPlaygame(); } if(gm_gamemode == POSTGAME && !gm_quit) { clvPostgame(); } } } void clvShutdown() { } int main(int argc, char **argv) { clvInitialize(argc, argv); clvDriverloop(); clvShutdown(); return (0); }