wesnoth/src/team.cpp
Chris Beck ec379ce355 create "fight_on_without_leader" field of [side]
this field is intended to allow battles to continue after the death of a leader
if a side has this flag enabled and any units alive, they will be considered
to have a leader regardless of canrecruit.
2014-04-08 20:13:56 -04:00

904 lines
23 KiB
C++

/*
Copyright (C) 2003 - 2014 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Team-management, allies, setup at start of scenario.
*/
#include "team.hpp"
#include "ai/manager.hpp"
#include "game_events/pump.hpp"
#include "gamestatus.hpp"
#include "map.hpp"
#include "resources.hpp"
#include "game_preferences.hpp"
#include "whiteboard/side_actions.hpp"
#include <boost/foreach.hpp>
static lg::log_domain log_engine("engine");
#define DBG_NG LOG_STREAM(debug, log_engine)
#define LOG_NG LOG_STREAM(info, log_engine)
#define WRN_NG LOG_STREAM(warn, log_engine)
static lg::log_domain log_engine_enemies("engine/enemies");
#define DBG_NGE LOG_STREAM(debug, log_engine_enemies)
#define LOG_NGE LOG_STREAM(info, log_engine_enemies)
#define WRN_NGE LOG_STREAM(warn, log_engine_enemies)
static std::vector<team> *&teams = resources::teams;
//static member initialization
const int team::default_team_gold_ = 100;
// Update this list of attributes if you change what is used to define a side
// (excluding those attributes used to define the side's leader).
const char * const team::attributes[] = {
"ai_config", "color", "controller", "current_player",
"fight_on_without_leader", "flag",
"flag_icon", "fog", "fog_data", "gold", "hidden", "income",
"no_leader", "objectives", "objectives_changed", "persistent", "lost",
"recall_cost", "recruit", "save_id", "scroll_to_leader",
"share_maps", "share_view", "shroud", "shroud_data", "start_gold",
"suppress_end_turn_confirmation",
"team_name", "user_team_name", "village_gold", "village_support",
// Multiplayer attributes.
"action_bonus_count", "allow_changes", "allow_player", "color_lock",
"countdown_time", "disallow_observers", "faction",
"faction_from_recruit", "faction_name", "gold_lock", "income_lock",
"leader", "random_leader", "team_lock", "terrain_liked",
"user_description", "default_recruit", "controller_lock", "chose_random",
// Terminate the list with NULL.
NULL };
const std::vector<team>& teams_manager::get_teams()
{
assert(teams);
return *teams;
}
team::team_info::team_info() :
name(),
gold(0),
start_gold(0),
gold_add(false),
income(0),
income_per_village(0),
support_per_village(1),
minimum_recruit_price(0),
recall_cost(0),
can_recruit(),
team_name(),
user_team_name(),
save_id(),
current_player(),
countdown_time(),
action_bonus_count(0),
flag(),
flag_icon(),
description(),
scroll_to_leader(true),
objectives(),
objectives_changed(false),
controller(),
share_maps(false),
share_view(false),
disallow_observers(false),
allow_player(false),
chose_random(false),
no_leader(true),
fight_on_without_leader(false),
hidden(true),
no_turn_confirmation(false),
color(),
side(0),
persistent(false),
lost(false)
{
}
void team::team_info::read(const config &cfg)
{
name = cfg["name"].str();
gold = cfg["gold"];
income = cfg["income"];
team_name = cfg["team_name"].str();
user_team_name = cfg["user_team_name"];
save_id = cfg["save_id"].str();
current_player = cfg["current_player"].str();
countdown_time = cfg["countdown_time"].str();
action_bonus_count = cfg["action_bonus_count"];
flag = cfg["flag"].str();
flag_icon = cfg["flag_icon"].str();
description = cfg["id"].str();
scroll_to_leader = cfg["scroll_to_leader"].to_bool(true);
objectives = cfg["objectives"];
objectives_changed = cfg["objectives_changed"].to_bool();
disallow_observers = cfg["disallow_observers"].to_bool();
allow_player = cfg["allow_player"].to_bool(true);
chose_random = cfg["chose_random"].to_bool(false);
no_leader = cfg["no_leader"].to_bool();
fight_on_without_leader = cfg["fight_on_without_leader"].to_bool(false);
hidden = cfg["hidden"].to_bool();
no_turn_confirmation = cfg["suppress_end_turn_confirmation"].to_bool();
side = cfg["side"].to_int(1);
if(cfg.has_attribute("color")) {
color = cfg["color"].str();
} else {
color = cfg["side"].str();
}
// If arel starting new scenario override settings from [ai] tags
if (!user_team_name.translatable())
user_team_name = t_string::from_serialized(user_team_name);
if(cfg.has_attribute("ai_config")) {
ai::manager::add_ai_for_side_from_file(side, cfg["ai_config"], true);
} else {
ai::manager::add_ai_for_side_from_config(side, cfg, true);
}
std::vector<std::string> recruits = utils::split(cfg["recruit"]);
can_recruit.insert(recruits.begin(), recruits.end());
// at the start of a scenario "start_gold" is not set, we need to take the
// value from the gold setting (or fall back to the gold default)
if (!cfg["start_gold"].empty())
start_gold = cfg["start_gold"];
else if (!cfg["gold"].empty())
start_gold = gold;
else
start_gold = default_team_gold_;
if(team_name.empty()) {
team_name = cfg["side"].str();
}
if(save_id.empty()) {
save_id = description;
}
if (current_player.empty()) {
current_player = save_id;
}
income_per_village = cfg["village_gold"].to_int(game_config::village_income);
recall_cost = cfg["recall_cost"].to_int(game_config::recall_cost);
const std::string& village_support = cfg["village_support"];
if(village_support.empty())
support_per_village = game_config::village_support;
else
support_per_village = lexical_cast_default<int>(village_support, game_config::village_support);
std::string control = cfg["controller"];
//by default, persistence of a team is set depending on the controller
persistent = true;
if (control == "human")
controller = HUMAN;
else if (control == "network")
controller = NETWORK;
else if (control == "network_ai")
controller = NETWORK_AI;
else if (control == "null")
{
disallow_observers = cfg["disallow_observers"].to_bool(true);
controller = EMPTY;
persistent = false;
}
else
{
controller = AI;
persistent = false;
}
//override persistence flag if it is explicitly defined in the config
persistent = cfg["persistent"].to_bool(persistent);
//========================================================
//END OF MESSY CODE
// Share_view and share_maps can't both be enabled,
// so share_view overrides share_maps.
share_view = cfg["share_view"].to_bool();
share_maps = !share_view && cfg["share_maps"].to_bool(true);
LOG_NG << "team_info::team_info(...): team_name: " << team_name
<< ", share_maps: " << share_maps << ", share_view: " << share_view << ".\n";
}
char const *team::team_info::controller_string() const
{
switch(controller) {
case AI: return "ai";
case HUMAN: return "human";
case NETWORK: return "network";
case NETWORK_AI: return "network_ai";
case IDLE: return "idle";
case EMPTY: return "null";
default: assert(false); return NULL;
}
}
void team::team_info::write(config& cfg) const
{
cfg["gold"] = gold;
cfg["start_gold"] = start_gold;
cfg["gold_add"] = gold_add;
cfg["income"] = income;
cfg["name"] = name;
cfg["team_name"] = team_name;
cfg["user_team_name"] = user_team_name;
cfg["save_id"] = save_id;
cfg["current_player"] = current_player;
cfg["flag"] = flag;
cfg["flag_icon"] = flag_icon;
cfg["id"] = description;
cfg["objectives"] = objectives;
cfg["objectives_changed"] = objectives_changed;
cfg["countdown_time"]= countdown_time;
cfg["action_bonus_count"]= action_bonus_count;
cfg["village_gold"] = income_per_village;
cfg["village_support"] = support_per_village;
cfg["recall_cost"] = recall_cost;
cfg["disallow_observers"] = disallow_observers;
cfg["allow_player"] = allow_player;
cfg["chose_random"] = chose_random;
cfg["no_leader"] = no_leader;
cfg["fight_on_without_leader"] = fight_on_without_leader;
cfg["hidden"] = hidden;
cfg["suppress_end_turn_confirmation"] = no_turn_confirmation;
cfg["scroll_to_leader"] = scroll_to_leader;
cfg["controller"] = (controller == IDLE ? "human" : controller_string());
std::stringstream can_recruit_str;
for(std::set<std::string>::const_iterator cr = can_recruit.begin(); cr != can_recruit.end(); ++cr) {
if(cr != can_recruit.begin())
can_recruit_str << ",";
can_recruit_str << *cr;
}
cfg["recruit"] = can_recruit_str.str();
cfg["share_maps"] = share_maps;
cfg["share_view"] = share_view;
cfg["color"] = color;
cfg["persistent"] = persistent;
cfg["lost"] = lost;
cfg.add_child("ai",ai::manager::to_config(side));
}
team::team() :
savegame_config(),
gold_(0),
villages_(),
shroud_(),
fog_(),
fog_clearer_(),
auto_shroud_updates_(true),
info_(),
countdown_time_(0),
action_bonus_count_(0),
recall_list_(),
last_recruit_(),
enemies_(),
ally_shroud_(),
ally_fog_(),
planned_actions_()
{
}
team::~team()
{
}
void team::build(const config &cfg, const gamemap& map, int gold)
{
gold_ = gold;
info_.read(cfg);
fog_.set_enabled(cfg["fog"].to_bool());
fog_.read(cfg["fog_data"]);
shroud_.set_enabled(cfg["shroud"].to_bool());
shroud_.read(cfg["shroud_data"]);
auto_shroud_updates_ = cfg["auto_shroud"].to_bool(auto_shroud_updates_);
LOG_NG << "team::team(...): team_name: " << info_.team_name
<< ", shroud: " << uses_shroud() << ", fog: " << uses_fog() << ".\n";
// Load the WML-cleared fog.
const config &fog_override = cfg.child("fog_override");
if ( fog_override ) {
const std::vector<map_location> fog_vector =
parse_location_range(fog_override["x"], fog_override["y"], true);
fog_clearer_.insert(fog_vector.begin(), fog_vector.end());
}
// To ensure some minimum starting gold,
// gold is the maximum of 'gold' and what is given in the config file
gold_ = std::max(gold, info_.gold);
if (gold_ != info_.gold)
info_.start_gold = gold;
// Old code was doing:
// info_.start_gold = str_cast(gold) + " (" + info_.start_gold + ")";
// Was it correct?
// Load in the villages the side controls at the start
BOOST_FOREACH(const config &v, cfg.child_range("village"))
{
map_location loc(v, resources::gamedata);
if (map.is_village(loc)) {
villages_.insert(loc);
} else {
WRN_NG << "[side] " << name() << " [village] points to a non-village location " << loc << "\n";
}
}
countdown_time_ = cfg["countdown_time"];
action_bonus_count_ = cfg["action_bonus_count"];
planned_actions_.reset(new wb::side_actions());
planned_actions_->set_team_index(info_.side - 1);
}
void team::write(config& cfg) const
{
info_.write(cfg);
cfg["auto_shroud"] = auto_shroud_updates_;
cfg["shroud"] = uses_shroud();
cfg["fog"] = uses_fog();
cfg["gold"] = gold_;
// Write village locations
for(std::set<map_location>::const_iterator t = villages_.begin(); t != villages_.end(); ++t) {
t->write(cfg.add_child("village"));
}
cfg["shroud_data"] = shroud_.write();
cfg["fog_data"] = fog_.write();
if ( !fog_clearer_.empty() )
write_location_range(fog_clearer_, cfg.add_child("fog_override"));
cfg["countdown_time"] = countdown_time_;
cfg["action_bonus_count"] = action_bonus_count_;
}
bool team::get_village(const map_location& loc, const int owner_side, const bool fire_event)
{
villages_.insert(loc);
bool gamestate_changed = false;
if(fire_event) {
config::attribute_value& var = resources::gamedata->get_variable("owner_side");
const config::attribute_value old_value = var;
var = owner_side;
gamestate_changed = game_events::fire("capture",loc);
if(old_value.blank())
resources::gamedata->clear_variable("owner_side");
else
var = old_value;
}
return gamestate_changed;
}
void team::lose_village(const map_location& loc)
{
const std::set<map_location>::const_iterator vil = villages_.find(loc);
assert(vil != villages_.end());
villages_.erase(vil);
}
void team::set_recruits(const std::set<std::string>& recruits)
{
info_.can_recruit = recruits;
info_.minimum_recruit_price = 0;
ai::manager::raise_recruit_list_changed();
}
void team::add_recruit(const std::string &recruit)
{
info_.can_recruit.insert(recruit);
info_.minimum_recruit_price = 0;
ai::manager::raise_recruit_list_changed();
}
int team::minimum_recruit_price() const
{
if(info_.minimum_recruit_price){
return info_.minimum_recruit_price;
}else{
int min = 20;
BOOST_FOREACH(std::string recruit, info_.can_recruit){
const unit_type *ut = unit_types.find(recruit);
if(!ut)
continue;
else{
if(ut->cost() < min)
min = ut->cost();
}
}
info_.minimum_recruit_price = min;
}
return info_.minimum_recruit_price;
}
bool team::calculate_enemies(size_t index) const
{
if(teams == NULL || index >= teams->size()) {
return false;
}
while(enemies_.size() <= index) {
enemies_.push_back(calculate_is_enemy(enemies_.size()));
}
return enemies_.back();
}
bool team::calculate_is_enemy(size_t index) const
{
// We're not enemies of ourselves
if(&(*teams)[index] == this) {
return false;
}
// We are friends with anyone who we share a teamname with
std::vector<std::string> our_teams = utils::split(info_.team_name),
their_teams = utils::split((*teams)[index].info_.team_name);
LOG_NGE << "team " << info_.side << " calculates if it has enemy in team "<<index+1 << "; our team_name ["<<info_.team_name<<"], their team_name is ["<<(*teams)[index].info_.team_name<<"]"<< std::endl;
for(std::vector<std::string>::const_iterator t = our_teams.begin(); t != our_teams.end(); ++t) {
if(std::find(their_teams.begin(), their_teams.end(), *t) != their_teams.end())
{
LOG_NGE << "team " << info_.side << " found same team name [" << *t << "] in team "<< index+1 << std::endl;
return false;
} else {
LOG_NGE << "team " << info_.side << " not found same team name [" << *t << "] in team "<< index+1 << std::endl;
}
}
LOG_NGE << "team " << info_.side << " has enemy in team " << index+1 << std::endl;
return true;
}
void team::set_share_maps( bool share_maps ){
// Share_view and share_maps can't both be enabled,
// so share_view overrides share_maps.
// If you want to change them, be sure to change share_view FIRST
info_.share_maps = !info_.share_view && share_maps;
}
void team::set_share_view( bool share_view ){
info_.share_view = share_view;
}
void team::change_controller(const std::string& controller)
{
team::CONTROLLER cid;
if (controller == "human")
cid = team::HUMAN;
else if (controller == "network")
cid = team::NETWORK;
else if (controller == "network_ai")
cid = team::NETWORK_AI;
else if (controller == "null")
cid = team::EMPTY;
else if (controller == "idle")
cid = team::IDLE;
else
cid = team::AI;
info_.controller = cid;
}
void team::change_team(const std::string &name, const t_string &user_name)
{
info_.team_name = name;
if (!user_name.empty())
{
info_.user_team_name = user_name;
}
else
{
info_.user_team_name = name;
}
clear_caches();
}
void team::clear_caches(){
// Reset the cache of allies for all teams
if(teams != NULL) {
for(std::vector<team>::const_iterator i = teams->begin(); i != teams->end(); ++i) {
i->enemies_.clear();
i->ally_shroud_.clear();
i->ally_fog_.clear();
}
}
}
void team::set_objectives(const t_string& new_objectives, bool silently)
{
info_.objectives = new_objectives;
if(!silently)
info_.objectives_changed = true;
}
bool team::shrouded(const map_location& loc) const
{
if(!teams)
return shroud_.value(loc.x+1,loc.y+1);
return shroud_.shared_value(ally_shroud(*teams),loc.x+1,loc.y+1);
}
bool team::fogged(const map_location& loc) const
{
if(shrouded(loc)) return true;
// Check for an override of fog.
if ( fog_clearer_.count(loc) > 0 )
return false;
if(!teams)
return fog_.value(loc.x+1,loc.y+1);
return fog_.shared_value(ally_fog(*teams),loc.x+1,loc.y+1);
}
const std::vector<const team::shroud_map*>& team::ally_shroud(const std::vector<team>& teams) const
{
if(ally_shroud_.empty()) {
for(size_t i = 0; i < teams.size(); ++i) {
if(!is_enemy(i + 1) && (&(teams[i]) == this || teams[i].share_view() || teams[i].share_maps())) {
ally_shroud_.push_back(&(teams[i].shroud_));
}
}
}
return ally_shroud_;
}
const std::vector<const team::shroud_map*>& team::ally_fog(const std::vector<team>& teams) const
{
if(ally_fog_.empty()) {
for(size_t i = 0; i < teams.size(); ++i) {
if(!is_enemy(i + 1) && (&(teams[i]) == this || teams[i].share_view())) {
ally_fog_.push_back(&(teams[i].fog_));
}
}
}
return ally_fog_;
}
bool team::knows_about_team(size_t index, bool is_multiplayer) const
{
const team& t = (*teams)[index];
// We know about our own team
if(this == &t) return true;
// If we aren't using shroud or fog, then we know about everyone
if(!uses_shroud() && !uses_fog()) return true;
// We don't know about enemies
if(is_enemy(index+1)) return false;
// We know our allies in multiplayer
if (is_multiplayer) return true;
// We know about allies we're sharing maps with
if(share_maps() && t.uses_shroud()) return true;
// We know about allies we're sharing view with
if(share_view() && (t.uses_fog() || t.uses_shroud())) return true;
return false;
}
bool team::copy_ally_shroud()
{
if(!teams || !share_maps())
return false;
return shroud_.copy_from(ally_shroud(*teams));
}
/**
* Removes the record of hexes that were cleared of fog via WML.
* @param[in] hexes The hexes to no longer keep clear.
*/
void team::remove_fog_override(const std::set<map_location> &hexes)
{
// Take a set difference.
std::vector<map_location> result(fog_clearer_.size());
std::vector<map_location>::iterator result_end =
std::set_difference(fog_clearer_.begin(), fog_clearer_.end(),
hexes.begin(), hexes.end(), result.begin());
// Put the result into fog_clearer_.
fog_clearer_.clear();
fog_clearer_.insert(result.begin(), result_end);
}
int team::nteams()
{
if(teams == NULL) {
return 0;
} else {
return teams->size();
}
}
bool is_observer()
{
if(teams == NULL) {
return true;
}
BOOST_FOREACH(const team &t, *teams) {
if (t.is_local())
return false;
}
return true;
}
void validate_side(int side)
{
if(teams == NULL) {
return;
}
if(side < 1 || side > int(teams->size())) {
throw game::game_error("invalid side(" + str_cast(side) + ") found in unit definition");
}
}
bool team::shroud_map::clear(int x, int y)
{
if(enabled_ == false || x < 0 || y < 0)
return false;
if(x >= static_cast<int>(data_.size()))
data_.resize(x+1);
if(y >= static_cast<int>(data_[x].size()))
data_[x].resize(y+1);
if(data_[x][y] == false) {
data_[x][y] = true;
return true;
} else {
return false;
}
}
void team::shroud_map::place(int x, int y)
{
if(enabled_ == false || x < 0 || y < 0)
return;
if (x >= static_cast<int>(data_.size())) {
DBG_NG << "Couldn't place shroud on invalid x coordinate: ("
<< x << ", " << y << ") - max x: " << data_.size() - 1 << "\n";
} else if (y >= static_cast<int>(data_[x].size())) {
DBG_NG << "Couldn't place shroud on invalid y coordinate: ("
<< x << ", " << y << ") - max y: " << data_[x].size() - 1 << "\n";
} else {
data_[x][y] = false;
}
}
void team::shroud_map::reset()
{
if(enabled_ == false)
return;
for(std::vector<std::vector<bool> >::iterator i = data_.begin(); i != data_.end(); ++i) {
std::fill(i->begin(),i->end(),false);
}
}
bool team::shroud_map::value(int x, int y) const
{
if ( !enabled_ )
return false;
// Locations for which we have no data are assumed to still be covered.
if ( x < 0 || x >= static_cast<int>(data_.size()) )
return true;
if ( y < 0 || y >= static_cast<int>(data_[x].size()) )
return true;
// data_ stores whether or not a location has been cleared, while
// we want to return whether or not a location is covered.
return !data_[x][y];
}
bool team::shroud_map::shared_value(const std::vector<const shroud_map*>& maps, int x, int y) const
{
if ( !enabled_ )
return false;
// A quick abort:
if ( x < 0 || y < 0 )
return true;
// A tile is uncovered if it is uncovered on any shared map.
BOOST_FOREACH(const shroud_map * const shared_map, maps) {
if ( shared_map->enabled_ && !shared_map->value(x,y) )
return false;
}
return true;
}
std::string team::shroud_map::write() const
{
std::stringstream shroud_str;
for(std::vector<std::vector<bool> >::const_iterator sh = data_.begin(); sh != data_.end(); ++sh) {
shroud_str << '|';
for(std::vector<bool>::const_iterator i = sh->begin(); i != sh->end(); ++i) {
shroud_str << (*i ? '1' : '0');
}
shroud_str << '\n';
}
return shroud_str.str();
}
void team::shroud_map::read(const std::string& str)
{
data_.clear();
for(std::string::const_iterator sh = str.begin(); sh != str.end(); ++sh) {
if(*sh == '|')
data_.resize(data_.size()+1);
if(data_.empty() == false) {
if(*sh == '1')
data_.back().push_back(true);
else if(*sh == '0')
data_.back().push_back(false);
}
}
}
void team::shroud_map::merge(const std::string& str)
{
int x=0, y=0;
for(std::string::const_iterator sh = str.begin(); sh != str.end(); ++sh) {
if(*sh == '|' && sh != str.begin()) {
y=0;
x++;
} else if(*sh == '1') {
clear(x,y);
y++;
} else if(*sh == '0') {
y++;
}
}
}
bool team::shroud_map::copy_from(const std::vector<const shroud_map*>& maps)
{
if(enabled_ == false)
return false;
bool cleared = false;
for(std::vector<const shroud_map*>::const_iterator i = maps.begin(); i != maps.end(); ++i) {
if((*i)->enabled_ == false)
continue;
const std::vector<std::vector<bool> >& v = (*i)->data_;
for(size_t x = 0; x != v.size(); ++x) {
for(size_t y = 0; y != v[x].size(); ++y) {
if(v[x][y]) {
cleared |= clear(x,y);
}
}
}
}
return cleared;
}
const color_range team::get_side_color_range(int side){
std::string index = get_side_color_index(side);
std::map<std::string, color_range>::iterator gp=game_config::team_rgb_range.find(index);
if(gp != game_config::team_rgb_range.end()){
return(gp->second);
}
return(color_range(0x00FF0000,0x00FFFFFF,0x00000000,0x00FF0000));
}
SDL_Color team::get_side_color(int side)
{
return int_to_color(get_side_color_range(side).mid());
}
SDL_Color team::get_minimap_color(int side)
{
// Note: use mid() instead of rep() unless
// high contrast is needed over a map or minimap!
return int_to_color(get_side_color_range(side).rep());
}
std::string team::get_side_color_index(int side)
{
size_t index = size_t(side-1);
if(teams != NULL && index < teams->size()) {
const std::string side_map = (*teams)[index].color();
if(!side_map.empty()) {
return side_map;
}
}
return str_cast(side);
}
std::string team::get_side_highlight(int side)
{
return rgb2highlight(get_side_color_range(side+1).mid());
}
std::string team::get_side_highlight_pango(int side)
{
return rgb2highlight_pango(get_side_color_range(side+1).mid());
}
void team::log_recruitable(){
LOG_NG << "Adding recruitable units: \n";
for (std::set<std::string>::const_iterator it = info_.can_recruit.begin();
it != info_.can_recruit.end(); ++it) {
LOG_NG << *it << std::endl;
}
LOG_NG << "Added all recruitable units\n";
}
config team::to_config() const
{
config cfg;
config& result = cfg.add_child("side");
write(result);
return result;
}
/**
* Given the location of a village, will return the 0-based index
* of the team that currently owns it, and -1 if it is unowned.
*/
int village_owner(const map_location& loc)
{
if(! teams) {
return -1;
}
for(size_t i = 0; i != teams->size(); ++i) {
if((*teams)[i].owns_village(loc))
return i;
}
return -1;
}