From 932379f9f59ea8cca4acd3275efa3308ed9686f1 Mon Sep 17 00:00:00 2001 From: Ihnatus Date: Sun, 13 Nov 2022 00:31:22 +0300 Subject: [PATCH] AI: Improve unit danger calculation Look at "Attack_Bonus" effect. Look at action enablers that may fire. If a unit may occupy city or bring occupiers, never consider it completely harmless. This patch includes a generic mechanism for arbitrary requirement vector evaluation (i.e., substituting is_req_active() with a custom callback). See OSDN#46046 Signed-off-by: Ihnatus --- ai/default/daimilitary.c | 213 +++++++++++++++++++++++++++++++++++-- common/requirements.c | 224 +++++++++++++++++++++++++++++++++++++++ common/requirements.h | 32 ++++++ 3 files changed, 463 insertions(+), 6 deletions(-) diff --git a/ai/default/daimilitary.c b/ai/default/daimilitary.c index d541432c84..78944e39b5 100644 --- a/ai/default/daimilitary.c +++ b/ai/default/daimilitary.c @@ -345,6 +345,161 @@ static int assess_defense_igwall(struct ai_type *ait, struct city *pcity) return assess_defense_backend(ait, pcity, TRUE); } +/**********************************************************************//** + While assuming an attack on a target in few turns, which requirements + on this target are considered mutable + data_sz for a number of turns in which the strike is awaited +**************************************************************************/ +static enum fc_tristate +tactical_req_cb(const struct req_context *context, + const struct player *other_player, + const struct requirement *req, + void *data, int data_sz) +{ + switch (req->source.kind) { + case VUT_IMPROVEMENT: + { + const struct impr_type *b = req->source.value.building; + + /* FIXME: in actor_reqs, may allow attack _from_ a city with... */ + if (req->survives || NULL == context->city || is_great_wonder(b) + || !city_has_building(context->city, b) || b->sabotage <= 0) { + return tri_req_active(context, other_player, req); + } + /* Else may be sabotaged */ + } + fc__fallthrough; + case VUT_UNITSTATE: + case VUT_ACTIVITY: + case VUT_MINSIZE: + case VUT_MAXTILEUNITS: + case VUT_MINHP: + case VUT_MINMOVES: + case VUT_COUNTER: + /* Can be changed back or forth quickly */ + return TRI_MAYBE; + case VUT_MINVETERAN: + /* Can be changed forth but not back */ + return is_req_preventing(context, other_player, req, RPT_POSSIBLE) + > REQUCH_CTRL ? TRI_NO : TRI_MAYBE; + case VUT_MINFOREIGNPCT: + case VUT_NATIONALITY: + /* Can be changed back but hardly forth (foreign citizens reduced first) */ + switch (tri_req_active(context, other_player, req)) { + case TRI_NO: + return req->present ? TRI_NO : TRI_MAYBE; + case TRI_YES: + return req->present ? TRI_MAYBE : TRI_YES; + default: + return TRI_MAYBE; + } + case VUT_DIPLREL: + case VUT_DIPLREL_TILE: + case VUT_DIPLREL_TILE_O: + case VUT_DIPLREL_UNITANY: + case VUT_DIPLREL_UNITANY_O: + /* If the attack happens, there is a diplrel that allows it */ + return TRI_YES; + case VUT_AGE: + case VUT_MINCALFRAG: + case VUT_MINYEAR: + /* If it is not near, won't change */ + return tri_req_active_turns(data_sz, 5 /* WAG */, + context, other_player, req); + case VUT_CITYSTATUS: + if (CITYS_OWNED_BY_ORIGINAL != req->source.value.citystatus) { + return TRI_MAYBE; + } + fc__fallthrough; + case VUT_UTYPE: + case VUT_UTFLAG: + case VUT_UCLASS: + case VUT_UCFLAG: + /* FIXME: support converting siege machines (needs hard reqs checked) */ + case VUT_ACTION: + case VUT_OTYPE: + case VUT_SPECIALIST: + case VUT_EXTRAFLAG: + case VUT_MINLATITUDE: + case VUT_MAXLATITUDE: + case VUT_AI_LEVEL: + case VUT_CITYTILE: + case VUT_STYLE: + case VUT_TOPO: + case VUT_SERVERSETTING: + case VUT_NATION: + case VUT_NATIONGROUP: + case VUT_ADVANCE: + case VUT_TECHFLAG: + case VUT_GOVERNMENT: + case VUT_ACHIEVEMENT: + case VUT_IMPR_GENUS: + case VUT_MINCULTURE: + case VUT_MINTECHS: + case VUT_ORIGINAL_OWNER: + case VUT_ROADFLAG: + case VUT_TERRAIN: + case VUT_EXTRA: + case VUT_GOOD: + case VUT_TERRAINCLASS: + case VUT_TERRFLAG: + case VUT_TERRAINALTER: + case VUT_NONE: + return tri_req_active(context, other_player, req); + case VUT_COUNT: + /* Not implemented. */ + break; + } + fc_assert_ret_val(FALSE, TRI_NO); +} + +/**********************************************************************//** + See if there is an enabler for particular action to be targeted at + pcity's tile after turns (for 5 turns more). + Note that hard reqs are not tested except for utype + and that the actor player is ignored; also, it's not tested can utype + attack the city's particular terrain. +**************************************************************************/ +static bool +action_may_happen_unit_on_city(const action_id wanted_action, + const struct unit *actor, + const struct city *pcity, + int turns) +{ + const struct player *target_player = city_owner(pcity), + *actor_player = unit_owner(actor); + const struct unit_type *utype = unit_type_get(actor); + const struct req_context target_ctx = { + .player = target_player, + .city = pcity, + .tile = city_tile(pcity) + }, actor_ctx = { + .player = actor_player, + .unit = actor, + .unittype = utype + }; + + if (!utype_can_do_action(utype, wanted_action)) { + return FALSE; + } + action_enabler_list_iterate(action_enablers_for_action(wanted_action), + enabler) { + /* We assume that we could build or move units into the city + * that are not present there yet */ + if (TRI_NO != tri_reqs_cb_active(&actor_ctx, target_player, + &enabler->actor_reqs, NULL, + tactical_req_cb, NULL, turns) + && + TRI_NO != tri_reqs_cb_active(&target_ctx, actor_player, + &enabler->target_reqs, NULL, + tactical_req_cb, NULL, turns)) { + return TRUE; + } + } action_enabler_list_iterate_end; + + return FALSE; +} + /**********************************************************************//** How dangerous and far a unit is for a city? **************************************************************************/ @@ -356,9 +511,11 @@ static unsigned int assess_danger_unit(const struct city *pcity, struct pf_position pos; const struct unit_type *punittype = unit_type_get(punit); const struct tile *ptile = city_tile(pcity); + const struct player *uowner = unit_owner(punit); const struct unit *ferry; unsigned int danger; - int mod; + int amod = -99, dmod; + bool attack_danger = FALSE; *move_time = PF_IMPOSSIBLE_MC; @@ -398,10 +555,50 @@ static unsigned int assess_danger_unit(const struct city *pcity, return 0; } + /* Find the worst attack action to expect */ + action_by_result_iterate(paction, id, ACTRES_ATTACK) { + /* Is it possible that punit will do action id to the city? */ + int b; + + if (action_may_happen_unit_on_city(id, punit, pcity, *move_time)) { + attack_danger = TRUE; + } else { + continue; + } + b = get_unittype_bonus(uowner, ptile, punittype, paction, EFT_ATTACK_BONUS); + if (b > amod) { + amod = b; + } + } action_by_result_iterate_end; + + /* FIXME: it's a dummy support for anti-bombard defense just to do something against + * approaching bombarders. Some better logic is needed, see OSDN#41778 */ + if (!attack_danger) { + action_by_result_iterate(paction, id, ACTRES_BOMBARD) { + int b; + + if (action_may_happen_unit_on_city(id, punit, pcity, *move_time)) { + attack_danger = TRUE; + } else { + continue; + } + b = get_unittype_bonus(uowner, ptile, punittype, paction, EFT_ATTACK_BONUS); + if (b > amod) { + amod = b; + } + } action_by_result_iterate_end; + /* Here something should be done cuz the modifier affects + * more than one unit but not their full hp, but is not done yet...*/ + } + if (!attack_danger) { + /* If the unit is dangerous, it's not about its combat strength */ + return 0; + } + danger = adv_unit_att_rating(punit); - mod = 100 + get_unittype_bonus(city_owner(pcity), ptile, - punittype, NULL, EFT_DEFEND_BONUS); - return danger * 100 / MAX(mod, 1); + dmod = 100 + get_unittype_bonus(city_owner(pcity), ptile, + punittype, NULL, EFT_DEFEND_BONUS); + return danger * (amod + 100) / MAX(dmod, 1); } /**********************************************************************//** @@ -638,8 +837,10 @@ static unsigned int assess_danger(struct ai_type *ait, struct city *pcity, continue; } - if ((0 < vulnerability && unit_can_take_over(punit)) - || utai->carries_occupiers) { + if (unit_can_take_over(punit) || utai->carries_occupiers) { + /* Even if there is no attack strength, + * we need ANY protector for the city */ + vulnerability = MAX(vulnerability, 1); if (3 >= move_time) { urgency++; if (1 >= move_time) { diff --git a/common/requirements.c b/common/requirements.c index 28947e746f..4174c865a2 100644 --- a/common/requirements.c +++ b/common/requirements.c @@ -40,6 +40,7 @@ #include "server_settings.h" #include "specialist.h" #include "style.h" +#include "victory.h" /* victory_enabled() */ #include "requirements.h" @@ -1522,6 +1523,19 @@ static bool present_implies_not_present(const struct requirement *req1, &present->source); } +/**********************************************************************//** + Returns TRUE if req2 is always fulfilled when req1 is (i.e. req1 => req2) +**************************************************************************/ +bool req_implies_req(const struct requirement *req1, + const struct requirement *req2) +{ + struct requirement nreq2; + + req_copy(&nreq2, req2); + nreq2.present = !nreq2.present; + return are_requirements_contradictions(req1, &nreq2); +} + /**********************************************************************//** Returns TRUE if req1 and req2 contradicts each other. @@ -4905,6 +4919,32 @@ enum fc_tristate tri_req_present(const struct req_context *context, return req_definitions[req->source.kind].cb(context, other_player, req); } +/**********************************************************************//** + Evaluates req in context with other_player to fc_tristate. + + context may be NULL. This is equivalent to passing an empty context. + + Fields of context that are NULL are considered unspecified + and will produce TRI_MAYBE if req needs them to evaluate. +**************************************************************************/ +enum fc_tristate tri_req_active(const struct req_context *context, + const struct player *other_player, + const struct requirement *req) +{ + enum fc_tristate eval = tri_req_present(context, other_player, req); + + if (!req->present) { + if (TRI_NO == eval) { + return TRI_YES; + } + if (TRI_YES == eval) { + return TRI_NO; + } + } + + return eval; +} + /**********************************************************************//** Checks the requirement(s) to see if they are active on the given target. @@ -4955,6 +4995,190 @@ bool are_reqs_active_ranges(const enum req_range min_range, return TRUE; } +/**********************************************************************//** + A tester callback for tri_reqs_cb_active() that uses the default + requirement calculation except for requirements in data[data_sz] array + and ones implied by them or contradicting them +**************************************************************************/ +enum fc_tristate default_tester_cb + (const struct req_context *context, + const struct player *other_player, + const struct requirement *req, + void *data, int data_sz) +{ + fc_assert_ret_val(data || data_sz == 0, TRI_NO); + + for (int i = 0; i < data_sz; i++) { + if (are_requirements_contradictions(&((struct requirement *) data)[i], + req)) { + return TRI_NO; + } else if (req_implies_req(&((struct requirement *) data)[i], req)) { + return TRI_YES; + } + } + return tri_req_active(context, other_player, req); +} + +/**********************************************************************//** + Test requirements in reqs with tester according to (data, data_sz) + and give the resulting tristate. + If maybe_reqs is not NULL, copies requirements that are evaluated + to TRI_MAYBE into it (stops as soon as one evaluates to TRI_NO). +**************************************************************************/ +enum fc_tristate + tri_reqs_cb_active(const struct req_context *context, + const struct player *other_player, + const struct requirement_vector *reqs, + struct requirement_vector *maybe_reqs, + req_tester_cb tester, + void *data, int data_sz) +{ + bool active = TRUE; + bool certain = TRUE; + int sz = requirement_vector_size(reqs); + + requirement_vector_iterate(reqs, preq) { + switch(tester(context, other_player, preq, + data, data_sz)) { + case TRI_NO: + active = FALSE; + certain = TRUE; + break; + case TRI_YES: + break; + case TRI_MAYBE: + certain = FALSE; + if (maybe_reqs) { + requirement_vector_append(maybe_reqs, *preq); + } + break; + default: + fc_assert(FALSE); + active = FALSE; + } + if (!active) { + break; + } + sz--; + } requirement_vector_iterate_end; + + return certain ? (active ? TRI_YES : TRI_NO) : TRI_MAYBE; +} + +/**********************************************************************//** + For requirements changing with time, will they be active for the target + after pass in period turns if nothing else changes? + Since year and calfrag changing is effect dependent, the result + may appear not precise. (Does not consider research progress etc.) +**************************************************************************/ +enum fc_tristate +tri_req_active_turns(int pass, int period, + const struct req_context *context, + const struct player *other_player, + const struct requirement *req) +{ + /* FIXME: doubles code from calendar.c */ + int ypt = get_world_bonus(EFT_TURN_YEARS); + int fpt = get_world_bonus(EFT_TURN_FRAGMENTS); + int fragment = game.info.fragment_count; + int fragment1 = fragment; /* if fragments don't advance */ + int year_inc, year_inc1; + const int slowdown = (victory_enabled(VC_SPACERACE) + ? get_world_bonus(EFT_SLOW_DOWN_TIMELINE) : 0); + bool present, present1; + + fc_assert(pass >= 0 && period >= 0); + if (slowdown >= 3) { + if (ypt > 1) { + ypt = 1; + } + } else if (slowdown >= 2) { + if (ypt > 2) { + ypt = 2; + } + } else if (slowdown >= 1) { + if (ypt > 5) { + ypt = 5; + } + } + year_inc = ypt * pass; + year_inc1 = year_inc + ypt * period; + if (game.calendar.calendar_fragments) { + int fragment_years; + + fragment += fpt * pass; + fragment_years = fragment / game.calendar.calendar_fragments; + year_inc += fragment_years; + fragment -= fragment_years * game.calendar.calendar_fragments; + fragment1 = fragment + fpt * period; + fragment_years += fragment1 / game.calendar.calendar_fragments; + year_inc1 += fragment_years; + fragment1 -= fragment_years * game.calendar.calendar_fragments; + } + if (game.calendar.calendar_skip_0 && game.info.year < 0) { + if (year_inc + game.info.year >= 0) { + year_inc++; + year_inc1++; + } else if (year_inc1 + game.info.year >= 0) { + year_inc1++; + } + } + + switch (req->source.kind) { + case VUT_AGE: + switch (req->range) { + case REQ_RANGE_LOCAL: + if (context->unit == NULL || !is_server()) { + return TRI_MAYBE; + } else { + int ua = game.info.turn + pass - context->unit->server.birth_turn; + + present = req->source.value.age <= ua; + present1 = req->source.value.age <= ua + period; + } + break; + case REQ_RANGE_CITY: + if (context->city == NULL) { + return TRI_MAYBE; + } else { + int ca = game.info.turn + pass - context->city->turn_founded; + + present = req->source.value.age <= ca; + present1 = req->source.value.age <= ca + period; + } + case REQ_RANGE_PLAYER: + if (context->player == NULL) { + return TRI_MAYBE; + } else { + present = req->source.value.age + <= player_age(context->player) + pass; + present1 = req->source.value.age + <= player_age(context->player) + pass + period; + } + default: + return TRI_MAYBE; + } + break; + case VUT_MINYEAR: + present = game.info.year + year_inc >= req->source.value.minyear; + present1 = game.info.year + year_inc1 >= req->source.value.minyear; + break; + case VUT_MINCALFRAG: + if (game.calendar.calendar_fragments <= period) { + /* Hope that the requirement is valid and fragments advance fine */ + return TRI_YES; + } + present = fragment >= req->source.value.mincalfrag; + present1 = fragment1 >= req->source.value.mincalfrag; + break; + default: + /* No special handling invented */ + return tri_req_active(context, other_player, req); + } + return BOOL_TO_TRISTATE(req->present + ? present || present1 : !(present && present1)); +} + /**********************************************************************//** Gives a suggestion may req ever evaluate to another value with given context. (The other player is not supplied since it has no value diff --git a/common/requirements.h b/common/requirements.h index 1eeccb3ba9..c10ca0ce8a 100644 --- a/common/requirements.h +++ b/common/requirements.h @@ -153,9 +153,15 @@ bool are_requirements_equal(const struct requirement *req1, bool are_requirements_contradictions(const struct requirement *req1, const struct requirement *req2); +bool req_implies_req(const struct requirement *req1, + const struct requirement *req2); + bool does_req_contradicts_reqs(const struct requirement *req, const struct requirement_vector *vec); +enum fc_tristate tri_req_active(const struct req_context *context, + const struct player *other_player, + const struct requirement *req); bool is_req_active(const struct req_context *context, const struct player *other_player, const struct requirement *req, @@ -171,6 +177,32 @@ bool are_reqs_active_ranges(const enum req_range min_range, const struct requirement_vector *reqs, const enum req_problem_type prob_type); +/* Type of a callback that tests requirements due to a context + * and something else in some manner different from tri_req_active() */ +typedef enum fc_tristate + (*req_tester_cb)(const struct req_context *context, + const struct player *other_player, + const struct requirement *req, + void *data, int data_sz); + +enum fc_tristate +default_tester_cb(const struct req_context *context, + const struct player *other_player, + const struct requirement *req, + void *data, int data_sz); +enum fc_tristate + tri_reqs_cb_active(const struct req_context *context, + const struct player *other_player, + const struct requirement_vector *reqs, + struct requirement_vector *maybe_reqs, + req_tester_cb tester, + void *data, int data_sz); +enum fc_tristate +tri_req_active_turns(int pass, int period, + const struct req_context *context, + const struct player *other_player, + const struct requirement *req); + enum req_unchanging_status is_req_unchanging(const struct req_context *context, const struct requirement *req); -- 2.34.1