From 54fcfab152e059fad3a363ff7d63c6ae03a0eded Mon Sep 17 00:00:00 2001 From: Alina Lenk Date: Thu, 16 May 2024 00:28:25 +0200 Subject: [PATCH 04/10] Add MaxDistanceSq requirement (maximum squared distance) See RM #616 Signed-off-by: Alina Lenk --- ai/default/daieffects.c | 1 + ai/default/daimilitary.c | 1 + common/fc_types.h | 1 + common/metaknowledge.c | 10 +++ common/reqtext.c | 61 ++++++++++++++ common/requirements.c | 116 +++++++++++++++++++++++---- doc/README.actions | 2 + doc/README.effects | 4 + gen_headers/enums/fc_types_enums.def | 1 + server/cityturn.c | 1 + server/ruleset/rssanity.c | 16 ++++ tools/ruledit/univ_value.c | 4 + 12 files changed, 202 insertions(+), 16 deletions(-) diff --git a/ai/default/daieffects.c b/ai/default/daieffects.c index a307ba9a8a..7c7f086dcc 100644 --- a/ai/default/daieffects.c +++ b/ai/default/daieffects.c @@ -954,6 +954,7 @@ bool dai_can_requirement_be_met_in_city(const struct requirement *preq, case VUT_ACTION: case VUT_GOOD: case VUT_MINCALFRAG: + case VUT_MAX_DISTANCE_SQ: case VUT_COUNT: /* No sensible implementation possible with data available. */ break; diff --git a/ai/default/daimilitary.c b/ai/default/daimilitary.c index ec9ecf1339..5bf24ad888 100644 --- a/ai/default/daimilitary.c +++ b/ai/default/daimilitary.c @@ -459,6 +459,7 @@ tactical_req_cb(const struct req_context *context, case VUT_TERRAINCLASS: case VUT_TERRFLAG: case VUT_TERRAINALTER: + case VUT_MAX_DISTANCE_SQ: case VUT_NONE: return tri_req_active(context, other_context, req); case VUT_COUNT: diff --git a/common/fc_types.h b/common/fc_types.h index 9acde4a8aa..0f0f140fe6 100644 --- a/common/fc_types.h +++ b/common/fc_types.h @@ -739,6 +739,7 @@ typedef union { int min_techs; int min_cities; int latitude; + int distance_sq; enum topo_flag topo_property; enum wrap_flag wrap_property; diff --git a/common/metaknowledge.c b/common/metaknowledge.c index 488dd8889c..7e1303cc52 100644 --- a/common/metaknowledge.c +++ b/common/metaknowledge.c @@ -701,6 +701,16 @@ static bool is_req_knowable(const struct player *pov_player, } } + if (req->source.kind == VUT_MAX_DISTANCE_SQ) { + if (context->tile == nullptr || other_context->tile == nullptr) { + /* The tiles may exist but not be passed when the problem type is + * RPT_POSSIBLE. */ + return prob_type == RPT_CERTAIN; + } + /* Tile locations and their distance are fixed */ + return TRUE; + } + if (req->source.kind == VUT_ACTION || req->source.kind == VUT_OTYPE) { /* This requirement type is intended to specify the situation. */ diff --git a/common/reqtext.c b/common/reqtext.c index be72a231f4..6bba4582ec 100644 --- a/common/reqtext.c +++ b/common/reqtext.c @@ -3292,6 +3292,67 @@ bool req_text_insert(char *buf, size_t bufsz, struct player *pplayer, } break; + case VUT_MAX_DISTANCE_SQ: + switch (preq->range) { + case REQ_RANGE_TILE: + fc_strlcat(buf, prefix, bufsz); + /* Test some special cases */ + switch (preq->source.value.distance_sq) + { + case 0: + if (preq->present) { + fc_strlcat(buf, _("Must be the same tile."), bufsz); + } else { + fc_strlcat(buf, _("Must not be the same tile."), bufsz); + } + break; + case 1: + if (preq->present) { + fc_strlcat(buf, _("Must be cardinally adjacent."), bufsz); + } else { + fc_strlcat(buf, _("Must not be cardinally adjacent."), bufsz); + } + break; + case 2: + case 3: + if (preq->present) { + fc_strlcat(buf, _("Must be adjacent."), bufsz); + } else { + fc_strlcat(buf, _("Must not be adjacent."), bufsz); + } + break; + + default: + if (preq->present) { + cat_snprintf(buf, bufsz, + _("The squared distance between the tiles " + "must be at most %d."), + preq->source.value.distance_sq); + } else { + cat_snprintf(buf, bufsz, + _("The squared distance between the tiles " + "must be at least %d."), + preq->source.value.distance_sq + 1); + } + break; + } + return TRUE; + case REQ_RANGE_CADJACENT: + case REQ_RANGE_ADJACENT: + case REQ_RANGE_CITY: + case REQ_RANGE_TRADE_ROUTE: + case REQ_RANGE_CONTINENT: + case REQ_RANGE_PLAYER: + case REQ_RANGE_TEAM: + case REQ_RANGE_ALLIANCE: + case REQ_RANGE_WORLD: + case REQ_RANGE_LOCAL: + case REQ_RANGE_COUNT: + /* Not supported. */ + break; + } + break; + case VUT_COUNT: break; } diff --git a/common/requirements.c b/common/requirements.c index da6846be34..4e2ccb219d 100644 --- a/common/requirements.c +++ b/common/requirements.c @@ -611,6 +611,12 @@ void universal_value_from_str(struct universal *source, const char *value) return; } break; + case VUT_MAX_DISTANCE_SQ: + source->value.distance_sq = atoi(value); + if (0 <= source->value.distance_sq) { + return; + } + break; case VUT_COUNT: break; } @@ -843,6 +849,9 @@ struct universal universal_by_number(const enum universals_n kind, case VUT_MAXLATITUDE: source.value.latitude = value; return source; + case VUT_MAX_DISTANCE_SQ: + source.value.distance_sq = value; + return source; case VUT_COUNT: break; } @@ -994,6 +1003,8 @@ int universal_number(const struct universal *source) case VUT_MINLATITUDE: case VUT_MAXLATITUDE: return source->value.latitude; + case VUT_MAX_DISTANCE_SQ: + return source->value.distance_sq; case VUT_COUNT: break; } @@ -1106,6 +1117,7 @@ struct requirement req_from_str(const char *type, const char *range, case VUT_MAXTILEUNITS: case VUT_MINLATITUDE: case VUT_MAXLATITUDE: + case VUT_MAX_DISTANCE_SQ: req.range = REQ_RANGE_TILE; break; case VUT_COUNTER: @@ -1263,6 +1275,7 @@ struct requirement req_from_str(const char *type, const char *range, && req.range != REQ_RANGE_ADJACENT); break; case VUT_TERRAINALTER: /* XXX could in principle support C/ADJACENT */ + case VUT_MAX_DISTANCE_SQ: invalid = (req.range != REQ_RANGE_TILE); break; case VUT_CITYTILE: @@ -1396,6 +1409,7 @@ struct requirement req_from_str(const char *type, const char *range, case VUT_MINCITIES: case VUT_MINLATITUDE: case VUT_MAXLATITUDE: + case VUT_MAX_DISTANCE_SQ: /* Most requirements don't support 'survives'. */ invalid = survives; break; @@ -1678,6 +1692,28 @@ bool req_implies_req(const struct requirement *req1, return are_requirements_contradictions(req1, &nreq2); } +/**********************************************************************//** + Returns TRUE iff two bounds that could each be either an upper or lower + bound are contradicting each other. This function assumes that of the + upper and lower bounds, one will be inclusive, the other exclusive. +**************************************************************************/ +static inline bool are_bounds_contradictions(int bound1, bool is_upper1, + int bound2, bool is_upper2) +{ + /* If the bounds are on opposite sides, and one is inclusive, the other + * exclusive, the number of values that satisfy both bounds is exactly + * their difference, (upper bound) - (lower bound). + * The bounds contradict each other iff this difference is 0 or less, + * i.e. iff (upper bound) <= (lower bound) */ + if (is_upper1 && !is_upper2) { + return bound1 <= bound2; + } else if (!is_upper1 && is_upper2) { + return bound1 >= bound2; + } + /* Both are upper or both are lower ~> no contradiction possible */ + return FALSE; +} + /**********************************************************************//** Returns TRUE if req1 and req2 contradicts each other. @@ -1757,21 +1793,10 @@ bool are_requirements_contradictions(const struct requirement *req1, /* Finding contradictions across requirement kinds aren't supported * for MinMoveFrags requirements. */ return FALSE; - } else if (req1->present == req2->present) { - /* No contradiction possible. */ - return FALSE; - } else { - /* Number of move fragments left can't be larger than the number - * required to be present and smaller than the number required to not - * be present when the number required to be present is smaller than - * the number required to not be present. */ - if (req1->present) { - return req1->source.value.minmoves >= req2->source.value.minmoves; - } else { - return req1->source.value.minmoves <= req2->source.value.minmoves; - } } - break; + return are_bounds_contradictions( + req1->source.value.minmoves, !req1->present, + req2->source.value.minmoves, !req2->present); case VUT_MINLATITUDE: case VUT_MAXLATITUDE: if (req2->source.kind != VUT_MINLATITUDE @@ -1853,6 +1878,15 @@ bool are_requirements_contradictions(const struct requirement *req1, } return FALSE; + case VUT_MAX_DISTANCE_SQ: + if (req2->source.kind != VUT_MAX_DISTANCE_SQ) { + /* Finding contradictions across requirement kinds isn't supported + * for MaxDistanceSq requirements. */ + return FALSE; + } + return are_bounds_contradictions( + req1->source.value.distance_sq, req1->present, + req2->source.value.distance_sq, req2->present); default: /* No special knowledge exists. The requirements aren't the exact * opposite of each other per the initial check. */ @@ -1945,9 +1979,10 @@ static inline bool players_in_same_range(const struct player *pplayer1, #define IS_REQ_ACTIVE_VARIANT_ASSERT(_kind) \ { \ - fc_assert_ret_val(req != NULL, TRI_MAYBE); \ + fc_assert_ret_val(req != nullptr, TRI_MAYBE); \ fc_assert_ret_val(req->source.kind == _kind, TRI_MAYBE); \ - fc_assert(context != NULL); \ + fc_assert(context != nullptr); \ + fc_assert(other_context != nullptr); \ } /**********************************************************************//** @@ -5531,6 +5566,42 @@ is_latitude_req_active(const struct civ_map *nmap, return TRI_MAYBE; } +/**********************************************************************//** + Determine whether a maximum squared distance requirement is satisfied in + a given context, ignoring parts of the requirement that can be handled + uniformly for all requirement types. + + context, other_context and req must not be null, + and req must be a max squared distance requirement +**************************************************************************/ +static enum fc_tristate +is_max_distance_sq_req_active(const struct civ_map *nmap, + const struct req_context *context, + const struct req_context *other_context, + const struct requirement *req) +{ + IS_REQ_ACTIVE_VARIANT_ASSERT(VUT_MAX_DISTANCE_SQ); + + switch (req->range) { + case REQ_RANGE_TILE: + if (context->tile == nullptr || other_context->tile == nullptr) { + return TRI_MAYBE; + } + return BOOL_TO_TRISTATE( + sq_map_distance(context->tile, other_context->tile) + <= req->source.value.distance_sq + ); + default: + break; + } + + fc_assert_msg(FALSE, + "Illegal range %d for max squared distance requirement.", + req->range); + + return TRI_MAYBE; +} + /**********************************************************************//** Determine whether a minimum year requirement is satisfied in a given context, ignoring parts of the requirement that can be handled uniformly @@ -5659,6 +5730,7 @@ static struct req_def req_definitions[VUT_COUNT] = { [VUT_IMPR_FLAG] = {is_buildingflag_req_active, REQUCH_YES}, [VUT_PLAYER_FLAG] = {is_plr_flag_req_active, REQUCH_NO}, [VUT_PLAYER_STATE] = {is_plr_state_req_active, REQUCH_NO}, + [VUT_MAX_DISTANCE_SQ] = {is_max_distance_sq_req_active, REQUCH_YES}, [VUT_MAXLATITUDE] = {is_latitude_req_active, REQUCH_YES}, [VUT_MAXTILEUNITS] = {is_maxunitsontile_req_active, REQUCH_NO}, [VUT_MINCALFRAG] = {is_mincalfrag_req_active, REQUCH_NO}, @@ -6228,6 +6300,7 @@ bool universal_never_there(const struct universal *source) case VUT_TERRFLAG: case VUT_TERRAINALTER: case VUT_MINYEAR: + case VUT_MAX_DISTANCE_SQ: case VUT_NONE: case VUT_COUNT: /* Not implemented. */ @@ -6887,6 +6960,8 @@ bool are_universals_equal(const struct universal *psource1, case VUT_MINLATITUDE: case VUT_MAXLATITUDE: return psource1->value.latitude == psource2->value.latitude; + case VUT_MAX_DISTANCE_SQ: + return psource1->value.distance_sq == psource2->value.distance_sq; case VUT_COUNT: break; } @@ -7044,6 +7119,10 @@ const char *universal_rule_name(const struct universal *psource) case VUT_MAXLATITUDE: fc_snprintf(buffer, sizeof(buffer), "%d", psource->value.latitude); + return buffer; + case VUT_MAX_DISTANCE_SQ: + fc_snprintf(buffer, sizeof(buffer), "%d", psource->value.distance_sq); + return buffer; case VUT_COUNT: break; @@ -7401,6 +7480,11 @@ const char *universal_name_translation(const struct universal *psource, cat_snprintf(buf, bufsz, _("Latitude <= %d"), psource->value.latitude); return buf; + case VUT_MAX_DISTANCE_SQ: + /* TRANS: here <= means 'less than or equal'. */ + cat_snprintf(buf, bufsz, _("Squared distance <= %d"), + psource->value.distance_sq); + return buf; case VUT_COUNT: break; } diff --git a/doc/README.actions b/doc/README.actions index 6d9ca96b37..fec101e614 100644 --- a/doc/README.actions +++ b/doc/README.actions @@ -102,6 +102,8 @@ actor requirements. target requirement vector to the actor requirement vector. * Asymmetric local DiplRel requirements must test for the same thing in the opposite direction. Example: "Hosts embassy" -> "Has embassy" +A "MaxDistanceSq" requirement with the range "Tile" should always be put in +the actor requirements, for the same reasons. Actions and Lua =============== diff --git a/doc/README.effects b/doc/README.effects index e57e703289..b1337b6b38 100644 --- a/doc/README.effects +++ b/doc/README.effects @@ -118,6 +118,7 @@ Activity: Local MinMoveFrags: Local MinVeteran: Local MinHitPoints: Local +MaxDistanceSq: Tile MinSize is the minimum size of a city required. @@ -130,6 +131,9 @@ CityTile is "Center" (city center), "Claimed" (tile owned by any player), is a port, it's a tile of the nearby ocean but not of its continent). MinLatitude and MaxLatitude are numbers from -1000 (south pole) to 1000 (north pole). +MaxDistanceSq is about the (squared) distance between two tiles; currently + only available for action enablers (see also README.actions) and a select + few effects. CityStatus is "OwnedByOriginal", "Transferred", "Starved", "Disorder", or "Celebration" The difference between "OwnedByOriginal" and "Transferred" is that diff --git a/gen_headers/enums/fc_types_enums.def b/gen_headers/enums/fc_types_enums.def index 0040bd7c6a..bdc141845b 100644 --- a/gen_headers/enums/fc_types_enums.def +++ b/gen_headers/enums/fc_types_enums.def @@ -98,4 +98,5 @@ values PLAYER_STATE "PlayerState" FORM_AGE "FormAge" MINCITIES "MinCities" + MAX_DISTANCE_SQ "MaxDistanceSq" end diff --git a/server/cityturn.c b/server/cityturn.c index f4c67de5c4..60934a0747 100644 --- a/server/cityturn.c +++ b/server/cityturn.c @@ -1969,6 +1969,7 @@ static bool worklist_item_postpone_req_vec(struct universal *target, case VUT_ACTION: case VUT_OTYPE: case VUT_SPECIALIST: + case VUT_MAX_DISTANCE_SQ: case VUT_TERRAINALTER: /* XXX could do this in principle */ /* Will only happen with a bogus ruleset. */ log_error("worklist_change_build_target() has bogus preq"); diff --git a/server/ruleset/rssanity.c b/server/ruleset/rssanity.c index 671f2d79e2..5ca9b6a01d 100644 --- a/server/ruleset/rssanity.c +++ b/server/ruleset/rssanity.c @@ -354,6 +354,7 @@ static bool sanity_check_req_set(rs_conversion_logger logger, case VUT_IMPR_GENUS: case VUT_ORIGINAL_OWNER: /* City range -> only one original owner */ case VUT_FORM_AGE: + case VUT_MAX_DISTANCE_SQ: /* Breaks nothing, but has no sense either */ /* There can be only one requirement of these types (with current * range limitations) * Requirements might be identical, but we consider multiple @@ -1343,6 +1344,21 @@ bool sanity_check_ruleset_data(struct rscompat_info *compat) req_to_fstring(preq, &astr)); astr_free(&astr); ok = FALSE; + } else if (preq->source.kind == VUT_MAX_DISTANCE_SQ + && preq->range == REQ_RANGE_TILE) { + struct astring astr; + + /* A Tile-ranged MaxDistanceSq requirement can be expressed as a + * requirement in actor_reqs. Demand that it is there. */ + ruleset_error(logger, LOG_ERROR, + _("Action enabler for %s has a tile MaxDistanceSq " + "requirement %s in target_reqs! Please read the " + "section \"Requirement vector rules\" in " + "doc/README.actions"), + action_id_rule_name(act), + req_to_fstring(preq, &astr)); + astr_free(&astr); + ok = FALSE; } } requirement_vector_iterate_end; diff --git a/tools/ruledit/univ_value.c b/tools/ruledit/univ_value.c index 0904a659d6..5a7923c363 100644 --- a/tools/ruledit/univ_value.c +++ b/tools/ruledit/univ_value.c @@ -252,6 +252,9 @@ bool universal_value_initial(struct universal *src) case VUT_MAXLATITUDE: src->value.latitude = 0; return TRUE; + case VUT_MAX_DISTANCE_SQ: + src->value.distance_sq = 0; + return TRUE; case VUT_COUNT: fc_assert(src->kind != VUT_COUNT); return FALSE; @@ -504,6 +507,7 @@ void universal_kind_values(struct universal *univ, case VUT_MINCITIES: case VUT_MINLATITUDE: case VUT_MAXLATITUDE: + case VUT_MAX_DISTANCE_SQ: /* Requirement types having numerical value */ cb(nullptr, FALSE, data); break; -- 2.34.1