/* Previous implementation of the "mplayer" AI in Xconq. Copyright (C) 1987-1989, 1991-2000 Stanley T. Shebs. Xconq 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, or (at your option) any later version. See the file COPYING. */ /* NOTE: This AI is kept around for comparison purposes when testing AI intelligence. The current AI ought to be able to beat this consistently. This means that oplayer should *not* be improved or altered unless required by a general code change. */ #include "conq.h" #include "kpublic.h" #include "ai.h" extern void register_oplayer(AI_ops *ops); #define can_see_actual_units(side, x, y) \ ((side)->see_all || cover((side), (x), (y)) > 0) static Theater *tmptheater; static Side *anewside; extern int victim_x, victim_y, victim_rating, victim_utype, victim_sidenum; /* Local function declarations. */ static void oplayer_init(Side *side); static void oplayer_init_turn(Side *side); static void oplayer_create_strategy(Side *side); static void reset_strategy(Side *side); static void analyze_the_game(Side *side); static void determine_subgoals(Side *side); static void review_theaters(Side *side); static void create_initial_theaters(Side *side); static Theater *create_theater(Side *side); static void remove_theater(Side *side, Theater *theater); static void move_theater_cell(int x, int y); static void remove_small_theaters(Side *side); static void compute_theater_bounds(Side *side); static void review_goals(Side *side); static void oplayer_review_units(Side *side); static void update_side_strategy(Side *side); static void decide_theater_needs(Side *side, Theater *theater); static void estimate_strengths(Side *side); static void decide_resignation(Side *side); static void add_goal(Side *side, Goal *goal); static Goal *has_goal(Side *side, GoalType goaltype); static Goal *has_unsatisfied_goal(Side *side, GoalType goaltype); static void oplayer_decide_plan(Side *side, Unit *unit); static int oplayer_adjust_plan(Side *side, Unit *unit); static int need_this_type_to_explore(Side *side, int u); static int need_this_type_to_build_explorers(Side *side, int u); static int type_can_build_attackers(Side *side, int u); static int type_can_build_colonizers(Side *side, int u); static void oplayer_react_to_task_result(Side *side, Unit *unit, Task *task, TaskOutcome rslt); static void change_to_adjacent_theater(Side *side, Unit *unit); static void oplayer_react_to_new_side(Side *side, Side *side2); static void oplayer_finish_movement(Side *side); static Unit *search_for_available_transport(Unit *unit, int purpose); static void oplayer_rethink_plan(Unit *unit); static char *oplayer_at_desig(Side *side, int x, int y); static int oplayer_theater_at(Side *side, int x, int y); static int oplayer_read_strengths(Side *side); static Obj *oplayer_save_state(Side *side); #if 0 static void oplayer_react_to_unit_loss(Side *side, Unit *unit); #endif void register_oplayer(AI_ops *ops) { ops->name = "oplayer"; ops->help = "older version of general AI that can play any game"; ops->to_init = oplayer_init; ops->to_init_turn = oplayer_init_turn; ops->to_decide_plan = oplayer_decide_plan; ops->to_react_to_task_result = oplayer_react_to_task_result; ops->to_react_to_new_side = oplayer_react_to_new_side; ops->to_adjust_plan = oplayer_adjust_plan; ops->to_finish_movement = oplayer_finish_movement; ops->to_save_state = oplayer_save_state; ops->region_at = oplayer_theater_at; ops->at_desig = oplayer_at_desig; } static int has_advance_to_research(Side *side, int a) { int a2; for_all_advance_types(a2) { if (aa_needed_to_research(a, a2) && !has_advance(side, a2)) { return FALSE; } } return TRUE; } static int has_advance_to_build(Side *side, int u) { int a; for_all_advance_types(a) { if (ua_needed_to_build(u, a) && !has_advance(side, a)) { return FALSE; } } /* Account for an obsoleting advance. */ a = u_obsolete(u); if (a != NONATYPE && has_advance(side, a)) return FALSE; return TRUE; } static void oplayer_init(Side *side) { Unit *unit; if (game_class == gc_none) { game_class = find_game_class(); } /* Delete any old strategy object in case we just switched AI type. */ if (side->ai != NULL) { free(side->ai); side->ai = NULL; } /* Then always create a new strategy from scratch. */ oplayer_create_strategy(side); /* If the side has no units at the moment, it doesn't really need to plan. */ if (!side_has_units(side)) return; /* Compute an initial estimation of units on each side. */ /* (Needed for save/restore consistency, otherwise not critical to do here.) */ estimate_strengths(side); /* Study the scorekeepers and such, decide how to play the game. */ analyze_the_game(side); /* Reset plans of any units that were not doing anything. */ for_all_side_units(side, unit) { if (in_play(unit) && unit->plan && unit->plan->aicontrol) { net_force_replan(side, unit, TRUE); } } } /* At the beginning of each turn, make plans and review the situation. */ static void oplayer_init_turn(Side *side) { int u, u2; /* Cases where we no longer need to run. */ if (!side->ingame) return; /* A side without units hasn't got anything to do but wait. */ /* (should account for possible units on controlled sides) */ if (!side_has_units(side)) return; /* Oplayers in a hacked game will not play, unless they're being debugged. */ if (compromised && !DebugM) return; update_all_progress_displays("ai turn init start", side->id); DMprintf("%s oplayer init turn\n", side_desig(side)); /* Make sure a strategy object exists. */ if (ai(side) == NULL) oplayer_create_strategy(side); /* Look over the game design we're playing with. */ analyze_the_game(side); if (ai(side)->report_not_understood) { notify_all("%s AI doesn't understand scoring in this game!", short_side_title(side)); ai(side)->report_not_understood = FALSE; } /* code specific to the "time" game */ if (game_class == gc_time) { for_all_unit_types(u) { if (ai(side)->develop_status[u] == RS_DEVELOP_ASSIGNED) { u2 = ai(side)->develop_on[u]; if (!needs_develop (side, u2)) { /* develop done, start upgrading */ DMprintf("%s has completed develop on %s\n", side_desig(side), u_type_name(u2)); ai(side)->develop_status[u] = RS_UPGRADE_NEEDED; } } } } /* If this game is one that can be won, as opposed to just dinking around, figure how to win it. */ if (ai(side)->trytowin) { /* Check out the current goal tree first. */ review_goals(side); /* Goal analysis might have triggered resignation. */ if (!side->ingame) goto done; /* Check out all the theaters. */ review_theaters(side); /* Check out all of our units. */ oplayer_review_units(side); /* (should be integrated better) */ oplayer_finish_movement(side); /* Decide on the new current plan. */ update_side_strategy(side); /* Propagate this to individual unit plans. */ update_unit_plans(side); } else { update_unit_plans_randomly(side); } done: update_all_progress_displays("", side->id); DMprintf("%s oplayer init turn done\n", side_desig(side)); } /* Create and install an entirely new strategy object for the side. */ static void oplayer_create_strategy(Side *side) { Strategy *strategy = (Strategy *) xmalloc(sizeof(Strategy)); /* Put the specific structure into a generic slot. */ side->ai = (struct a_ai *) strategy; /* Allocate a table of pointers to theaters, for access via small numbers rather than full pointers. */ strategy->theatertable = (Theater **) xmalloc(127 * sizeof(Theater *)); /* Allocate a layer of indexes into the theater table. */ strategy->areatheaters = malloc_area_layer(char); /* Allocate random things. */ /* Arrays for unit types. */ strategy->actualmix = (short *) xmalloc(numutypes * sizeof(short)); strategy->expectedmix = (short *) xmalloc(numutypes * sizeof(short)); strategy->idealmix = (short *) xmalloc(numutypes * sizeof(short)); strategy->develop_status = (short *) xmalloc(numutypes * sizeof(short)); strategy->develop_on = (short *) xmalloc(numutypes * sizeof(short)); /* Arrays for terrain types. */ strategy->terrainguess = (short *) xmalloc(numttypes * sizeof(short)); strategy->writable_state = lispnil; /* Set everything to correct initial values. */ reset_strategy(side); } /* Put all the right initial values into the strategy, but don't allocate anything. */ static void reset_strategy(Side *side) { int u, u2, t, dir; Strategy *strategy = (Strategy *) side->ai; /* Remember when we did this. */ strategy->creationdate = g_turn(); /* Null out various stuff. */ strategy->numgoals = 0; strategy->theaters = NULL; /* Actually we start with no theaters, but it's convenient to leave entry 0 in the theater table pointing to NULL. */ strategy->numtheaters = 1; /* Clear pointers to special-purpose theaters. */ strategy->homefront = NULL; for_all_directions(dir) { strategy->perimeters[dir] = NULL; strategy->midranges[dir] = NULL; strategy->remotes[dir] = NULL; } strategy->explorersneeded = 0; /* Reset the summation of our exploration needs. */ for_all_unit_types(u) { strategy->actualmix[u] = 0; strategy->expectedmix[u] = 0; strategy->idealmix[u] = 0; strategy->develop_status[u] = 0; strategy->develop_on[u] = 0; /* code specific to the "time" game */ if (game_class == gc_time) { for_all_unit_types(u2) { if (needs_develop (side, u2) && can_develop_on(u, u2)) { strategy->develop_status[u] = RS_DEVELOP_NEEDED; strategy->develop_on[u] = u2; DMprintf("%s can develop on %s (to level %d)\n", u_type_name(u), u_type_name(u2), u_tech_to_build(u2)); } } } } for_all_terrain_types(t) { strategy->terrainguess[t] = 0; } strategy->analyzegame = TRUE; /* Analyze the game and decide our basic goals. */ analyze_the_game(side); } /* Look over the game design and decide what we're supposed to be doing, if anything at all. This just sets up toplevel goals based on the game design, does not evaluate goals or any such. */ static void analyze_the_game(Side *side) { int maybedraw, i; Goal *goal; if (ai(side)->analyzegame) { if (should_try_to_win(side)) { ai(side)->trytowin = TRUE; /* This is our whole purpose in the game. */ goal = create_goal(GOAL_WON_GAME, side, TRUE); add_goal(side, goal); /* Now figure what exactly we have to do in order to win. */ determine_subgoals(side); /* Machine will want to keep playing as long as it thinks it has a chance to win. */ maybedraw = FALSE; } else { ai(side)->trytowin = FALSE; /* Since the side is not trying to win anything, it will be pretty laidback about whether to keep the game going. */ maybedraw = TRUE; } /* Be trusting about game saves, at least for now. (The problem is that a human player could escape fate by saving the game and then either editing the saved game or just throwing it away.) */ if (TRUE != side->willingtosave /* (should) and decision delegated to AI */) net_set_willing_to_save(side, TRUE); if (maybedraw != side->willingtodraw /* (should) and decision delegated to AI */) try_to_draw(side, maybedraw, "oplayer"); ai(side)->analyzegame = FALSE; /* Summarize our analysis of this game. */ DMprintf("%s will try to %s this game\n", side_desig(side), ai(side)->trytowin ? "win" : "have fun in"); for (i = 0; i < ai(side)->numgoals; ++i) { goal = ai(side)->goals[i]; DMprintf("%s has %s\n", side_desig(side), goal_desig(goal)); } } } static void determine_subgoals(Side *side) { int numvicgoals, understood; Unit *unit; Side *side2; Scorekeeper *sk; Goal *goal; understood = TRUE; /* Look at each scorekeeper and decide on appropriate goals. */ for_all_scorekeepers(sk) { /* (should test who scorekeeper applies to) */ if (match_keyword(sk->body, K_LAST_SIDE_WINS) || match_keyword(sk->body, K_LAST_ALLIANCE_WINS)) { /* We want to "kick butt" - *everybody* else's butt. */ for_all_sides(side2) { if (!trusted_side(side, side2) && side2->ingame) { /* Our goals include preventing other sides from accomplishing theirs. */ goal = create_goal(GOAL_WON_GAME, side2, FALSE); add_goal(side, goal); /* (should add "search-and-destroy" as corollaries) */ } } /* Add goals to protect our own units. */ numvicgoals = 0; for_all_side_units(side, unit) { if (point_value(unit) > 0 /* (should be "n most valuable") */ && in_play(unit) && numvicgoals < 10) { goal = create_goal(GOAL_VICINITY_HELD, side, TRUE); goal->args[0] = unit->x; goal->args[1] = unit->y; goal->args[2] = goal->args[3] = 2; add_goal(side, goal); ++numvicgoals; } } } else if (sk->initial != -10001) { /* This is a numerical scorekeeper whose value we want to maximize. */ if (consp(sk->body) && match_keyword(car(sk->body), K_SET)) { if (consp(cadr(sk->body)) && match_keyword(car(cadr(sk->body)), K_SUM)) { understood = FALSE; } else { understood = FALSE; } } else { understood = FALSE; } } else { understood = FALSE; } } if (!understood) { /* Can't notify anybody yet, no windows up, so record */ ai(side)->report_not_understood = TRUE; DMprintf("%s AI doesn't understand scoring in this game!", short_side_title(side)); } /* We might develop a sudden interest in exploration. */ /* (but should only be if information is really important to winning) */ if (!side->see_all) { if (!g_terrain_seen()) { add_goal(side, create_goal(GOAL_WORLD_KNOWN, side, TRUE)); } /* It will be important to keep track of other sides' units as much as possible. */ for_all_sides(side2) { if (side != side2) { goal = create_goal(GOAL_POSITIONS_KNOWN, side, TRUE); goal->args[0] = (long) side2; add_goal(side, goal); } } /* Also add the general goal of knowing where indeps are. */ goal = create_goal(GOAL_POSITIONS_KNOWN, side, TRUE); goal->args[0] = (long) NULL; add_goal(side, goal); } } /* Do a combination of analyzing existing theaters and creating new ones. */ static void review_theaters(Side *side) { int x, y, u, s, pop, totnumunits; int firstcontact = FALSE; int homefound = FALSE; Unit *unit; Side *firstcontactside, *homefoundside, *otherside, *side2; Theater *theater; UnitView *uview; /* Create some theaters if none exist. */ if (ai(side)->theaters == NULL) { create_initial_theaters(side); } for_all_theaters(side, theater) { theater->allied_units = 0; theater->makers = 0; theater->unexplored = 0; theater->border = FALSE; theater->allied_bases = 0; for_all_unit_types(u) { theater->numassigned[u] = 0; theater->numneeded[u] = 0; theater->numenemies[u] = 0; theater->numsuspected[u] = theater->numsuspectedmax[u] = 0; theater->numtotransport[u] = 0; } if (people_sides_defined()) { for (s = 0; s <= numsides; ++s) theater->people[s] = 0; } theater->units_lost /= 2; } compute_theater_bounds(side); /* Now look at all the units that we can. */ for_all_side_units(side, unit) { if (in_play(unit)) { theater = unit_theater(unit); if (theater != NULL) { ++(theater->allied_units); ++(theater->numassigned[unit->type]); if (isbase(unit)) ++(theater->allied_bases); if (unit->plan && unit->plan->waitingfortransport) ++(theater->numtotransport[unit->type]); } } } /* (should also analyze allies etc) */ /* Now look at the whole world. */ for_all_interior_cells(x, y) { theater = theater_at(side, x, y); if (theater != NULL) { if (can_see_actual_units(side, x, y)) { for_all_stack(x, y, unit) { /* what about occupants? */ if (in_play(unit) && !trusted_side(side, unit->side) && (!indep(unit) || u_point_value(unit->type) > 0)) { if (enemy_side(side, unit->side)) ++(theater->numenemies[unit->type]); if (ai(side)->contacted[side_number(unit->side)] == 0) { ai(side)->contacted[side_number(unit->side)] = 1; if (!indep(unit)) { firstcontact = TRUE; firstcontactside = unit->side; } } if (ai(side)->homefound[side_number(unit->side)] == 0 && !mobile(unit->type)) { ai(side)->homefound[side_number(unit->side)] = 1; if (!indep(unit)) { homefound = TRUE; homefoundside = unit->side; } } } } if (people_sides_defined()) { pop = people_side_at(x, y); if (pop != NOBODY) { ++(theater->people[pop]); if (ai(side)->homefound[pop] == 0) { ai(side)->homefound[pop] = 1; if (pop != 0) { homefound = TRUE; homefoundside = side_n(pop); } } } } } else { if (terrain_view(side, x, y) == UNSEEN) { ++(theater->unexplored); } else { for_all_view_stack(side, x, y, uview) { side2 = view_side(uview); if (enemy_side(side, side2)) { /* Note that we assume indeps are enemies here. */ u = view_type(uview); if (u_point_value(u) > 0) { ++(theater->numsuspected[u]); ++(theater->numsuspectedmax[u]); } } } if (people_sides_defined()) { pop = people_side_at(x, y); if (pop != NOBODY) { ++(theater->people[pop]); } } } } } } for_all_theaters(side, theater) { theater->x = (theater->xmin + theater->xmax) / 2; theater->y = (theater->ymin + theater->ymax) / 2; theater->enemystrengthmin = theater->enemystrengthmax = 0; for_all_unit_types(u) { theater->enemystrengthmin += theater->numenemies[u] + theater->numsuspected[u]; } theater->enemystrengthmax = theater->enemystrengthmin; } if (firstcontact || homefound) { for_all_side_units(side, unit) { if (unit->plan && unit->plan->aicontrol) { net_force_replan(side, unit, FALSE); set_unit_theater(unit, NULL); update_unit_display(side, unit, TRUE); } } } for_all_theaters(side, theater) { DMprintf("%s theater \"%s\" at %d,%d from %d,%d to %d,%d (size %d)\n", side_desig(side), theater->name, theater->x, theater->y, theater->xmin, theater->ymin, theater->xmax, theater->ymax, theater->size); /* Summarize what we know about the theater. */ DMprintf("%s theater \"%s\"", side_desig(side), theater->name); if (!side->see_all && theater->unexplored > 0) { DMprintf(" unexplored %d", theater->unexplored); } DMprintf(" enemy %d", theater->enemystrengthmin); if (theater->enemystrengthmin != theater->enemystrengthmax) { DMprintf("-%d", theater->enemystrengthmax); } for_all_unit_types(u) { if (theater->numenemies[u] + theater->numsuspected[u] > 0) { DMprintf(" %3s %d", u_type_name(u), theater->numenemies[u]); if (theater->numsuspected[u] > 0) { DMprintf("+%d", theater->numsuspected[u]); } } } if (people_sides_defined()) { DMprintf(" people"); for (s = 0; s <= numsides; ++s) { if (theater->people[s] > 0) { DMprintf(" s%d %d", s, theater->people[s]); } } } DMprintf("\n"); totnumunits = 0; for_all_unit_types(u) { totnumunits += (theater->numassigned[u] + theater->numneeded[u] + theater->numtotransport[u]); } if (totnumunits > 0) { /* Summarize the status of our own units in this theater. */ DMprintf("%s theater \"%s\" has ", side_desig(side), theater->name); for_all_unit_types(u) { if (theater->numassigned[u] + theater->numneeded[u] + theater->numtotransport[u] > 0) { DMprintf(" %d %3s", theater->numassigned[u], u_type_name(u)); if (theater->numneeded[u] > 0) { DMprintf(" (of %d needed)", theater->numneeded[u]); } if (theater->numtotransport[u] > 0) { DMprintf(" (%d awaiting transport)", theater->numtotransport[u]); } } } DMprintf("\n"); } } /* Also summarize contacts. */ for_all_sides(otherside) { if (otherside != side) { if (ai(side)->contacted[otherside->id]) { DMprintf("%s contacted s%d", side_desig(side), otherside->id); if (ai(side)->homefound[otherside->id]) { DMprintf(", home found"); } DMprintf("\n"); } } } } /* Set up the initial set of theaters. */ static void create_initial_theaters(Side *side) { int x, y, dir, dist, i, j; int xmin, ymin, xmax, ymax; int homeradius, perimradius, midradius, xxx; int numthx, numthy, thwid, thhgt; Unit *unit; Theater *homefront, *enemyarea, *theater; Theater *gridtheaters[8][8]; Strategy *strategy = ai(side); for (i = 0; i < 8; ++i) { for (j = 0; j < 8; ++j) { gridtheaters[i][j] = NULL; } } /* Compute bbox of initial (should also do enemy?) units. */ xmin = area.width; ymin = area.height; xmax = ymax = 0; for_all_side_units(side, unit) { if (alive(unit) /* and other preconditions? */) { if (unit->x < xmin) xmin = unit->x; if (unit->y < ymin) ymin = unit->y; if (unit->x > xmax) xmax = unit->x; if (unit->y > ymax) ymax = unit->y; } } /* Most games start with each side's units grouped closely together. If this is not the case, do something else. */ if (xmax - xmin > area.width / 4 && ymax - ymin > area.height / 4) { /* (should do some sort of clustering of units) */ if (0 /*people_sides_defined()*/) { homefront = create_theater(side); homefront->name = "Home Front"; enemyarea = create_theater(side); enemyarea->name = "Enemy Area"; for_all_interior_cells(x, y) { if (people_side_at(x, y) == side->id) { set_theater_at(side, x, y, homefront); } else { set_theater_at(side, x, y, enemyarea); } } } else { /* Divide the world up along a grid. */ numthx = (area.width > 60 ? (area.width > 120 ? 7 : 5) : 3); numthy = (area.height > 60 ? (area.height > 120 ? 7 : 5) : 3); thwid = max(8, area.width / numthx); thhgt = max(8, area.height / numthy); for_all_interior_cells(x, y) { i = x / thwid; j = y / thhgt; if (gridtheaters[i][j] == NULL) { theater = create_theater(side); sprintf(spbuf, "Grid %d,%d", i, j); theater->name = copy_string(spbuf); theater->x = x; theater->y = y; gridtheaters[i][j] = theater; } else { theater = gridtheaters[i][j]; } set_theater_at(side, x, y, theater); } } return; } else { /* Always create a first theater that covers the starting area. */ homefront = create_theater(side); homefront->name = "Home Front"; /* Calculate startxy if not already available. */ if (side->startx < 0 && side->starty < 0) calc_start_xy(side); homefront->x = side->startx; homefront->y = side->starty; strategy->homefront = homefront; homeradius = max(5, g_radius_min()); perimradius = max(homeradius + 5, g_separation_min() - homeradius); midradius = max(perimradius + 10, g_separation_min() * 2); xxx = max((side->startx - perimradius), (area.width - side->startx - perimradius)); xxx /= 2; midradius = min(midradius, perimradius + xxx); for_all_interior_cells(x, y) { dist = distance(x, y, side->startx, side->starty); if (people_sides_defined() && people_side_at(x, y) == side->id && dist < (perimradius - 3)) { set_theater_at(side, x, y, homefront); } else { if (dist < homeradius) { set_theater_at(side, x, y, homefront); } else { dir = approx_dir(x - side->startx, y - side->starty); if (dist < perimradius) { if (strategy->perimeters[dir] == NULL) { theater = create_theater(side); sprintf(spbuf, "Perimeter %s", dirnames[dir]); theater->name = copy_string(spbuf); theater->x = x; theater->y = y; strategy->perimeters[dir] = theater; } else { theater = strategy->perimeters[dir]; } } else if (dist < midradius) { if (strategy->midranges[dir] == NULL) { theater = create_theater(side); sprintf(spbuf, "Midrange %s", dirnames[dir]); theater->name = copy_string(spbuf); theater->x = x; theater->y = y; strategy->midranges[dir] = theater; } else { theater = strategy->midranges[dir]; } } else { if (strategy->remotes[dir] == NULL) { theater = create_theater(side); sprintf(spbuf, "Remote %s", dirnames[dir]); theater->name = copy_string(spbuf); theater->x = x; theater->y = y; strategy->remotes[dir] = theater; } else { theater = strategy->remotes[dir]; } } set_theater_at(side, x, y, theater); } } } } remove_small_theaters(side); /* Assign all units to the theater they're currently in. */ /* (how do reinforcements get handled? oplayer should get hold of perhaps) */ for_all_side_units(side, unit) { if (in_play(unit) /* and other preconditions? */) { set_unit_theater(unit, theater_at(side, unit->x, unit->y)); } } } /* Create a single theater object and link it into the list of theaters. */ /* (should be able to re-use theaters in already in theater table) */ static Theater * create_theater(Side *side) { Theater *theater = (Theater *) xmalloc(sizeof(Theater)); if (ai(side)->numtheaters > MAXTHEATERS) return NULL; theater->id = (ai(side)->numtheaters)++; theater->name = "?"; theater->maingoal = NULL; theater->people = (int *) xmalloc ((numsides + 1) * sizeof(int)); /* (should alloc other array slots too) */ /* Connect theater into a linked list. */ theater->next = ai(side)->theaters; ai(side)->theaters = theater; /* Install it into the theater table also. */ ai(side)->theatertable[theater->id] = theater; return theater; } /* Clear all references to the theater and remove it from the list. Note that the theater size must already be zero. */ static void remove_theater(Side *side, Theater *theater) { int dir; Theater *prev; if (ai(side)->homefront == theater) ai(side)->homefront = NULL; for_all_directions(dir) { if (ai(side)->perimeters[dir] == theater) ai(side)->perimeters[dir] = NULL; if (ai(side)->midranges[dir] == theater) ai(side)->midranges[dir] = NULL; if (ai(side)->remotes[dir] == theater) ai(side)->remotes[dir] = NULL; } if (ai(side)->theaters == theater) ai(side)->theaters = theater->next; else { prev = NULL; for_all_theaters(side, prev) { if (prev->next == theater) { prev->next = theater->next; break; } } /* If prev still null, badness */ } --(ai(side)->numtheaters); } static void move_theater_cell(int x, int y) { int dir, x1, y1; Theater *theater2; if (theater_at(tmpside, x, y) == tmptheater) { for_all_directions(dir) { if (interior_point_in_dir(x, y, dir, &x1, &y1)) { theater2 = theater_at(tmpside, x1, y1); if (theater2 != NULL && theater2 != tmptheater) { set_theater_at(tmpside, x, y, theater2); ++(theater2->size); /* (should recompute bbox too) */ --(tmptheater->size); } } } } } static void remove_small_theaters(Side *side) { int domore; Theater *theater; compute_theater_bounds(side); domore = TRUE; while (domore) { domore = FALSE; for_all_theaters(side, theater) { if (between(1, theater->size, 5)) { tmpside = side; tmptheater = theater; apply_to_area(theater->x, theater->y, 6, move_theater_cell); if (theater->size == 0) { remove_theater(side, theater); /* Have to start over now. */ domore = TRUE; break; } } } } /* Redo, many random changes to bounds. */ compute_theater_bounds(side); } /* Compute the size and bounding box of each theater. This should be run each time theaters change in size or shape. */ static void compute_theater_bounds(Side *side) { int x, y; Theater *theater; for_all_theaters(side, theater) { theater->size = 0; theater->xmin = theater->ymin = -1; theater->xmax = theater->ymax = -1; } for_all_interior_cells(x, y) { theater = theater_at(side, x, y); if (theater != NULL) { ++(theater->size); /* Compute bounding box of theater if not already done. */ if (theater->xmin < 0 || x < theater->xmin) theater->xmin = x; if (theater->ymin < 0 || y < theater->ymin) theater->ymin = y; if (theater->xmax < 0 || x > theater->xmax) theater->xmax = x; if (theater->ymax < 0 || y > theater->ymax) theater->ymax = y; } } } /* Examine the goals to see what has been accomplished and what still needs to be done. */ static void review_goals(Side *side) { int i; Scorekeeper *sk; Goal *goal; Side *side2; Strategy *strategy = ai(side); /* First check on our friends and enemies. */ for_all_sides(side2) { /* If they're not trusting us, we don't want to trust them. */ /* (should be able to update this immediately after other side changes trust) */ if (!trusted_side(side2, side) && trusted_side(side, side2)) net_set_trust(side, side2, FALSE); } for (i = 0; i < strategy->numgoals; ++i) { goal = strategy->goals[i]; DMprintf("%s has %s\n", side_desig(side), goal_desig(goal)); } /* Should look at certainty of each goal and decide whether to keep or drop it, and mention in debug output also. */ /* Also think about resigning. */ if (keeping_score()) { for_all_scorekeepers(sk) { if (symbolp(sk->body) && (match_keyword(sk->body, K_LAST_SIDE_WINS) || match_keyword(sk->body, K_LAST_ALLIANCE_WINS))) { decide_resignation(side); } } } } static void estimate_strengths(Side *side) { int u, sn1, x, y; Side *side1, *side2; Strategy *strategy = ai(side); Unit *unit; UnitView *uview; for_all_sides(side1) { sn1 = side_number(side1); for_all_unit_types(u) { strategy->strengths[sn1][u] = 0; } /* this lets us count even semi-trusted allies' units accurately... */ if (side1 == side || allied_side(side, side1)) { for_all_side_units(side1, unit) { /* Note that we count off-area units, since they are reinforcements usually. */ if (alive(unit) && completed(unit)) { ++(strategy->strengths[sn1][unit->type]); } } } } if (side->see_all || (!strategy->initial_strengths_computed && !oplayer_read_strengths(side))) { /* If we can see everything, we can add up units accurately. We also allow initial strengths to be known accurately; this prevents the AI from resigning just because an important unit appears for the first time, even though all players know that it was one of the starting units. */ for_all_cells(x, y) { for_all_stack(x, y, unit) { side2 = unit->side; if (side2 != NULL && !(side2 == side || allied_side(side, side2))) { if (completed(unit)) { ++(strategy->strengths[side2->id][unit->type]); } } } } } else { /* Look at the current view to get enemy strength. */ /* This is too easily faked, and doesn't know about hiding units... */ /* Should also discount old data. */ for_all_cells(x, y) { for_all_view_stack(side, x, y, uview) { side2 = view_side(uview); /* Count only units on other sides. */ if (!(side2 == side || allied_side(side, side2))) ++(strategy->strengths[side2->id][view_type(uview)]); } } } /* Estimate point values. */ /* (should try to account for individual units with special point values) */ for_all_sides(side1) { sn1 = side1->id; strategy->points[sn1] = 0; for_all_unit_types(u) { strategy->points[sn1] += strategy->strengths[sn1][u] * u_point_value(u); } } /* Estimate how many of each type in allied group. */ for_all_sides(side1) { sn1 = side1->id; for_all_unit_types(u) { strategy->alstrengths[sn1][u] = strategy->strengths[sn1][u]; for_all_sides(side2) { if (side1 != side2 && allied_side(side1, side2)) { strategy->alstrengths[sn1][u] += strategy->strengths[side2->id][u]; } } } strategy->alpoints[sn1] = strategy->points[sn1]; for_all_sides(side2) { if (side1 != side2 && allied_side(side1, side2)) { strategy->alpoints[sn1] += strategy->points[side2->id]; } } } /* The first time we estimate strength, record it specially. Later we will use this to compute which sides are getting stronger/weaker as time goes on. */ if (!strategy->initial_strengths_computed) { if (!oplayer_read_strengths(side)) { for_all_sides(side1) { sn1 = side1->id; strategy->points0[sn1] = strategy->points[sn1]; strategy->alpoints0[sn1] = strategy->alpoints[sn1]; for_all_unit_types(u) { strategy->strengths0[sn1][u] = strategy->strengths[sn1][u]; strategy->alstrengths0[sn1][u] = strategy->alstrengths[sn1][u]; } } } ai_save_state(side); strategy->initial_strengths_computed = TRUE; } /* If we're calling strength estimation because a new side has come into existence, use the current strengths as the new side's initial strengths. */ if (anewside != NULL) { sn1 = anewside->id; strategy->points0[sn1] = strategy->points[sn1]; strategy->alpoints0[sn1] = strategy->alpoints[sn1]; for_all_unit_types(u) { strategy->strengths0[sn1][u] = strategy->strengths[sn1][u]; strategy->alstrengths0[sn1][u] = strategy->alstrengths[sn1][u]; } /* Have to redo the saveable state also; force this by blasting any existing recordable state (it should all be re-creatable from oplayer's internal state). */ strategy->writable_state = lispnil; ai_save_state(side); } /* Dump out a detailed listing of our estimates. */ if (DebugM) { for_all_sides(side1) { sn1 = side1->id; DMprintf("%s ", side_desig(side)); DMprintf("est init streng of %s: ", side_desig(side1)); for_all_unit_types(u) { DMprintf(" %d", strategy->strengths0[sn1][u]); } DMprintf(" (%d points)\n", strategy->points0[sn1]); DMprintf("%s ", side_desig(side)); DMprintf("est curr streng of %s: ", side_desig(side1)); for_all_unit_types(u) { DMprintf(" %d", strategy->strengths[sn1][u]); } DMprintf(" (%d points)\n", strategy->points[sn1]); DMprintf("%s ", side_desig(side)); DMprintf("est init allied of %s: ", side_desig(side1)); for_all_unit_types(u) { DMprintf(" %d", strategy->alstrengths0[sn1][u]); } DMprintf(" (%d points)\n", strategy->alpoints0[sn1]); DMprintf("%s ", side_desig(side)); DMprintf("est curr allied of %s: ", side_desig(side1)); for_all_unit_types(u) { DMprintf(" %d", strategy->alstrengths[sn1][u]); } DMprintf(" (%d points)\n", strategy->alpoints[sn1]); } } } /* Sometimes there is no point in going on, but be careful not to be too pessimistic. Right now we only give up if no hope at all. Currently this is only used if there is a last-side-wins scorekeeper; it would need to be modified considerably to be useful with scorekeepers in general. */ static void decide_resignation(Side *side) { int sn, sn1, ratio, ratio0, chance = 0, chance1; Side *side1; Strategy *strategy = ai(side); sn = side->id; estimate_strengths(side); /* If our estimate of our own points is zero, then we're about to lose the game anyway, so just return and avoid screwing up ratio calcs below. */ if (strategy->alpoints[sn] <= 0) return; for_all_sides(side1) { if (side != side1 && side1->ingame && !allied_side(side, side1)) { sn1 = side1->id; /* Note that ratio calculations always scale up by 100, so that we can do finer comparisons without needing floating point. */ ratio = (strategy->alpoints[sn1] * 100) / strategy->alpoints[sn]; /* code specific to the "time" game */ /* the oplayer can severely underestimates its own strength */ if (game_class == gc_time) { ratio /= 3; } if (strategy->alpoints0[sn] > 0) { ratio0 = (strategy->alpoints0[sn1] * 100) / strategy->alpoints0[sn]; /* If we estimated 0 points for some side's initial strength, then our estimate is bad; assume parity. */ if (ratio0 <= 0) ratio0 = 100; /* This formula basically calls for no resignation if ratio is no more than twice what it was initially, 50% chance if ratio is four times what it was (if we started out even, then we're outnumbered 4 to 1), and interpolates for ratios in between. */ chance1 = (((ratio * 100) / ratio0) - 200) / 5; chance1 = max(chance1, 0); } else { /* work by absolute ratios */ if (ratio > 400) { chance1 = ratio / 10; } } /* The overall chance is determined by the most threatening side or alliance. */ chance = max(chance, chance1); } } /* Never go all the way to 100%; perhaps the lopsided ratio is just a temporary setback. */ chance = min(chance, 90); /* Whether or not we actually resign, we may be willing to go for a draw if other players want to. */ /* (Note that toggling this flag is not exactly poker-faced behavior, but I doubt human players will be able to derive much advantage, since they'll already have a pretty good idea if the AI is in trouble or not.) */ try_to_draw(side, (chance > 0), "oplayer"); /* Maybe resign. */ if (chance > 0) { if (probability(chance)) { give_up(side, "oplayer"); } } } /* Go through all our units (and allied ones?). */ static void oplayer_review_units(Side *side) { int u, u2, cp, cpmin, any; int numoffensive[MAXUTYPES], numdefensive[MAXUTYPES]; Unit *unit, *occ, *unit2; Plan *plan; Theater *oldtheater, *theater; /* This code is specific to the "time" game. */ if (game_class == gc_time) { for_all_unit_types(u) { u2 = ai(side)->develop_on[u]; if (ai(side)->develop_status[u] == RS_DEVELOP_ASSIGNED) { /* is anyone developing? */ unit2 = NULL; for_all_side_units(side, unit) { if (unit->type==u && in_play(unit) && unit->plan && unit->plan->aicontrol) { if (unit->plan->tasks && unit->plan->tasks->type == TASK_DEVELOP && unit->plan->tasks->args[0] == u2) unit2 = unit; } } if (unit2 != NULL) { DMprintf("%s is developing for %s on %s (level %d/%d)\n", unit_desig(unit2), side_desig(side), u_type_name(u2), side->tech[u2], u_tech_to_build(u2)); } else { DMprintf("no %s is developing for %s on %s!\n", u_type_name(u), side_desig(side), u_type_name(u2)); ai(side)->develop_status[u] = RS_DEVELOP_NEEDED; } } if (ai(side)->develop_status[u] == RS_DEVELOP_NEEDED && needs_develop (side, u2)) { /* pick for develop a unit not building; if all are building, choose the one which started last */ unit2 = NULL; cpmin = 9999; any = 0; for_all_side_units(side, unit) { if (unit->type == u && in_play(unit) && unit->plan && unit->plan->aicontrol) { any = 1; cp = 0; occ = NULL; if ((unit->plan->tasks != NULL && unit->plan->tasks->type == TASK_BUILD)) occ = find_unit_to_complete(unit, unit->plan->tasks); if (occ != NULL) { cp = occ->cp - uu_creation_cp(u,occ->type); if (uu_cp_per_build(u,u2) > 0) cp /= uu_cp_per_build(u,u2); } if (cp < cpmin) { unit2 = unit; cpmin = cp; } } } if (unit2 == NULL) { if (any) DMprintf("no %s is available to develop for %s on %s!\n", u_type_name(u), side_desig(side), u_type_name(u2)); } else { if (assign_to_develop_on(side, unit2, u2)) { ai(side)->develop_status[u] = RS_DEVELOP_ASSIGNED; } } } } } for_all_unit_types(u) { numoffensive[u] = numdefensive[u] = 0; } for_all_side_units(side, unit) { if (in_play(unit) && unit->plan && unit->plan->aicontrol) { /* Count plan types. */ switch (unit->plan->type) { case PLAN_OFFENSIVE: case PLAN_EXPLORATORY: ++numoffensive[unit->type]; break; case PLAN_DEFENSIVE: ++numdefensive[unit->type]; break; default: break; } } } for_all_side_units(side, unit) { if (in_play(unit) && unit->plan && unit->plan->aicontrol) { /* code specific to the "time" game */ if (game_class == gc_time) { u = unit->type; u2 = ai(side)->develop_on[u]; /* should we upgrade? */ if (ai(side)->develop_status[u] == RS_UPGRADE_NEEDED) { cp = 0; occ = NULL; if ((unit->plan->tasks != NULL && unit->plan->tasks->type == TASK_BUILD)) occ = find_unit_to_complete(unit, unit->plan->tasks); if (occ != NULL) { cp = occ->cp - uu_creation_cp(u,occ->type); if (uu_cp_per_build(u,u2)>0) cp /= uu_cp_per_build(u,u2); } if (occ != NULL && occ->type==u2) { /* already upgrading */ DMprintf("%s is upgrading to %s (%d/%d cp)\n", unit_desig(unit), u_type_name(u2), occ->cp, u_cp(occ->type)); } else if (cp >= u_cp(u2)/4) { /* rule-of-thumb... */ /* complete unit under construction */ DMprintf("%s will complete %s (now %d/%d cp) before upgrading to %s\n", unit_desig(unit), u_type_name(occ->type), occ->cp, u_cp(occ->type), u_type_name(u2)); } else { /* start upgrading */ if (occ != NULL && !fullsized(occ)) { DMprintf("%s will drop work on %s (%d/%d cp) and immediately start upgrading to %s\n", unit_desig(unit), u_type_name(occ->type), occ->cp, u_cp(occ->type), u_type_name(u2)); } else { DMprintf("%s will start upgrading to %s\n", unit_desig(unit), u_type_name(u2)); } net_set_unit_plan_type(side, unit, PLAN_IMPROVING); net_clear_task_agenda(side, unit); net_set_build_task(unit, u2, 1, 0, 0); } } } plan = unit->plan; oldtheater = unit_theater(unit); /* Goal might have become satisfied. */ if (plan->maingoal) { if (goal_truth(side, plan->maingoal) == 100) { DMprintf("%s %s satisfied, removing\n", unit_desig(unit), goal_desig(plan->maingoal)); net_force_replan(side, unit, FALSE); set_unit_theater(unit, NULL); } } /* Theater might have become explored enough (90% known). */ if (plan->type == PLAN_EXPLORATORY && (theater = unit_theater(unit)) != NULL && theater->unexplored < theater->size / 10) { DMprintf("%s theater %s is mostly known\n", unit_desig(unit), theater->name); net_force_replan(side, unit, FALSE); set_unit_theater(unit, NULL); } /* Don't let defense-only units pile up. */ if (plan->type == PLAN_DEFENSIVE && mobile(unit->type) && (numoffensive[unit->type] / 3) < numdefensive[unit->type] /* However, don't mess with units that have specific defensive goals. */ && (plan->maingoal == NULL || (plan->maingoal->type != GOAL_UNIT_OCCUPIED && plan->maingoal->type != GOAL_CELL_OCCUPIED)) && flip_coin()) { DMprintf("%s one of too many on defense (%d off, %d def), replanning\n", unit_desig(unit), numoffensive[unit->type], numdefensive[unit->type]); net_force_replan(side, unit, FALSE); } theater = unit_theater(unit); DMprintf("%s currently assigned to %s", unit_desig(unit), (theater ? theater->name : "no theater")); if (oldtheater != theater) { DMprintf(" (was %s)", (oldtheater ? oldtheater->name : "no theater")); } DMprintf("\n"); } } } /* Look at our current overall strategy and hack it as needed. */ static void update_side_strategy(Side *side) { Theater *theater; DMprintf("%s updating strategy\n", side_desig(side)); /* Add something to add/update theaters as things open up. (?) */ for_all_theaters(side, theater) { decide_theater_needs(side, theater); } /* Decide if we need to be doing any per-side research. */ if (numatypes > 0 && g_side_can_research() && (side->research_topic == NOADVANCE || side->research_topic == NONATYPE) /* (should also test that any advances are available) */ ) { int n, a, i; i = 0; for_all_advance_types(a) { if (has_advance_to_research(side, a) && !has_advance(side, a)) { ++i; } } n = xrandom(i); i = 0; for_all_advance_types(a) { if (has_advance_to_research(side, a) && !has_advance(side, a)) { if (i == n) { net_set_side_research(side, a); return; } ++i; } } net_set_side_research(side, NONATYPE); } } /* Figure out how many units to request for each area. */ static void decide_theater_needs(Side *side, Theater *theater) { if (theater->unexplored > 0) { /* Exploration is less important when 90% of a theater is known. */ if (theater->unexplored > (theater->size / 10)) { ++(ai(side)->explorersneeded); } /* Should look for good exploration units. */ theater->importance = 50; /* should depend on context */ /* theater->reinforce = EXPLORE_AREA; */ #if 0 } else if (0 /* few enemies? */) { if (theater->allied_makers == 0 && theater->makers > 0 && theater->nearby) { theater->reinforce = GUARD_BORDER_TOWN + 2 * theater->makers; } else if (theater->makers > 0) { theater->reinforce = (theater->border ? GUARD_BORDER_TOWN : GUARD_TOWN) + 2 * theater->allied_makers; } else if (theater->allied_bases > 0) { theater->reinforce = (theater->border ? GUARD_BORDER: GUARD_BASE); } else if (theater->border) { theater->reinforce = NO_UNITS; } else { theater->reinforce = NO_UNITS; } } else { if (theater->allied_makers > 0) { theater->reinforce = DEFEND_TOWN + 5 * theater->makers; } else if (theater->allied_bases > 0) { theater->reinforce = DEFEND_BASE + theater->allied_bases; } else { theater->reinforce = 0 /* DEFEND_AREA */; } #endif } } /* Push a new goal onto the side's list of goals. */ /* (this should only add goals that are not already present) */ static void add_goal(Side *side, Goal *goal) { if (ai(side)->numgoals < MAXGOALS) { ai(side)->goals[(ai(side)->numgoals)++] = goal; DMprintf("%s added %s\n", side_desig(side), goal_desig(goal)); } else { DMprintf("%s has no room for %s\n", side_desig(side), goal_desig(goal)); } } /* Return any goal of the given type. */ static Goal * has_goal(Side *side, GoalType goaltype) { int i; Goal *goal; for (i = 0; i < ai(side)->numgoals; ++i) { goal = ai(side)->goals[i]; if (goal != NULL && goal->type == goaltype) { return goal; } } return NULL; } /* Return an unfulfilled goal of the given type. */ static Goal * has_unsatisfied_goal(Side *side, GoalType goaltype) { int i; Goal *goal; for (i = 0; i < ai(side)->numgoals; ++i) { goal = ai(side)->goals[i]; if (goal != NULL && goal->type == goaltype && goal_truth(side, goal) < 100) { return goal; } } return NULL; } /* This is for when a unit needs a plan and asks its side for one. */ static void oplayer_decide_plan(Side *side, Unit *unit) { int u = unit->type; Plan *plan = unit->plan; Goal *goal; /* Nothing to do for units that can't act or are not under AI control. */ if (plan == NULL || !plan->aicontrol) return; /* Plans for advanced units are (for now) handled in run.c instead. */ if (u_advanced(u) #if 0 /* not the ideal test, but good enough */ && u_advanced_auto_construct(u) && u_advanced_auto_research(u) #endif ) return; /* code specific to the "time" game */ /* don't mess up with units developing or upgrading */ if (game_class == gc_time) { if (ai(side)->develop_status[u] == RS_UPGRADE_NEEDED && plan->tasks != NULL && plan->tasks->type == TASK_BUILD) return; if (ai(side)->develop_status[u] == RS_DEVELOP_ASSIGNED && plan->tasks != NULL && plan->tasks->type == TASK_DEVELOP) return; } /* If we're not trying to win at this game, then make the unit act randomly. */ if (!ai(side)->trytowin) { net_set_unit_plan_type(side, unit, PLAN_RANDOM); net_clear_task_agenda(side, unit); return; } switch (plan->type) { case PLAN_PASSIVE: case PLAN_NONE: if (mobile(u)) { if (u_colonizer_worth(u) > 0 && flip_coin()) { assign_to_colonize(side, unit); return; } /* Maybe assign to exploration. */ if (has_goal(side, GOAL_WORLD_KNOWN)) { if (need_this_type_to_explore(side, u) && flip_coin()) { /* also limit to a total percentage, in case exploration needs are very high */ assign_to_exploration(side, unit); return; } } /* Maybe assign to collecting materials. */ if ((goal = has_unsatisfied_goal(side, GOAL_HAS_MATERIAL_TYPE))) { if (need_this_type_to_collect(side, u, goal->args[0])) { assign_to_collection(side, unit, goal->args[0]); return; } } if (type_can_attack(u) || type_can_fire(u)) { /* (A more precise test would be "can attack types known to be or likely to be in the current game".) */ /* Assign most units to offense, save some for defense. */ if (probability(75)) assign_to_offense(side, unit); else assign_to_defense(side, unit); } else if (type_can_build_attackers(side, u)) { assign_to_offense_support(side, unit); } else { } } else { /* Unit doesn't move. */ if (type_can_build_colonizers(side, u) /* (should fine-tune this test) */ && probability(60)) { assign_to_colonization_support(side, unit); } else if (has_unsatisfied_goal(side, GOAL_VICINITY_HELD) && type_can_build_attackers(side, u)) { assign_to_offense_support(side, unit); } else if (has_goal(side, GOAL_WORLD_KNOWN) && need_this_type_to_build_explorers(side, u)) { assign_to_explorer_construction(side, unit); } else if (type_can_build_attackers(side, u)) { assign_to_offense_support(side, unit); } else if (type_can_attack(u) || type_can_fire(u)) { assign_to_defense(side, unit); } else { assign_to_defense_support(side, unit); } } break; case PLAN_OFFENSIVE: /* leave plan alone */ break; case PLAN_COLONIZING: /* leave plan alone */ break; case PLAN_IMPROVING: /* leave plan alone */ break; case PLAN_EXPLORATORY: /* leave plan alone */ break; case PLAN_DEFENSIVE: /* leave plan alone */ break; default: break; } } static int oplayer_adjust_plan(Side *side, Unit *unit) { int u3, m; Task *task; /* Plans for advanced units are (for now) handled in run.c instead. */ if (u_advanced(unit->type) #if 0 /* not the ideal test, but good enough */ && u_advanced_auto_construct(unit->type) && u_advanced_auto_research(unit->type) #endif ) return TRUE; /* Non-mobile units may be able to execute on a plan by construction; here we let the oplayer decide which type to construct. */ if ((unit->plan->type == PLAN_OFFENSIVE || unit->plan->type == PLAN_COLONIZING || unit->plan->type == PLAN_IMPROVING || unit->plan->type == PLAN_EXPLORATORY) && !mobile(unit->type) && unit->plan->aicontrol && !unit->plan->asleep && unit->plan->tasks == NULL ) { u3 = preferred_build_type(side, unit, unit->plan->type); if (is_unit_type(u3)) { task = unit->plan->tasks; if (task == NULL || task->type != TASK_BUILD) { DMprintf("%s directed to build %s\n", unit_desig(unit), u_type_name(u3)); net_set_build_task(unit, u3, 2, 0, 0); for_all_material_types(m) { int consump = um_consumption_on_creation(u3, m); if (consump > 0 && side_has_treasury(side, m) && um_takes_from_treasury(unit->type, m)) { Goal *goal; goal = create_goal(GOAL_HAS_MATERIAL_TYPE, side, TRUE); goal->args[0] = m; goal->args[1] = 3 * consump; add_goal(side, goal); } } } else { DMprintf("%s already building, leaving alone\n", unit_desig(unit)); } /* Only do one at a time, wait for next go-around for next unit. */ /* (why?) */ return FALSE; } } /* If a collecting unit achieves its goal, cancel its plan. */ if (unit->plan->type == PLAN_IMPROVING && mobile(unit->type) && unit->plan->aicontrol && !unit->plan->asleep && unit->plan->tasks == NULL ) { net_force_replan(side, unit, FALSE); } if (unit->plan->waitingfortasks && unit->plan->aicontrol ) { net_force_replan(side, unit, FALSE); } if (!unit->plan->reserve && g_units_may_go_into_reserve() && unit->plan->execs_this_turn > 10 * max(1, u_acp(unit->type))) { net_set_unit_reserve(side, unit, TRUE, FALSE); } /* Look at more units. */ return TRUE; } static int need_this_type_to_explore(Side *side, int u) { int s, numcontacted = 0, numfound = 0; /* Non-mobile units aren't very good explorers. */ /* (should someday account for possibility of perpetual passenger with good vision radius) */ if (!mobile(u)) return FALSE; for (s = 1; s <= numsides; ++s) { if (s == side->id) continue; if (ai(side)->contacted[s]) ++numcontacted; if (ai(side)->homefound[s]) ++numfound; } if (numcontacted == 0) { /* If we've haven't found anybody, always explore. */ return TRUE; } else if (numfound == 0) { /* If we've made contact but haven't found their home base, we still need to explore, but not so much. */ return probability(50); } else if (numfound < numsides - 1) { /* If we haven't found everybody's home base, we still need to have a few units continuing to explore. */ return probability(10); } else { /* If everybody has been found, then we likely have more pressing concerns; don't do more exploration. */ return FALSE; } } /* Return true if the given type is wanted to build units that can explore. */ static int need_this_type_to_build_explorers(Side *side, int u) { int s, u2; /* If we've made contact with other sides, we don't want to explore anymore. (seems extreme tho) */ for (s = 1; s <= numsides; ++s) { if (ai(side)->contacted[s]) return FALSE; if (ai(side)->homefound[s]) return FALSE; } for_all_unit_types(u2) { if (mobile(u2) && type_allowed_on_side(u2, side) && has_advance_to_build(side, u2) /* should also check u2 is a useful explorer */ && uu_acp_to_create(u, u2) > 0) return TRUE; } return FALSE; } static int type_can_build_attackers(Side *side, int u) { int u2; for_all_unit_types(u2) { if (mobile(u2) && (type_can_attack(u2) || type_can_fire(u2)) && could_create(u, u2) && has_advance_to_build(side, u2) && type_allowed_on_side(u2, side)) return TRUE; } return FALSE; } static int type_can_build_colonizers(Side *side, int u) { int u2; for_all_unit_types(u2) { if (mobile(u2) && u_colonizer_worth(u2) > 0 && could_create(u, u2) && has_advance_to_build(side, u2) && type_allowed_on_side(u2, side)) return TRUE; } return FALSE; } /* This is a hook that runs after each task is executed. */ static void oplayer_react_to_task_result(Side *side, Unit *unit, Task *task, TaskOutcome rslt) { int dx, dy, x1, y1, fact; Unit *occ; Theater *theater; /* React to an apparent blockage. */ if (rslt == TASK_FAILED && task != NULL && task->type == TASK_MOVE_TO && task->retrynum > 2) { if (desired_direction_impassable(unit, task->args[0], task->args[1])) { if (could_be_ferried(unit, task->args[0], task->args[1])) { if (unit->plan->type == PLAN_EXPLORATORY && flip_coin()) { DMprintf("%s blocked while exploring, ", unit_desig(unit)); if (flip_coin()) { DMprintf("changing theaters\n"); change_to_adjacent_theater(side, unit); } else { DMprintf("changing goal within theater\n"); /* Clear the existing goal and create a new one. */ assign_explorer_to_theater(side, unit, unit_theater(unit)); } return; } else if (flip_coin()) { DMprintf("%s blocked, will wait for transport\n", unit_desig(unit)); theater = theater_at(side, unit->x, unit->y); if (theater != NULL) { ++(theater->numtotransport[unit->type]); } net_set_unit_reserve(side, unit, TRUE, FALSE); net_set_unit_waiting_for_transport(side, unit, TRUE); return; } } else { if (unit->occupant) { DMprintf("%s blocked while transporting, will sit briefly\n", unit_desig(unit)); net_set_unit_reserve(side, unit, TRUE, FALSE); for_all_occupants(unit, occ) { net_wake_unit(side, occ, FALSE); } return; } /* Another option is to transfer to another theater. This is especially useful when exploring. */ if (unit->plan->type == PLAN_EXPLORATORY && flip_coin()) { DMprintf("%s blocked while exploring, changing theaters\n", unit_desig(unit)); change_to_adjacent_theater(side, unit); return; } } /* Try moving sideways. */ if (probability(80)) { dx = task->args[0] - unit->x; dy = task->args[1] - unit->y; fact = (flip_coin() ? 50 : -50); x1 = unit->x - ((fact * dy) / 100); y1 = unit->y + ((fact * dx) / 100); if (inside_area(x1, y1)) net_push_move_to_task(unit, x1, y1, 1); } return; } else if (blocked_by_enemy(unit, task->args[0], task->args[1], TRUE)) { /* (should decide if allowable risk to passengers) */ DMprintf("%s blocked by enemy\n", unit_desig(unit)); if (!attack_blockage(side, unit, task->args[0], task->args[1], TRUE)) { if (blocked_by_enemy(unit, task->args[0], task->args[1], FALSE)) { attack_blockage(side, unit, task->args[0], task->args[1], FALSE); } else { /* (should move sideways?) */ } } } else { /* what to do about other failures? */ } return; } /* React to inability to resupply by trying to build a base. */ if (rslt == TASK_FAILED && task != NULL && task->type == TASK_RESUPPLY && task->retrynum > 2) { net_set_unit_reserve(side, unit, FALSE, FALSE); build_depot_for_self(side, unit); } } /* Reassign the given unit to another theater, usually because it is unable to fulfill its goal in its current theater. */ static void change_to_adjacent_theater(Side *side, Unit *unit) { int dir; Theater *theater, *newtheater = NULL; theater = unit_theater(unit); if (theater != NULL) { for_all_directions(dir) { /* If we have a radial pattern of theaters, randomly choose between inward/outward or right/left. */ if (theater == ai(side)->perimeters[dir]) { if (probability(20) && ai(side)->midranges[dir] != NULL) newtheater = ai(side)->midranges[dir]; else newtheater = ai(side)->perimeters[flip_coin() ? left_dir(dir) : right_dir(dir)]; break; } if (theater == ai(side)->midranges[dir]) { if (probability(20) && ai(side)->perimeters[dir] != NULL) newtheater = ai(side)->perimeters[dir]; else if (probability(20) && ai(side)->remotes[dir] != NULL) newtheater = ai(side)->remotes[dir]; else newtheater = ai(side)->midranges[flip_coin() ? left_dir(dir) : right_dir(dir)]; break; } if (theater == ai(side)->remotes[dir]) { if (probability(20) && ai(side)->midranges[dir] != NULL) newtheater = ai(side)->midranges[dir]; else newtheater = ai(side)->remotes[flip_coin() ? left_dir(dir) : right_dir(dir)]; break; } } /* (should add grid case also?) */ if (newtheater != NULL) { assign_explorer_to_theater(side, unit, newtheater); } } } /* This function is called whenever a new side appears in the game. It mainly needs to make that any allocated data is resized appropriately. */ static void oplayer_react_to_new_side(Side *side, Side *side2) { /* (Assumes we call this right after adding each new side) */ int oldnumsides = numsides - 1; int *newpeople, s; Theater *theater; for_all_theaters(side, theater) { /* Grow any people count arrays if present. */ if (theater->people != NULL) { newpeople = (int *) xmalloc ((numsides + 1) * sizeof(int)); for (s = 0; s <= oldnumsides; ++s) newpeople[s] = theater->people[s]; free(theater->people); theater->people = newpeople; } } anewside = side2; estimate_strengths(side); anewside = NULL; } /* At the end of a turn, re-evaluate the plans of some units in case the situation changed. */ static void oplayer_finish_movement(Side *side) { int u, scan; Unit *unit; Theater *theater; scan = FALSE; for_all_theaters(side, theater) { for_all_unit_types(u) { if (theater->numtotransport[u] > 0) { scan = TRUE; break; } } if (scan) break; } if (scan) { /* Find a unit needing transport. */ for_all_side_units(side, unit) { if (is_active(unit) && unit->plan && unit->plan->aicontrol && unit->plan->waitingfortransport) { search_for_available_transport(unit, 0); net_set_unit_waiting_for_transport(side, unit, FALSE); } } } for_all_side_units(side, unit) { if (is_active(unit) && unit->plan && unit->plan->aicontrol) { oplayer_rethink_plan(unit); } } } /* Given a unit and a reason to be needing transport, look around for something suitable. We need a transport that has available space and is not doing something else more important. */ /* (should also choose transports that can reach the unit's destination or at least close by) */ static Unit * search_for_available_transport(Unit *unit, int purpose) { int dist, closestdist = area.maxdim; Unit *transport, *closesttransport = NULL; Theater *theater = unit_theater(unit); /* (more efficient to search adjacent cells first?) */ for_all_side_units(unit->side, transport) { if (is_active(transport) && mobile(transport->type) && could_carry(transport->type, unit->type) && can_carry(transport, unit) && transport->act != NULL /* not quite correct, but to fix bug */ && (purpose == 1 ? accelerator(transport->type, unit->type) : TRUE) ) { /* Don't grab at units being moved manually. */ if (transport->plan && !transport->plan->aicontrol) continue; /* Maybe this one is already coming to get somebody. */ if (transport->plan && transport->plan->tasks != NULL && transport->plan->tasks->type == TASK_PICKUP) { if (transport->plan->tasks->args[0] == unit->id) return transport; /* Picking up somebody else - don't hijack. */ continue; } if (transport->plan && transport->plan->tasks != NULL && transport->plan->tasks->type == TASK_MOVE_TO && transport->plan->tasks->next != NULL && transport->plan->tasks->next->type == TASK_PICKUP) { if (transport->plan->tasks->next->args[0] == unit->id) return transport; /* Picking up somebody else - don't hijack. */ continue; } dist = distance(unit->x, unit->y, transport->x, transport->y); if (dist < closestdist || (dist == closestdist && flip_coin())) { closesttransport = transport; closestdist = dist; } /* If transport already adjacent, no need to keep looking. */ if (closestdist <= 1) break; } } if (closesttransport != NULL && closesttransport->plan != NULL) { net_clear_task_agenda(unit->side, unit); /* (could inherit unit's goal, but not needed) */ if (unit->plan) net_set_unit_plan_type(unit->side, closesttransport, unit->plan->type); net_set_unit_main_goal(unit->side, closesttransport, NULL); net_push_pickup_task(closesttransport, unit); net_push_move_to_task(closesttransport, unit->x, unit->y, 1); net_push_occupy_task(unit, closesttransport); /* No longer count this unit as needing transport. */ if (theater != NULL) { --(theater->numtotransport[unit->type]); set_unit_theater(closesttransport, theater); } DMprintf("%s will be picked up by closest transport %s\n", unit_desig(unit), unit_desig(closesttransport)); return closesttransport; } return NULL; } /* For units with plans and that are under AI control, consider changing the current plan/tasks. */ static void oplayer_rethink_plan(Unit *unit) { int dist, x1, y1; Task *toptask = unit->plan->tasks, *nexttask = NULL; Plan *plan = unit->plan; Unit *transport; if (toptask) nexttask = toptask->next; /* If we have a long ways to go, see if there is a transport available that can get us there faster. */ if (toptask != NULL && (toptask->type == TASK_HIT_UNIT || (toptask->type == TASK_MOVE_TO && nexttask != NULL && nexttask->type == TASK_HIT_UNIT)) && !plan->reserve && !plan->asleep && !plan->waitingfortransport && (unit->transport == NULL || !mobile(unit->transport->type)) && ((dist = distance(unit->x, unit->y, toptask->args[0], toptask->args[1])) >= 4 * u_acp(unit->type)) && accelerable(unit->type) ) { DMprintf("%s looking for transport to accelerate with;\n", unit_desig(unit)); transport = search_for_available_transport(unit, 1); if (transport != NULL) { /* net_push_sentry_task(unit, max(1, dist / max(1, u_acp(transport->type)))); */ if (g_units_may_go_into_reserve()) net_set_unit_reserve(unit->side, unit, TRUE, FALSE); net_set_unit_waiting_for_transport(unit->side, unit, FALSE); } else { DMprintf(" found nothing\n"); } } if (unit->plan->type == PLAN_OFFENSIVE && toptask != NULL && toptask->type == TASK_MOVE_TO && distance(unit->x, unit->y, toptask->args[0], toptask->args[1]) >= min(2, u_acp(unit->type)) && enemy_close_by(unit->side, unit, 1 /* 2 would be better? */, &x1, &y1) ) { net_push_hit_unit_task(unit, x1, y1, NONUTYPE, -1); DMprintf("%s sees enemy close by, will attack it\n", unit_desig(unit)); } /* (should also notice fire opportunities) */ /* If we see somebody that could be captured and help us explore, set up to produce capturers. */ if (!mobile(unit->type) && (unit->plan->type == PLAN_EXPLORATORY || unit->plan->type == PLAN_OFFENSIVE) ) { int range = 4, rslt, x, y; DMprintf("%s searching for useful capture within %d in order to choose build; found ", unit_desig(unit), range); tmpunit = unit; rslt = search_around(unit->x, unit->y, range, useful_captureable_here, &x, &y, 1); if (rslt && is_unit_type(tmputype)) { DMprintf("%s at %d,%d", u_type_name(tmputype), x, y); if (toptask != NULL && toptask->type == TASK_BUILD && uu_capture(toptask->args[0], tmputype) ) { /* Already doing the right thing. */ DMprintf(" - already building %s", u_type_name(toptask->args[0])); } else { /* (should find best type that can capture quickly, schedule to build it) */ DMprintf(" - duhhh, what now?"); } } else { DMprintf("nothing"); } DMprintf("\n"); } } #if 0 static void oplayer_react_to_unit_loss(side, unit) Side *side; Unit *unit; { int x = unit->x, y = unit->y; Theater *th; if (!inside_area(x, y)) { x = unit->prevx; y = unit->prevy; } if (!inside_area(x, y)) return; /* Count the unit as having been lost in a particular theater. */ if (ai(side) && (th = theater_at(side, x, y)) != NULL) { ++(th->units_lost); } } #endif /* This is used by interfaces to display the name of the theater in use at a given point. */ static char * oplayer_at_desig(Side *side, int x, int y) { Theater *theater; if (ai(side) == NULL) return ""; theater = theater_at(side, x, y); return (theater ? theater->name : ""); } /* This is used by interfaces to display boundaries between theaters, by comparing the numbers returned. */ static int oplayer_theater_at(Side *side, int x, int y) { Theater *theater; if (ai(side) == NULL) return 0; theater = theater_at(side, x, y); return (theater ? theater->id : 0); } /* Collect initial strength information stored in the oplayer's private saved data. */ static int oplayer_read_strengths(Side *side) { int sn1, u, found = FALSE; char *propname; Obj *props, *bdg, *rest, *sidebdg, *urest; Side *side1; Strategy *strategy = ai(side); props = find_at_key(side->aidata, "oplayer"); for (; props != lispnil; props = cdr(props)) { bdg = car(props); propname = c_string(car(bdg)); if (strcmp(propname, "strengths0") == 0) { found = TRUE; rest = cdr(bdg); for_all_sides(side1) { sn1 = side1->id; sidebdg = car(rest); urest = cadr(sidebdg); for_all_unit_types(u) { strategy->strengths0[sn1][u] = c_number(car(urest)); urest = cdr(urest); } rest = cdr(rest); } } else if (strcmp(propname, "alstrengths0") == 0) { found = TRUE; rest = cdr(bdg); for_all_sides(side1) { sn1 = side1->id; sidebdg = car(rest); urest = cadr(sidebdg); for_all_unit_types(u) { strategy->alstrengths0[sn1][u] = c_number(car(urest)); urest = cdr(urest); } rest = cdr(rest); } } else if (strcmp(propname, "points0") == 0) { found = TRUE; rest = cdr(bdg); for_all_sides(side1) { sn1 = side1->id; strategy->points0[sn1] = c_number(car(rest)); rest = cdr(rest); } } else if (strcmp(propname, "alpoints0") == 0) { found = TRUE; rest = cdr(bdg); for_all_sides(side1) { sn1 = side1->id; strategy->alpoints0[sn1] = c_number(car(rest)); rest = cdr(rest); } } else { } } return found; } /* Write out any state that the oplayer must preserve. We don't actually write; instead we build a Lisp object and pass that back to the writing routines. */ static Obj * oplayer_save_state(Side *side) { int sn1, u; Obj *rslt, *vallist, *uvallist; Side *side1; Strategy *strategy = ai(side); rslt = lispnil; /* Just return last result if it's already been computed. */ if (strategy->writable_state != lispnil || xmalloc_warnings) return strategy->writable_state; /* We're pushing bindings onto a list, so do in reverse of desired order. */ vallist = lispnil; for_all_sides(side1) { sn1 = side1->id; uvallist = lispnil; for_all_unit_types(u) { uvallist = cons(new_number(strategy->alstrengths0[sn1][u]), uvallist); } uvallist = reverse(uvallist); push_binding(&vallist, new_number(sn1), uvallist); } vallist = reverse(vallist); push_cdr_binding(&rslt, intern_symbol("alstrengths0"), vallist); vallist = lispnil; for_all_sides(side1) { sn1 = side1->id; uvallist = lispnil; for_all_unit_types(u) { uvallist = cons(new_number(strategy->strengths0[sn1][u]), uvallist); } uvallist = reverse(uvallist); push_binding(&vallist, new_number(sn1), uvallist); } vallist = reverse(vallist); push_cdr_binding(&rslt, intern_symbol("strengths0"), vallist); vallist = lispnil; for_all_sides(side1) { sn1 = side1->id; vallist = cons(new_number(strategy->alpoints0[sn1]), vallist); } vallist = reverse(vallist); push_cdr_binding(&rslt, intern_symbol("alpoints0"), vallist); vallist = lispnil; for_all_sides(side1) { sn1 = side1->id; vallist = cons(new_number(strategy->points0[sn1]), vallist); } vallist = reverse(vallist); push_cdr_binding(&rslt, intern_symbol("points0"), vallist); strategy->writable_state = rslt; return rslt; }