Files
core/hwe/sammo/GeneralAI.php
T
2020-04-07 02:55:59 +09:00

3531 lines
121 KiB
PHP

<?php
namespace sammo;
use DateTimeImmutable;
use Generator;
use PDO;
use sammo\Command\GeneralCommand;
use sammo\Command\NationCommand;
use SplQueue;
class GeneralAI
{
/**
* @var General $general
*/
protected $general;
/** @var array */
protected $city;
/** @var array */
protected $nation;
/** @var int */
protected $genType;
/** @var AutorunGeneralPolicy */
protected $generalPolicy;
/** @var AutorunNationPolicy */
protected $nationPolicy;
/** @var array */
protected $env;
protected $baseDevelCost;
protected $leadership;
protected $strength;
protected $intel;
protected $fullLeadership;
protected $fullStrength;
protected $fullIntel;
protected $dipState;
protected $warTargetNation;
/** @var bool */
protected $attackable;
protected $devRate = null;
protected $nationCities;
protected $frontCities;
protected $supplyCities;
protected $backupCities;
protected $warRoute;
/** @var General[] */
protected $nationGenerals;
/** @var General[] */
protected $npcCivilGenerals;
/** @var General[] */
protected $npcWarGenerals;
/** @var General[] */
protected $userGenerals;
/** @var General[] */
protected $userWarGenerals;
/** @var General[] */
protected $userCivilGenerals;
/** @var General[] */
protected $lostGenerals;
/** @var General[] */
protected $troopLeaders;
const t무장 = 1;
const t지장 = 2;
const t통솔장 = 4;
const d평화 = 0;
const d선포 = 1;
const d징병 = 2;
const d직전 = 3;
const d전쟁 = 4;
//수뇌용
public function __construct(General $general)
{
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$this->env = $gameStor->getValues(['startyear', 'year', 'month', 'turnterm', 'killturn', 'scenario', 'gold_rate', 'rice_rate', 'develcost', 'autorun_user']);
$this->baseDevelCost = $this->env['develcost'] * 12;
$this->general = $general;
if ($general->getRawCity() === null) {
$city = $db->queryFirstRow('SELECT * FROM city WHERE city = %i', $general->getCityID());
$general->setRawCity($city);
}
$this->city = $general->getRawCity();
$this->nation = $db->queryFirstRow(
'SELECT nation,name,color,capital,capset,gennum,gold,rice,bill,rate,rate_tmp,scout,war,strategic_cmd_limit,surlimit,tech,power,level,type,aux FROM nation WHERE nation = %i',
$general->getNationID()
) ?? [
'nation' => 0,
'level' => 0,
'capital' => 0,
'capset' => false,
'gennum' => 0,
'tech' => 0,
'gold' => 0,
'rice' => 0,
'type' => GameConst::$neutralNationType,
'color' => '#000000',
'name' => '재야',
];
$this->nation['aux'] = Json::decode($this->nation['aux']??'{}');
$this->leadership = $general->getLeadership();
$this->strength = $general->getStrength();
$this->intel = $general->getIntel();
$this->fullLeadership = $general->getLeadership(false);
$this->fullStrength = $general->getStrength(false);
$this->fullIntel = $general->getIntel(false);
$this->genType = $this::calcGenType($general);
$this->calcDiplomacyState();
$serverPolicy = KVStorage::getStorage($db, 'autorun_nation_policy_0');
$nationPolicy = KVStorage::getStorage($db, "autorun_nation_policy_{$this->nation['nation']}");
$this->nationPolicy = new AutorunNationPolicy($general, $nationPolicy->getAll(), $serverPolicy->getAll());
$this->generalPolicy = new AutorunGeneralPolicy($general, $this->env['autorun_user']['options']??[]);
}
public function getGeneralObj(): General
{
return $this->general;
}
protected static function calcGenType(General $general)
{
$leadership = $general->getLeadership();
$strength = Util::valueFit($general->getStrength(), 1);
$intel = Util::valueFit($general->getIntel(), 1);
//무장
if ($strength >= $intel) {
$genType = self::t무장;
if ($intel >= $strength * 0.8) { //무지장
if (Util::randBool($intel / $strength / 2)) {
$genType |= self::t지장;
}
}
//지장
} else {
$genType = self::t지장;
if ($strength >= $intel * 0.8 && Util::randBool(0.2)) { //지무장
if (Util::randBool($strength / $intel / 2)) {
$genType |= self::t무장;
}
}
}
//통솔
if ($leadership >= $this->nationPolicy->minNPCWarLeadership) {
$genType |= self::t통솔장;
}
return $genType;
}
protected function calcDiplomacyState()
{
$db = DB::db();
$nationID = $this->general->getNationID();
$env = $this->env;
if (Util::joinYearMonth($env['year'], $env['month']) <= Util::joinYearMonth($env['startyear'] + 2, 5)) {
$this->dipState = self::d평화;
$this->attackable = false;
return;
}
$frontStatus = $db->queryFirstField('SELECT max(front) FROM city WHERE nation=%i AND supply=1', $nationID);
// 공격가능도시 있으면
$this->attackable = ($frontStatus !== null) ? $frontStatus : false;
$warTarget = $db->queryAllLists(
'SELECT you, state FROM diplomacy WHERE me = %i AND (state = 0 OR (state = 1 AND term < 5))',
$nationID
);
$onWar = false;
$warTargetNation = [];
foreach ($warTarget as [$warNationID, $warState]) {
if ($warState == 0) {
$onWar = true;
$warTargetNation[$warNationID] = 2;
} else {
$warTargetNation[$warNationID] = 1;
}
}
$warTargetNation[0] = 0;
$this->warTargetNation = $warTargetNation;
$minWarTerm = $db->queryFirstField('SELECT min(term) FROM diplomacy WHERE me = %i AND state=1', $nationID);
if ($minWarTerm === null) {
$this->dipState = self::d평화;
} else if ($minWarTerm > 8) {
$this->dipState = self::d선포;
} else if ($minWarTerm > 5) {
$this->dipState = self::d징병;
} else {
$this->dipState = self::d직전;
}
if ($this->attackable) {
//전쟁으로 인한 attackable인가?
if ($onWar) {
$this->dipState = self::d전쟁;
} else {
$this->dipState = self::d징병;
}
}
}
protected function calcWarRoute(){
if($this->warRoute !== null){
return;
}
$target = $this->warTargetNation;
$target[] = $this->nation['nation'];
$this->warRoute = searchAllDistanceByNationList($target, false);
}
protected function do부대전방발령(LastTurn $lastTurn): ?NationCommand
{
$this->calcWarRoute();
$troopCandidate = [];
foreach($this->troopLeaders as $troopLeader){
$leaderID = $troopLeader->getID();
if(!key_exists($leaderID, $this->nationPolicy->CombatForce)){
continue;
}
$currentCityID = $troopLeader->getCityID();
if(key_exists($currentCityID, $this->frontCities)){
continue;
}
[$fromCityID, $toCityID] = $this->nationPolicy->CombatForce[$leaderID];
if(!key_exists($fromCityID, $this->warRoute) && !key_exists($toCityID, $this->warRoute)){
//공격 루트 상실, 전방 아무데나
$troopCandidate[] = [$leaderID, Util::choiceRandom($this->frontCities)['city']];
continue;
}
if(!key_exists($toCityID, $this->warRoute[$fromCityID])){
//공격 루트 상실, 전방 아무데나
$troopCandidate[] = [$leaderID, Util::choiceRandom($this->frontCities)['city']];
continue;
}
if(key_exists($fromCityID, $this->supplyCities) && key_exists($toCityID, $this->supplyCities)){
//점령 완료, 전방 아무데나
$troopCandidate[] = [$leaderID, Util::choiceRandom($this->frontCities)['city']];
continue;
}
//출발지가 아국땅이 아닌경우 수도->출발지
if(!key_exists($fromCityID, $this->supplyCities)){
$toCityID = $fromCityID;
$fromCityID = $this->nation['capital'];
}
$targetCityID = $fromCityID;
//접경에 도달할때까지 전진
while(!key_exists($targetCityID, $this->frontCities)){
$distance = $this->warRoute[$targetCityID][$toCityID];
$nextCityCandidate = [];
foreach(CityConst::byID($targetCityID)->path as $nearCityID){
if(!key_exists($nearCityID, $this->warRoute) || !key_exists($toCityID, $this->warRoute[$nearCityID])){
continue;
}
if($this->warRoute[$nearCityID][$toCityID] + 1 > $distance){
continue;
}
$nextCityCandidate[] = $nearCityID;
}
if(!$nextCityCandidate){
throw new MustNotBeReachedException('경로 계산 버그');
}
if(count($nextCityCandidate) == 1){
$targetCityID = $nextCityCandidate[0];
continue;
}
$targetCityID = Util::choiceRandom($nextCityCandidate);
}
$troopCandidate[] = ['destGenaralID'=>$leaderID, 'destCityID'=>$targetCityID];
}
if(!$troopCandidate){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, Util::choiceRandom($troopCandidate));
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do부대후방발령(LastTurn $lastTurn): ?NationCommand
{
$troopCandidate = [];
foreach($this->troopLeaders as $troopLeader){
$leaderID = $troopLeader->getID();
if(key_exists($leaderID, $this->nationPolicy->CombatForce)){
continue;
}
$currentCityID = $troopLeader->getCityID();
if(!key_exists($currentCityID, $this->supplyCities)){
$troopCandidate[$leaderID] = $troopLeader;
continue;
}
$city = $this->supplyCities[$currentCityID];
if($city['pop'] / $city['pop2'] >= $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$troopCandidate[$leaderID] = $troopLeader;
}
if(!$troopCandidate){
return null;
}
if(count($this->supplyCities) == 1){
return null;
}
$cityCandidates = [];
foreach($this->backupCities as $city){
if($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$cityCandidates[] = $city;
}
if(!$cityCandidates){
foreach($this->supplyCities as $city){
if($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$cityCandidates[] = $city;
}
}
if(!$cityCandidates){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
'destGeneralID'=>Util::choiceRandom($troopCandidate)->getID(),
'destCityID'=>Util::choiceRandom($cityCandidates)['city']
]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do부대유저장후방발령(LastTurn $lastTurn): ?NationCommand
{
if($this->dipState !== self::d전쟁){
return null;
}
$generalCadidates = [];
$db = DB::db();
$chiefTurnTime = new \DateTimeImmutable($this->general->getVar('turntime'));
foreach($this->userWarGenerals as $userGeneral){
$generalID = $userGeneral->getID();
if($generalID == $this->general->getID()){
continue;
}
$city = $this->supplyCities[$userGeneral->getCityID()];
if(!key_exists($generalID, $this->supplyCities)){
continue;
}
if($userGeneral->getVar('troop') === 0){
continue;
}
if($userGeneral->getVar('troop') === $userGeneral->getID()){
continue;
}
if($city['pop'] / $city['pop_max'] >= $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
if($userGeneral->getVar('crew') >= $this->nationPolicy->minWarCrew){
continue;
}
$generalTurnTime = new \DateTimeImmutable($userGeneral->getVar('turntime'));
$troopLeader = $this->nationGenerals[$userGeneral->getVar('troop')];
$troopTurnTime = new \DateTimeImmutable($troopLeader->getVar('turntime'));
if($chiefTurnTime < $generalTurnTime && $generalTurnTime < $troopTurnTime){
$generalCadidates[$generalID] = $userGeneral;
}
}
if(!$generalCadidates){
return null;
}
$rawTurn = Util::convertArrayToDict($db->query('SELECT * FROM general_turn WHERE general_id IN %li AND turn_idx = 0', array_keys($generalCadidates)), 'no');
$generalCadidates = array_filter($generalCadidates, function(General $general)use($rawTurn){
$generalID = $general->getID();
if(in_array($rawTurn[$generalID]??null, ['che_징병', 'che_모병'])){
return true;
}
else{
return false;
}
}, ARRAY_FILTER_USE_KEY);
if(count($this->supplyCities) == 1){
return null;
}
$cityCandidates = [];
foreach($this->backupCities as $city){
if($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$cityCandidates[] = $city;
}
if(!$cityCandidates){
foreach($this->supplyCities as $city){
if($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$cityCandidates[] = $city;
}
}
if(!$cityCandidates){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
'destGeneralID'=>Util::choiceRandom($generalCadidates)->getID(),
'destCityID'=>Util::choiceRandom($cityCandidates)['city']
]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do유저장후방발령(LastTurn $lastTurn): ?NationCommand
{
if($this->dipState !== self::d전쟁){
return null;
}
$generalCadidates = [];
foreach($this->userWarGenerals as $userGeneral){
$generalID = $userGeneral->getID();
if($generalID == $this->general->getID()){
continue;
}
$city = $this->supplyCities[$userGeneral->getCityID()];
if(!key_exists($generalID, $this->supplyCities)){
continue;
}
if($userGeneral->getVar('troop') !== 0){
continue;
}
if($city['pop'] / $city['pop_max'] >= $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
if($userGeneral->getVar('crew') >= $this->nationPolicy->minWarCrew){
continue;
}
$generalCadidates[$generalID] = $userGeneral;
}
if(!$generalCadidates){
return null;
}
if(count($this->supplyCities) == 1){
return null;
}
$cityCandidates = [];
if($this->backupCities){
$cities = $this->backupCities;
}
else{
$cities = $this->supplyCities;
}
foreach($cities as $city){
if($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio){
continue;
}
$cityCandidates[] = $city;
}
if(!$cityCandidates){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
'destGeneralID'=>Util::choiceRandom($generalCadidates)->getID(),
'destCityID'=>Util::choiceRandom($cityCandidates)['city']
]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do유저장구출발령(LastTurn $lastTurn): ?NationCommand
{
//고립 도시 장수 발령
$args = [];
foreach ($this->lostGenerals as $lostGeneral) {
if ($lostGeneral->getVar('npc') >= 2) {
continue;
}
if (in_array($this->dipState, [self::d직전, self::d전쟁]) && count($this->frontCities) > 2) {
$selCity = Util::choiceRandom($this->frontCities);
} else {
$selCity = Util::choiceRandom($this->supplyCities);
}
//고립된 장수가 많을 수록 발령 확률 증가
$args = [
'destGeneralID' => $lostGeneral->getID(),
'destCityID' => $selCity['city']
];
}
if(!$args){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, Util::choiceRandom($args));
if(!$cmd->isRunnable()){
return null;
}
return null;
}
protected function do유저장전방발령(LastTurn $lastTurn): ?NationCommand
{
return null;
}
protected function do유저장내정발령(LastTurn $lastTurn): ?NationCommand
{
$env = $this->env;
$args = [];
foreach($this->userCivilGenerals as $userGeneral){
}
return null;
}
protected function doNPC후방발령(LastTurn $lastTurn): ?NationCommand
{
return null;
}
protected function doNPC구출발령(LastTurn $lastTurn): ?NationCommand
{
//고립 도시 장수 발령
$args = [];
foreach ($this->lostGenerals as $lostGeneral) {
if ($lostGeneral->getVar('npc') < 2 || $lostGeneral->getVar('npc') == 5) {
continue;
}
$selCity = Util::choiceRandom($this->supplyCities);
//고립된 장수가 많을 수록 발령 확률 증가
$args = [
'destGeneralID' => $lostGeneral->getID(),
'destCityID' => $selCity['city']
];
}
if(!$args){
return null;
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, Util::choiceRandom($args));
if(!$cmd->isRunnable()){
return null;
}
return null;
}
protected function doNPC전방발령(LastTurn $lastTurn): ?NationCommand
{
return null;
}
protected function doNPC내정발령(LastTurn $lastTurn): ?NationCommand
{
return null;
}
protected function do유저장긴급포상(LastTurn $lastTurn): ?NationCommand
{
if(!$this->userGenerals){
return null;
}
$nation = $this->nation;
$candidateArgs = [];
$remainResource = [
'gold' => [
$nation['gold'],
$this->nationPolicy->reqHumanWarGold
],
'rice' => [
$nation['rice'],
$this->nationPolicy->reqHumanWarRice
]
];
$userWarGenerals = $this->userWarGenerals;
foreach($remainResource as $resName=>[$resVal,$reqHumanMinRes]){
usort($userWarGenerals, function ($lhs, $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
foreach($userWarGenerals as $idx=>$targetUserGeneral){
if($targetUserGeneral->getVar($resName) >= $reqHumanMinRes){
break;
}
$crewtype = $targetUserGeneral->getCrewTypeObj();
$reqMoney = $crewtype->costWithTech($this->nation['tech'], $targetUserGeneral->getLeadership()) * 2 * 4 * 1.1;
if ($this->env['year'] > $this->env['startyear'] + 5) {
$reqMoney = max($reqMoney, $reqHumanMinRes);
}
$enoughMoney = $reqMoney * 1.5;
if ($targetUserGeneral->getVar($resName) >= $reqMoney) {
continue;
}
//국고와 '충분한 금액'의 기하평균
$payAmount = sqrt(($enoughMoney - $targetUserGeneral->getVar($resName)) * $resVal);
if ($resVal < $payAmount / 2) {
continue;
}
$candidateArgs[] = [[
'destGeneralID' => $targetUserGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => Util::valueFit($payAmount, 100, GameConst::$maxResourceActionAmount)
],
count($userWarGenerals)-$idx
];
}
}
return buildNationCommandClass(
'che_포상', $this->general, $this->env, $lastTurn,
Util::choiceRandomUsingWeightPair($candidateArgs)
);
}
protected function do유저장포상(LastTurn $lastTurn): ?NationCommand
{
if(!$this->userGenerals){
return null;
}
$nation = $this->nation;
$candidateArgs = [];
$remainResource = [
'gold' => [
$nation['gold'],
$this->nationPolicy->reqHumanWarGold,
$this->nationPolicy->reqHumanDevelGold,
],
'rice' => [
$nation['rice'],
$this->nationPolicy->reqHumanWarRice,
$this->nationPolicy->reqHumanDevelRice
]
];
$userGenerals = $this->userGenerals;
foreach($remainResource as $resName=>[$resVal,$reqHumanMinWarRes,$reqHumanMinDevelRes]){
usort($userGenerals, function ($lhs, $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
foreach($userGenerals as $idx=>$targetUserGeneral){
if($targetUserGeneral->getVar($resName) >= $reqHumanMinWarRes){
break;
}
if(key_exists($targetUserGeneral->getID(), $this->userWarGenerals)){
$isWarGeneral = true;
}
else{
$isWarGeneral = false;
}
if($isWarGeneral){
$reqHumanMinRes = $reqHumanMinWarRes;
}
else{
$reqHumanMinRes = $reqHumanMinDevelRes;
}
$crewtype = $targetUserGeneral->getCrewTypeObj();
$reqMoney = $crewtype->costWithTech($this->nation['tech'], $targetUserGeneral->getLeadership()) * 2 * 4 * 1.1;
if ($this->env['year'] > $this->env['startyear'] + 5) {
$reqMoney = max($reqMoney, $reqHumanMinRes);
}
$enoughMoney = $reqMoney * 1.5;
if ($targetUserGeneral->getVar($resName) >= $reqMoney) {
continue;
}
//국고와 '충분한 금액'의 기하평균
$payAmount = sqrt(($enoughMoney - $targetUserGeneral->getVar($resName)) * $resVal);
if ($resVal < $payAmount / 2) {
continue;
}
$candidateArgs[] = [[
'destGeneralID' => $targetUserGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => Util::valueFit($payAmount, 100, GameConst::$maxResourceActionAmount)
],
count($userGenerals)-$idx
];
}
}
return buildNationCommandClass(
'che_포상', $this->general, $this->env, $lastTurn,
Util::choiceRandomUsingWeightPair($candidateArgs)
);
}
protected function doNPC긴급포상(LastTurn $lastTurn): ?NationCommand
{
if(!$this->npcWarGenerals){
return null;
}
$nation = $this->nation;
$candidateArgs = [];
$remainResource = [
'gold' => [
$nation['gold'],
$this->nationPolicy->reqNPCWarGold
],
'rice' => [
$nation['rice'],
$this->nationPolicy->reqNPCWarRice
]
];
$npcWarGenerals = $this->npcWarGenerals;
foreach($remainResource as $resName=>[$resVal,$reqNPCMinWarRes]){
usort($npcWarGenerals, function ($lhs, $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
foreach($npcWarGenerals as $idx=>$targetNPCGeneral){
if($targetNPCGeneral->getVar($resName) >= $reqNPCMinWarRes){
break;
}
$crewtype = $targetNPCGeneral->getCrewTypeObj();
$reqMoney = $crewtype->costWithTech($this->nation['tech'], $targetNPCGeneral->getLeadership()) * 2 * 4 * 1.1;
if ($this->env['year'] > $this->env['startyear'] + 5) {
$reqMoney = max($reqMoney, $reqNPCMinWarRes);
}
$enoughMoney = $reqMoney * 1.5;
if ($targetNPCGeneral->getVar($resName) >= $reqMoney) {
continue;
}
//국고와 '충분한 금액'의 기하평균
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
if ($resVal < $payAmount / 2) {
continue;
}
$candidateArgs[] = [[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => Util::valueFit($payAmount, 100, GameConst::$maxResourceActionAmount)
],
count($npcWarGenerals)-$idx
];
}
}
return buildNationCommandClass(
'che_포상', $this->general, $this->env, $lastTurn,
Util::choiceRandomUsingWeightPair($candidateArgs)
);
}
protected function doNPC포상(LastTurn $lastTurn): ?NationCommand
{
if(!$this->npcWarGenerals && !$this->npcCivilGenerals){
return null;
}
$nation = $this->nation;
$candidateArgs = [];
$remainResource = [
'gold' => [
$nation['gold'],
$this->nationPolicy->reqNPCWarGold,
$this->nationPolicy->reqNPCDevelGold
],
'rice' => [
$nation['rice'],
$this->nationPolicy->reqNPCWarRice,
$this->nationPolicy->reqNPCDevelRice
]
];
$npcWarGenerals = $this->npcWarGenerals;
$npcCivilGenerals = $this->npcCivilGenerals;
foreach($remainResource as $resName=>[$resVal,$reqNPCMinWarRes,$reqNPCMinDevelRes]){
usort($npcWarGenerals, function ($lhs, $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
foreach($npcWarGenerals as $idx=>$targetNPCGeneral){
if($targetNPCGeneral->getVar($resName) >= $reqNPCMinWarRes){
break;
}
$crewtype = $targetNPCGeneral->getCrewTypeObj();
$reqMoney = $crewtype->costWithTech($nation['tech'], $targetNPCGeneral->getLeadership()) * 2 * 4 * 1.1;
if ($this->env['year'] > $this->env['startyear'] + 5) {
$reqMoney = max($reqMoney, $reqNPCMinWarRes);
}
$enoughMoney = $reqMoney * 1.5;
if ($targetNPCGeneral->getVar($resName) >= $reqMoney) {
continue;
}
//국고와 '충분한 금액'의 기하평균
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
if ($resVal < $payAmount / 2) {
continue;
}
$candidateArgs[] = [[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => Util::valueFit($payAmount, 100, GameConst::$maxResourceActionAmount)
],
max(count($npcWarGenerals), count($npcCivilGenerals))-$idx
];
}
usort($npcCivilGenerals, function ($lhs, $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
foreach($npcCivilGenerals as $idx=>$targetNPCGeneral){
if($targetNPCGeneral->getVar($resName) >= $reqNPCMinDevelRes){
break;
}
$enoughMoney = $reqNPCMinDevelRes * 1.5;
if ($targetNPCGeneral->getVar($resName) >= $reqMoney) {
continue;
}
$payAmount = $enoughMoney - $targetNPCGeneral->getVar($resName);
$payAmount = Util::valueFit($payAmount, 100, GameConst::$maxResourceActionAmount);
$candidateArgs[] = [[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => $payAmount
],
max(count($npcWarGenerals), count($npcCivilGenerals)) - $idx
];
}
}
return buildNationCommandClass(
'che_포상', $this->general, $this->env, $lastTurn,
Util::choiceRandomUsingWeightPair($candidateArgs)
);
}
protected function doNPC몰수(LastTurn $lastTurn): ?NationCommand
{
if(!$this->npcWarGenerals && !$this->npcCivilGenerals){
return null;
}
$nation = $this->nation;
$candidateArgs = [];
$remainResource = [
'gold' => [
$nation['gold'],
$this->nationPolicy->reqNationGold,
$this->nationPolicy->reqNPCWarGold,
$this->nationPolicy->reqNPCDevelGold,
],
'rice' => [
$nation['rice'],
$this->nationPolicy->reqNationRice,
$this->nationPolicy->reqNPCWarRice,
$this->nationPolicy->reqNPCDevelRice,
]
];
$npcWarGenerals = $this->npcWarGenerals;
$npcCivilGenerals = $this->npcCivilGenerals;
foreach($remainResource as $resName=>[$resVal,$reqNationResVal,$reqNPCMinWarRes,$reqNPCMinDevelRes]){
usort($npcCivilGenerals, function ($lhs, $rhs) use ($resName) {
return -($lhs->getVar($resName) <=> $rhs->getVar($resName));
});
foreach($npcCivilGenerals as $idx=>$targetNPCGeneral){
if($targetNPCGeneral->getVar($resName) <= $reqNPCMinDevelRes + 100){
break;
}
$takeAmount = $targetNPCGeneral->getVar($resName) - $reqNPCMinDevelRes;
$takeAmount = Util::valueFit($takeAmount, 100, GameConst::$maxResourceActionAmount);
$candidateArgs[] = [[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => $takeAmount
],
$takeAmount
];
}
//전투 NPC는 국고가 충분하지 않아보일때
$reqResValDelta = $reqNationResVal * 1.5 - $resVal;
if($reqResValDelta < 0){
continue;
}
if($resVal >= $reqNationResVal){
$willTakeSmallAmount = true;
}
else{
$willTakeSmallAmount = false;
}
usort($npcWarGenerals, function ($lhs, $rhs) use ($resName) {
return -($lhs->getVar($resName) <=> $rhs->getVar($resName));
});
foreach($npcWarGenerals as $idx=>$targetNPCGeneral){
if($willTakeSmallAmount){
if($targetNPCGeneral->getVar($resName) <= $reqNPCMinWarRes * 2){
break;
}
}
else if($targetNPCGeneral->getVar($resName) <= $reqNPCMinWarRes){
break;
}
if(!$willTakeSmallAmount){
$takeAmount = $targetNPCGeneral->getVar($resName) - $reqNPCMinWarRes;
$takeAmount = Util::valueFit(sqrt($takeAmount * $reqResValDelta), 0, $takeAmount);
}
else{
$maxTakeAmount = $targetNPCGeneral->getVar($resName) - $reqNPCMinWarRes;
$minTakeAmount = $targetNPCGeneral->getVar($resName) - $reqNPCMinWarRes * 2;
$takeAmount = Util::valueFit(sqrt($minTakeAmount * $reqResValDelta), 0, $maxTakeAmount);
}
if($takeAmount < 100){
break;
}
$takeAmount = Util::valueFit($takeAmount, 100, GameConst::$maxResourceActionAmount);
$candidateArgs[] = [[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => $takeAmount
],
$takeAmount
];
}
}
return buildNationCommandClass(
'che_몰수', $this->general, $this->env, $lastTurn,
Util::choiceRandomUsingWeightPair($candidateArgs)
);
}
// 군주 행동
protected function do선포(LastTurn $lastTurn): ?NationCommand
{
$general = $this->general;
if($general->getVar('level') < 12){
return null;
}
if($this->dipState !== self::d평화){
return null;
}
if($this->attackable){
return null;
}
$targetNationID = $this->findWarTarget();
if($targetNationID === null){
return null;
}
$cmd = buildNationCommandClass('che_선전포고', $general, $this->env, $lastTurn, [
'destNationID' => $targetNationID
]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do천도(LastTurn $lastTurn): ?NationCommand
{
$general = $this->general;
$db = DB::db();
$nationStor = KVStorage::getStorage($db, 'nation_env');
$turnTerm = $this->env['turnterm'];
//천도를 한턴 넣었다면 계속 넣는다.
if($lastTurn->getCommand() === 'che_천도'){
$cmd = buildNationCommandClass('che_천도', $general, $this->env, $lastTurn, $lastTurn->getArg());
if($cmd->isRunnable()){
$nationStor->last천도Trial = [$general->getVar('level'), $general->getVar('turntime')];
return $cmd;
}
}
$lastTrial = $nationStor->last천도Trial;
if($lastTrial){
[$lastTrialLevel, $lastTrialTurnTime] = $lastTrial;
$timeDiffSeconds = TimeUtil::DateIntervalToSeconds(
date_create_immutable($lastTrialTurnTime)->diff(
date_create_immutable($general->getVar('turntime'))
)
);
if($timeDiffSeconds < $turnTerm * 30 && $lastTrialLevel !== $general->getVar('level')){ //0.5Turn
return null;
}
}
/*
도시 점수 공식
sqrt(인구) * sqrt(모든 아국 도시까지의 거리의 합) * sqrt(내정률)
가장 높은 도시로 이동하되, 상위 25% 내에 들었다면 정지
*/
//checkSupply()와 비슷하면서 다름
$nationCityIDList = [];
$this->categorizeNationCities();
foreach($this->nationCities as $city){
$nationCityIDList[$city['city']] = true;
}
//애초에 도시랄 것이 없음
if(count($nationCityIDList) <= 1){
return null;
}
$queue = new \SplQueue();
$capital = $this->nation['capital'];
$cityList = [
$capital=>0
];
$queue->enqueue($capital);
//수도와 연결된 도시 탐색
while(!$queue->isEmpty()){
$cityID = $queue->dequeue();
foreach(CityConst::byID($cityID)->path as $nextCityID){
if(!key_exists($cityID, $nationCityIDList)){
continue;
}
if(key_exists($cityID, $cityList)){
continue;
}
$cityList[$cityID] = 0;
$queue->enqueue($nextCityID);
}
}
$cityList = array_keys($cityList);
//수도와 연결된 도시가 없음
if(count($cityList) == 1){
return null;
}
$distanceList = searchAllDistanceByCityList($cityList);
$maxDistance = 0;
foreach($distanceList as $cityID=>$subDistanceList){
$maxDistance = max($maxDistance, array_sum($subDistanceList));
}
$cityScoreList = [];
foreach($cityList as $cityID){
$city = $this->nationCities[$cityID];
$cityScoreList[$cityID] = sqrt($city['pop']) * $maxDistance / array_sum($distanceList[$cityID]) * sqrt($city['dev']);
}
arsort($cityScoreList);
$enoughLimit = ceil(count($cityScoreList) * 0.25);
foreach(array_keys($cityScoreList) as $idx=>$cityID){
if($idx > $enoughLimit){
break;
}
if($idx === $capital){
return null;
}
}
$finalCityID = Util::array_first_key($cityScoreList);
$dist = $distanceList[$capital][$finalCityID];
$targetCityID = $finalCityID;
if($dist > 1){
$candidates = [];
foreach(CityConst::byID($capital)->path as $stopID){
if(!key_exists($stopID, $distanceList)){
continue;
}
if($distanceList[$stopID][$finalCityID] + 1 === $dist){
$candidates[] = $stopID;
}
}
$targetCityID = Util::choiceRandom($candidates);
}
$cmd = buildNationCommandClass('che_천도', $general, $this->env, $lastTurn, [
'destCityID'=>$targetCityID
]);
if(!$cmd->isRunnable()){
return null;
}
$nationStor->last천도Trial = [$general->getVar('level'), $general->getVar('turntime')];
return $cmd;
}
//일반장 행동
protected function do일반내정(GeneralCommand $reservedCommand): ?GeneralCommand
{
$leadership = $this->leadership;
$strength = $this->strength;
$intel = $this->intel;
$general = $this->general;
$env = $this->env;
$genType = $this->genType;
$city = $this->city;
$nation = $this->nation;
$develRate = [
'trust' => Util::valueFit($city['trust'], 0.001),
'pop' => Util::valueFit($city['pop'] / $city['pop_max'], 0.001),
'agri' => Util::valueFit($city['agri'] / $city['agri_max'], 0.001),
'comm' => Util::valueFit($city['comm'] / $city['comm_max'], 0.001),
'secu' => Util::valueFit($city['secu'] / $city['secu_max'], 0.001),
'def' => Util::valueFit($city['def'] / $city['def_max'], 0.001),
'wall' => Util::valueFit($city['wall'] / $city['wall_max'], 0.001),
];
$cmdList = [];
if ($genType & self::t통솔장) {
if ($develRate['trust'] < 0.95) {
$cmd = buildGeneralCommandClass('che_주민선정', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $leadership / $develRate['trust'] * 2];
}
}
if ($develRate['pop'] < 0.8) {
$cmd = buildGeneralCommandClass('che_정착장려', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $leadership / $develRate['pop']];
}
}
}
if($genType & self::t무장){
if($develRate['def'] < 1){
$cmd = buildGeneralCommandClass('che_수비강화', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / $develRate['def']];
}
}
if($develRate['wall'] < 1){
$cmd = buildGeneralCommandClass('che_성벽보수', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / $develRate['wall']];
}
}
if($develRate['comm'] < 0.9){
$cmd = buildGeneralCommandClass('che_치안강화', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / Util::valueFit($develRate['comm'] / 0.8, null, 1)];
}
}
}
if($genType & self::t지장){
if (!TechLimit($env['startyear'], $env['year'], $nation['tech'])) {
$cmd = buildGeneralCommandClass('che_기술연구', $general, $env);
if ($cmd->isRunnable()) {
if (!TechLimit($env['startyear'], $env['year'], $nation['tech'] + 1000)) {
//한등급 이상 뒤쳐져 있다면, 조금 더 열심히 하자.
$cmdList[] = [$cmd, $intel * 2];
} else {
$cmdList[] = [$cmd, $intel];
}
}
}
if ($develRate['agri'] < 1) {
$cmd = buildGeneralCommandClass('che_농지개간', $general, $env);
if ($cmd->isRunnable()) {
$cmdList[] = [$cmd, $intel];
}
}
if ($develRate['comm'] < 1) {
$cmd = buildGeneralCommandClass('che_상업투자', $general, $env);
if ($cmd->isRunnable()) {
$cmdList[] = [$cmd, $intel];
}
}
}
return Util::choiceRandomUsingWeightPair($cmdList);
}
protected function do긴급내정(GeneralCommand $reservedCommand): ?GeneralCommand
{
if($this->dipState === self::d평화){
return null;
}
$leadership = $this->leadership;
$strength = $this->strength;
$intel = $this->intel;
$general = $this->general;
$env = $this->env;
$genType = $this->genType;
$city = $this->city;
if($city['trust'] < 0.3 && Util::randBool($leadership / GameConst::$chiefStatMin)){
$cmd = buildGeneralCommandClass('che_주민선정', $general, $env);
if($cmd->isRunnable()){
return $cmd;
}
}
if($city['pop'] < $this->nationPolicy->safeRecruitCityPopulation && Util::randBool($leadership / GameConst::$chiefStatMin / 2)){
$cmd = buildGeneralCommandClass('che_정착장려', $general, $env);
if($cmd->isRunnable()){
return $cmd;
}
}
}
protected function do전쟁내정(GeneralCommand $reservedCommand): ?GeneralCommand
{
if($this->dipState === self::d평화){
return null;
}
$leadership = $this->leadership;
$strength = $this->strength;
$intel = $this->intel;
$general = $this->general;
$env = $this->env;
$genType = $this->genType;
$city = $this->city;
$nation = $this->nation;
$develRate = [
'trust' => Util::valueFit($city['trust'], 0.001),
'pop' => Util::valueFit($city['pop'] / $city['pop_max'], 0.001),
'agri' => Util::valueFit($city['agri'] / $city['agri_max'], 0.001),
'comm' => Util::valueFit($city['comm'] / $city['comm_max'], 0.001),
'secu' => Util::valueFit($city['secu'] / $city['secu_max'], 0.001),
'def' => Util::valueFit($city['def'] / $city['def_max'], 0.001),
'wall' => Util::valueFit($city['wall'] / $city['wall_max'], 0.001),
];
$cmdList = [];
if ($genType & self::t통솔장) {
if ($develRate['trust'] < 0.9) {
$cmd = buildGeneralCommandClass('che_주민선정', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $leadership / $develRate['trust'] / 2];
}
}
if ($develRate['pop'] < 0.8) {
$cmd = buildGeneralCommandClass('che_정착장려', $general, $env);
if($cmd->isRunnable()){
if (in_array($city['front'], [1, 3])) {
$cmdList[] = [$cmd, $leadership / $develRate['pop']];
}
else{
$cmdList[] = [$cmd, $leadership / $develRate['pop'] / 2];
}
}
}
}
if($genType & self::t무장){
if($develRate['def'] < 0.5){
$cmd = buildGeneralCommandClass('che_수비강화', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / $develRate['def'] / 2];
}
}
if($develRate['wall'] < 0.5){
$cmd = buildGeneralCommandClass('che_성벽보수', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / $develRate['wall'] / 2];
}
}
if($develRate['comm'] < 0.5){
$cmd = buildGeneralCommandClass('che_치안강화', $general, $env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, $strength / Util::valueFit($develRate['comm'] / 0.8, null, 1) / 4];
}
}
}
if($genType & self::t지장){
if (!TechLimit($env['startyear'], $env['year'], $nation['tech'])) {
$cmd = buildGeneralCommandClass('che_기술연구', $general, $env);
if ($cmd->isRunnable()) {
if (!TechLimit($env['startyear'], $env['year'], $nation['tech'] + 1000)) {
//한등급 이상 뒤쳐져 있다면, 조금 더 열심히 하자.
$cmdList[] = [$cmd, $intel * 2];
} else {
$cmdList[] = [$cmd, $intel];
}
}
}
if ($develRate['agri'] < 0.5) {
$cmd = buildGeneralCommandClass('che_농지개간', $general, $env);
if ($cmd->isRunnable()) {
if (in_array($city['front'], [1, 3])) {
$cmdList[] = [$cmd, $intel / 4];
}
else{
$cmdList[] = [$cmd, $intel / 2];
}
}
}
if ($develRate['comm'] < 0.5) {
$cmd = buildGeneralCommandClass('che_상업투자', $general, $env);
if ($cmd->isRunnable()) {
if (in_array($city['front'], [1, 3])) {
$cmdList[] = [$cmd, $intel / 4];
}
else{
$cmdList[] = [$cmd, $intel / 2];
}
}
}
}
return Util::choiceRandomUsingWeightPair($cmdList);
}
protected function do금쌀구매(GeneralCommand $reservedCommand): ?GeneralCommand
{
$general = $this->general;
$avgAmount = ($general->getVar('gold') + $general->getVar('rice'))/2;
if($this->city['trade'] === null && !$this->generalPolicy->can상인무시){
return null;
}
if($avgAmount < $this->baseDevelCost){
return null;
}
if($this->dipState !== self::d평화 && ($this->genType & self::t통솔장)){
$crewType = $general->getCrewTypeObj();
if($this->generalPolicy->can모병){
$costCmd = buildGeneralCommandClass('che_모병', $general, $this->env, [
'crewType'=>$crewType->id,
'amount'=>$this->fullLeadership*100
]);
}
else{
$costCmd = buildGeneralCommandClass('che_징병', $general, $this->env, [
'crewType'=>$crewType->id,
'amount'=>$this->fullLeadership*100
]);
}
$goldCost = $costCmd->getCost()[0];
$riceCost = $crewType->riceWithTech(
$this->nation['tech'],
$this->fullLeadership*100 *
$general->getRankVar('killcrew')/max($general->getRankVar('deathcrew'),1)
);
if($avgAmount * 2 > $goldCost + $riceCost){
if ($general->getVar('rice') < $riceCost * 2 && $general->getVar('gold') >= $goldCost * 4) {
//1:1
return buildGeneralCommandClass('che_군량매매', $general, $this->env,
[
'buyRice' => true,
'amount' => Util::valueFit(Util::toInt($general->getVar('gold') - $avgAmount), 100, GameConst::$maxResourceActionAmount)
]
);
}
if ($general->getVar('gold') < $goldCost && $general->getVar('rice') >= $riceCost * 2) {
$avgAmount = ($general->getVar('gold') + $general->getVar('rice'))/2;
return buildGeneralCommandClass('che_군량매매', $general, $this->env,
[
'buyRice' => false,
'amount' => Util::valueFit(Util::toInt($general->getVar('rice') - $avgAmount), 100, GameConst::$maxResourceActionAmount)
]
);
}
}
}
if ($general->getVar('rice') < $this->baseDevelCost && $general->getVar('gold') >= $this->baseDevelCost * 3) {
return buildGeneralCommandClass('che_군량매매', $general, $this->env,
[
'buyRice' => true,
'amount' => Util::valueFit(Util::toInt($general->getVar('gold') - $avgAmount), 100, GameConst::$maxResourceActionAmount)
]
);
}
if ($general->getVar('gold') < $this->baseDevelCost && $general->getVar('rice') >= $this->baseDevelCost * 3) {
$avgAmount = ($general->getVar('gold') + $general->getVar('rice'))/2;
return buildGeneralCommandClass('che_군량매매', $general, $this->env,
[
'buyRice' => false,
'amount' => Util::valueFit(Util::toInt($general->getVar('rice') - $avgAmount), 100, GameConst::$maxResourceActionAmount)
]
);
}
return null;
}
protected function do징병(GeneralCommand $reservedCommand): ?GeneralCommand
{
$general = $this->getGeneralObj();
$nation = $this->nation;
$env = $this->env;
$nationID = $general->getNationID();
$genType = $this->genType;
$tech = $nation['tech'];
$db = DB::db();
$dex = [
GameUnitConst::T_FOOTMAN => sqrt($general->getVar('dex1') + 500),
GameUnitConst::T_ARCHER => sqrt($general->getVar('dex2') + 500),
GameUnitConst::T_CAVALRY => sqrt($general->getVar('dex3') + 500),
GameUnitConst::T_WIZARD => sqrt($general->getVar('dex4') + 500),
GameUnitConst::T_SIEGE => sqrt($general->getVar('dex5') + 500),
];
$cities = [];
$regions = [];
foreach ($db->queryAllLists('SELECT city, region FROM city WHERE nation = %i', $nationID) as [$cityID, $regionID]) {
$cities[$cityID] = true;
$regions[$regionID] = true;
}
$relYear = Util::valueFit($env['year'] - $env['startyear'], 0);
$typesAll = [];
if ($genType & self::t무장) {
$types = [];
foreach (GameUnitConst::byType(GameUnitConst::T_FOOTMAN) as $crewtype) {
if ($crewtype->isValid($cities, $regions, $relYear, $tech)) {
$score = $dex[GameUnitConst::T_FOOTMAN] * $crewtype->pickScore($tech);
$types[] = [$crewtype->id, $score];
}
}
foreach (GameUnitConst::byType(GameUnitConst::T_ARCHER) as $crewtype) {
if ($crewtype->isValid($cities, $regions, $relYear, $tech)) {
$score = $dex[GameUnitConst::T_ARCHER] * $crewtype->pickScore($tech);
$types[] = [$crewtype->id, $score];
}
}
foreach (GameUnitConst::byType(GameUnitConst::T_CAVALRY) as $crewtype) {
if ($crewtype->isValid($cities, $regions, $relYear, $tech)) {
$score = $dex[GameUnitConst::T_CAVALRY] * $crewtype->pickScore($tech);
$types[] = [$crewtype->id, $score];
}
}
foreach ($types as [$crewtype, $score]) {
$typesAll[$crewtype] = [$crewtype, $score / count($types)];
}
}
if ($genType & self::t지장) {
$types = [];
foreach (GameUnitConst::byType(GameUnitConst::T_WIZARD) as $crewtype) {
if ($crewtype->isValid($cities, $regions, $relYear, $tech)) {
$score = $dex[GameUnitConst::T_WIZARD] * $crewtype->pickScore($tech);
$types[] = [$crewtype->id, $score];
}
}
foreach ($types as [$crewtype, $score]) {
$typesAll[$crewtype] = [$crewtype, $score / count($types)];
}
}
if ($typesAll) {
$type = Util::choiceRandomUsingWeightPair($types);
} else {
$type = GameUnitConst::DEFAULT_CREWTYPE;
}
//NOTE: 훈련과 사기진작은 '금만 사용한다'는 가정을 하고 있음
$obj훈련 = buildGeneralCommandClass('che_훈련', $general, $env);
$obj사기진작 = buildGeneralCommandClass('che_사기진작', $general, $env);
$gold = $general->getVar('gold');
$gold -= $obj훈련->getCost()[0] * 2;
$gold -= $obj사기진작->getCost()[0] * 2;
$cost = $general->getCrewTypeObj()->costWithTech($tech);
$cost = $general->onCalcDomestic('징병', 'cost', $cost);
$crew = intdiv($gold, $cost);
$crew *= 100;
return buildGeneralCommandClass('che_징병', $general, $env, [
'crewType' => $type,
'amount' => $crew
]);
}
protected function do전투준비(GeneralCommand $reservedCommand): ?GeneralCommand
{
if (!$this->attackable){
return null;
}
if (in_array($this->dipState, [self::d평화, self::d선포])) {
return null;
}
$cmdList = [];
$general = $this->general;
$train = $general->getVar('train');
$atmos = $general->getVar('atmos');
if($train < $this->nationPolicy->properWarTrainAtmos){
$cmd = buildGeneralCommandClass('che_훈련', $general, $this->env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, GameConst::$maxTrainByCommand / Util::valueFit($train, 1)];
}
}
if($train < $this->nationPolicy->properWarTrainAtmos){
$cmd = buildGeneralCommandClass('che_사기진작', $general, $this->env);
if($cmd->isRunnable()){
$cmdList[] = [$cmd, GameConst::$maxAtmosByCommand / Util::valueFit($atmos, 1)];
}
}
if(!$cmd){
return null;
}
return Util::choiceRandomUsingWeightPair($cmdList);
}
public function do소집해제(GeneralCommand $reservedCommand): ?GeneralCommand
{
if ($this->attackable){
return null;
}
if ($this->dipState !== self::d평화) {
return null;
}
if ($this->general->getVar('crew') == 0){
return null;
}
if (Util::randBool(0.75)) {
return null;
}
$cmd = buildGeneralCommandClass('che_소집해제', $this->general, $this->env);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
protected function do출병(GeneralCommand $reservedCommand): ?GeneralCommand
{
$general = $this->getGeneralObj();
$city = $this->city;
$nation = $this->nation;
$cityID = $city['city'];
$nationID = $nation['nation'];
$db = DB::db();
if ($city['front'] <= 1) {
return null;
}
$attackableNations = [];
foreach ($this->warTargetNation as $targetNationID => $state) {
if ($state == 1) {
continue;
}
$attackableNations[] = $targetNationID;
}
$nearCities = array_keys(CityConst::byID($cityID)->path);
$attackableCities = $db->queryFirstColumn(
'SELECT city FROM city WHERE nation IN %li AND city IN %li',
$attackableNations,
$nearCities
);
if (count($attackableCities) == 0) {
throw new \RuntimeException('출병 불가' . $cityID . var_export($attackableNations, true) . var_export($nearCities, true));
}
return buildGeneralCommandClass('che_출병', $general, $this->env, ['destCityID' => Util::choiceRandom($attackableCities)]);
}
/*
protected function doNPC증여(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
*/
protected function doNPC헌납(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do후방워프(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do전방워프(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do내정워프(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do귀환(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do집합(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do방랑군이동(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do거병(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do건국(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do선양(GeneralCommand $reservedCommand): ?GeneralCommand
{
return null;
}
protected function do국가선택(GeneralCommand $reservedCommand): ?GeneralCommand
{
$general = $this->getGeneralObj();
$city = $this->city;
$env = $this->env;
$db = DB::db();
$arg = null;
// 오랑캐는 바로 임관
if ($general->getVar('npc') == 9) {
$rulerNation = $db->queryFirstField(
'SELECT nation FROM general WHERE `level`=12 AND npc=9 and nation not in %li ORDER BY RAND() limit 1',
$general->getAuxVar('joinedNations') ?? [0]
);
if ($rulerNation) {
$cmd = buildGeneralCommandClass('che_임관', $general, $env, ['destNationID' => $rulerNation]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
}
}
switch (Util::choiceRandomUsingWeight([
'임관' => 11.4,
'거병_견문' => 40,
'이동' => 20,
'기타' => 28.6
])) {
//임관
case '임관':
$available = true;
if ($env['startyear'] + 3 > $env['year']) {
//초기 임관 기간에서는 국가가 적을수록 임관 시도가 적음
$nationCnt = $db->queryFirstField('SELECT count(nation) FROM nation');
$notFullNationCnt = $db->queryFirstField('SELECT count(nation) FROM nation WHERE gennum < %i', GameConst::$initialNationGenLimit);
if ($nationCnt == 0 || $notFullNationCnt == 0) {
$available = false;
} else if (Util::randBool(pow(1 / $nationCnt / pow($notFullNationCnt, 3), 1 / 4))) {
//국가가 1개일 경우에는 '임관하지 않음'
$available = false;
}
}
if ($general->getVar('affinity') == 999 || !$available) {
return null;
}
//랜임 커맨드 입력.
$cmd = buildGeneralCommandClass('che_랜덤임관', $general, $env, ['destNationID' => $rulerNation]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
break;
case '거병_견문': //거병이나 견문
// 초반이면서 능력이 좋은놈 위주로 1.4%확률로 거병
if($general->getVar('makelimit')){
return null;
}
if(!$this->generalPolicy->can건국){
return null;
}
$prop = Util::randF() * (GameConst::$defaultStatNPCMax + GameConst::$chiefStatMin) / 2;
$ratio = ($general->getVar('leadership') + $general->getVar('strength') + $general->getVar('intel')) / 3;
if($prop >= $ratio){
return null;
}
if(Util::randBool(1 - 0.014)){
return null;
}
$cmd = buildGeneralCommandClass('che_거병', $general, $env, null);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
break;
case '이동': //이동
$paths = array_keys(CityConst::byID($city['city'])->path);
$cmd = buildGeneralCommandClass('che_이동', $general, $env, ['destCityID' => Util::choiceRandom($paths)]);
if(!$cmd->isRunnable()){
return null;
}
return $cmd;
break;
}
return null;
}
protected function NPC사망대비(): ?GeneralCommand
{
$general = $this->getGeneralObj();
if($general->getVar('killturn') > 5){
return null;
}
if($general->getNationID() == 0){
return buildGeneralCommandClass('che_견문', $general, $this->env);
}
if ($general->getVar('gold') + $general->getVar('rice') == 0) {
return buildGeneralCommandClass('che_물자조달', $general, $this->env);
}
if ($general->getVar('gold') >= $general->getVar('rice')) {
return buildGeneralCommandClass('che_헌납', $general, $this->env, [
'isGold' => true,
'amount' => GameConst::$maxResourceActionAmount
]);
} else {
return buildGeneralCommandClass('che_헌납', $general, $this->env, [
'isGold' => false,
'amount' => GameConst::$maxResourceActionAmount
]);
}
return null;
}
protected function do중립(GeneralCommand $reservedCommand): GeneralCommand
{
$general = $this->general;
if($general->getNationID() == 0){
return buildGeneralCommandClass('che_견문', $this->general, $this->env);
}
$candidate = [];
$nation = $this->nation;
if($nation['gold'] < $this->nationPolicy->reqNationGold){
$candidate[] = 'che_물자조달';
}
if($nation['rice'] < $this->nationPolicy->reqNationGold){
$candidate[] = 'che_물자조달';
}
$candidate[] = 'che_인재탐색';
return buildGeneralCommandClass(Util::choiceRandom($candidate), $this->general, $this->env);
}
protected function categorizeNationCities():void{
if($this->nationCities !== null){
return;
}
$nation = $this->nation;
$nationID = $nation['nation'];
$db = DB::db();
$nationCities = [];
$frontCities = [];
$supplyCities = [];
$backupCities = [];
foreach ($db->query('SELECT * FROM city WHERE nation = %i', $nationID) as $nationCity) {
$nationCity['generals'] = [];
$cityID = $nationCity['city'];
$dev =
($nationCity['agri'] + $nationCity['comm'] + $nationCity['secu'] + $nationCity['def'] + $nationCity['wall']) /
($nationCity['agri_max'] + $nationCity['comm_max'] + $nationCity['secu_max'] + $nationCity['def_max'] + $nationCity['wall_max']);
$nationCity['dev'] = $dev;
$importantScore = 1;
if ($nationCity['officer4']) {
$importantScore += 1;
}
if ($nationCity['officer3']) {
$importantScore += 1;
}
if ($nationCity['officer2']) {
$importantScore += 1;
}
$nationCity['important'] = $importantScore;
if($nationCity['supply']){
$supplyCities[$cityID] = &$nationCity;
}
if($nationCity['front']){
$frontCities[$cityID] = &$nationCity;
}
else{
$backupCities[$cityID] = &$nationCity;
}
$nationCities[$cityID] = &$nationCity;
}
$this->nationCities = $nationCities;
$this->frontCities = $frontCities;
$this->supplyCities = $supplyCities;
$this->backupCities = $backupCities;
}
protected function categorizeNationGeneral():void{
if($this->userGenerals !== null){
return;
}
$userGenerals = [];
$userCivilGenerals = [];
$userWarGenerals = [];
$lostGenerals = [];
$npcCivilGenerals = [];
$npcWarGenerals = [];
$troopLeaders = [];
$nationID = $this->nation['nation'];
$this->categorizeNationCities();
$nationCities = &$this->nationCities;
$db = DB::db();
$generalIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation = %i AND no != %i', $nationID, $this->general->getID());
foreach(General::createGeneralObjListFromDB($generalIDList, null, 2) as $nationGeneral){
$generalID = $nationGeneral->getID();
$cityID = $nationGeneral->getCityID();
$npcType = $nationGeneral->getVar('npc');
if (key_exists($cityID, $nationCities)) {
$nationCities[$cityID]['generals'][$generalID] = $nationGeneral;
if (!$nationCities[$cityID]['supply']) {
$lostGenerals[$generalID] = $nationGeneral;
}
} else {
$lostGenerals[$generalID] = $nationGeneral;
}
if($npcType == 5){
//부대장임
$troopLeaders[$generalID] = $nationGeneral;
}
else if($nationGeneral->getVar('killturn') < 5){
//삭턴이 몇 안남은 장수는 '내정장 npc'로 처리
$npcCivilGenerals[$generalID] = $nationGeneral;
}
else if($npcType < 2) {
$userGenerals[$generalID] = $nationGeneral;
if($nationGeneral->calcRecentWarTurn($this->env['turnterm']) <= 12){
$userWarGenerals[$generalID] = $nationGeneral;
}
else{
$userCivilGenerals[$generalID] = $nationGeneral;
}
} else if ($nationGeneral->getLeadership() >= $this->nationPolicy->minNPCWarLeadership) {
$npcWarGenerals[$generalID] = $nationGeneral;
} else {
$npcCivilGenerals[$generalID] = $nationGeneral;
}
}
$this->userGenerals = $userGenerals;
$this->userCivilGenerals = $userCivilGenerals;
$this->userWarGenerals = $userWarGenerals;
$this->lostGenerals = $lostGenerals;
$this->npcCivilGenerals = $npcCivilGenerals;
$this->npcWarGenerals = $npcWarGenerals;
$this->troopLeaders = $troopLeaders;
}
public function chooseNationTurn(NationCommand $reservedCommand): NationCommand{
//TODO: NationTurn과 InstantNationTurn 구분 필요
$lastTurn = $reservedCommand->getLastTurn();
$general = $this->general;
if($general->getVar('level') == 12){
$month = $this->env['month'];
if (in_array($month, [1, 4, 7, 10])) {
$this->calcPromotion();
} else if ($month == 12) {
$this->calcTexRate();
$this->calcGoldBillRate();
} else if ($month == 6) {
$this->calcTexRate();
$this->calcRiceBillRate();
}
}
if($reservedCommand->isRunnable()){
return $reservedCommand;
}
$this->categorizeNationGeneral();
foreach($this->nationPolicy->priority as $actionName){
if(!$this->nationPolicy->{'can'.$actionName}){
continue;
}
/** @var ?NationCommand */
$result = $this->{'do'.$actionName}($lastTurn);
if($result !== null){
return $result;
}
}
return buildNationCommandClass(null, $this->general, $this->env, $this->general->getLastTurn());
}
public function chooseInstantNationTurn(NationCommand $reservedCommand): ?NationCommand{
if($reservedCommand->isRunnable()){
return $reservedCommand;
}
foreach($this->nationPolicy->priority as $actionName){
/** @var ?NationCommand */
if(!key_exists($actionName, $this->nationPolicy::$availableInstantTurn)){
continue;
}
if(!$this->nationPolicy->{'can'.$actionName}){
continue;
}
$result = $this->{'do'.$actionName}($reservedCommand);
if($result !== null){
return $result;
}
}
return buildNationCommandClass(null, $this->general, $this->env, $this->general->getLastTurn());
}
public function chooseGeneralTurn(GeneralCommand $reservedCommand): GeneralCommand{
if($reservedCommand->isRunnable()){
return $reservedCommand;
}
$general = $this->general;
$npcType = $general->getVar('npc');
$nationID = $general->getNationID();
if($npcType == 5){
return $this->do집합($reservedCommand);
}
else if($npcType == 2 && $nationID == 0){
$result = $this->do거병($reservedCommand);
if($result !== null){
return $result;
}
}
if($nationID === 0 && $this->generalPolicy->can국가선택){
$result = $this->do국가선택($reservedCommand);
if($result !== null){
return $result;
}
}
else {
if($npcType >= 2 && $general->getVar('level') == 12 && !$this->nation['capital']){
//방랑군 건국
$result = $this->do건국($reservedCommand);
if($result !== null){
return $result;
}
$result = $this->do방랑군이동($reservedCommand);
if($result !== null){
return $result;
}
}
foreach($this->generalPolicy->priority as $actionName){
if(!$this->generalPolicy->{'can'.$actionName}){
continue;
}
/** @var ?GeneralCommand */
$result = $this->{'do'.$actionName}($reservedCommand);
if($result !== null){
return $result;
}
}
}
return $this->do중립($reservedCommand);
}
public function old_chooseNationTurn($command, $arg): array
{
$generalObj = $this->getGeneralObj();
//NOTE: 수뇌 턴에서는 general과 city이름의 충돌 가능성이 있음
$env = $this->env;
$chiefID = $generalObj->getID();
$nationID = $generalObj->getNationID();
$nation = $this->nation;
$db = DB::db();
if ($generalObj->getVar('npc') == 5) {
return [$command, $arg];
}
if ($command && $command != '휴식') {
return [$command, $arg];
}
if ($this->nation['level'] == 0) {
return ['휴식', null];
}
if ($generalObj->getVar('level') == 12 && $this->dipState == self::d평화 && !$this->attackable) {
$targetNationID = $this->findWarTarget();
if ($targetNationID !== null) {
return ['che_선전포고', ['destNationID' => $targetNationID]];
}
}
$nationCities = [];
$frontCitiesID = [];
$frontImportantCitiesID = [];
$supplyCitiesID = [];
$backupCitiesID = [];
$tech = getTechCost($nation['tech']);
foreach ($db->query('SELECT * FROM city WHERE nation = %i', $nationID) as $nationCity) {
$nationCity['generals'] = [];
$cityID = $nationCity['city'];
$dev =
($nationCity['agri'] + $nationCity['comm'] + $nationCity['secu'] + $nationCity['def'] + $nationCity['wall']) /
($nationCity['agri'] + $nationCity['comm'] + $nationCity['secu'] + $nationCity['def'] + $nationCity['wall']);
$dev += $nationCity['pop'] / $nationCity['pop_max'];
$dev /= 50;
$nationCity['dev'] = $dev;
$nationCities[$cityID] = $nationCity;
if ($nationCity['supply']) {
$supplyCitiesID[] = $cityID;
if ($nationCity['front']) {
$frontCitiesID[] = $cityID;
if ($nationCity['officer4']) {
$frontImportantCitiesID[] = $cityID;
}
} else {
$backupCitiesID[] = $cityID;
}
}
}
Util::shuffle_assoc($nationCities);
shuffle($frontCitiesID);
shuffle($supplyCitiesID);
assert($supplyCitiesID);
$commandList = [];
$userGenerals = [];
$lostGenerals = [];
$npcCivilGenerals = [];
$npcWarGenerals = [];
$generalIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation = %i', $nationID);
foreach(General::createGeneralObjListFromDB($generalIDList, null, 2) as $nationGeneral){
}
foreach ($db->query('SELECT `no`, nation, city, npc, `gold`, `rice`, leadership, `strength`, intel, killturn, crew, train, atmos, `level`, troop, recent_war FROM general WHERE nation = %i', $nationID) as $rawNationGeneral) {
$nationGeneral = (object) $rawNationGeneral;
$cityID = $nationGeneral->city;
$generalID = $nationGeneral->no;
if ($generalID == $chiefID) {
continue;
}
if (key_exists($cityID, $nationCities)) {
$nationCities[$cityID]['generals'][] = $nationGeneral;
if (!$nationCities[$cityID]['supply']) {
$lostGenerals[] = $nationGeneral;
}
} else {
$lostGenerals[] = $nationGeneral;
}
if ($nationGeneral->npc < 2 && $nationGeneral->killturn >= 5) {
$userGenerals[] = $nationGeneral;
} else if ($nationGeneral->getLeadership() >= $this->nationPolicy->minNPCWarLeadership && $nationGeneral->killturn >= 5) {
$npcWarGenerals[] = $nationGeneral;
} else {
//삭턴이 몇 안남은 장수는 '내정장 npc'로 처리
$npcCivilGenerals[] = $nationGeneral;
}
$nationGenerals[$generalID] = $nationGeneral;
}
Util::shuffle_assoc($nationGenerals);
$this->nationGenerals = $nationGenerals;
$this->npcCivilGenerals = $npcCivilGenerals;
$this->npcWarGenerals = $npcWarGenerals;
shuffle($lostGenerals);
$this->lostGenerals = $lostGenerals;
uasort($nationCities, function ($lhs, $rhs) {
//키 순서를 지키지 않지만, 원래부터 random order를 목표로 하므로 크게 신경쓰지 않는다.
return count($lhs['generals']) <=> count($rhs['generals']);
});
//타 도시에 있는 '유저장' 발령
foreach ($lostGenerals as $lostGeneral) {
if ($lostGeneral->npc < 2) {
if (in_array($this->dipState, [self::d직전, self::d전쟁]) && $frontCitiesID) {
$selCityID = Util::choiceRandom($frontCitiesID);
} else {
$selCityID = Util::choiceRandom($supplyCitiesID);
}
$commandList[] = [['che_발령', ['destGeneralID' => $lostGeneral->no, 'destCityID' => $selCityID]], 200];
}
}
$resName = Util::choiceRandom(['gold', 'rice']);
usort($userGenerals, function (General $lhs, General $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
usort($npcWarGenerals, function (General $lhs, General $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
usort($npcCivilGenerals, function (General $lhs, General $rhs) use ($resName) {
return $lhs->getVar($resName) <=> $rhs->getVar($resName);
});
$avgUserRes = 0;
foreach ($userGenerals as $userGeneral) {
$avgUserRes += $userGeneral->getVar($resName);
}
$avgUserRes /= max(1, count($userGenerals));
$avgNpcWarRes = 0;
foreach ($npcWarGenerals as $npcWarGeneral) {
$avgNpcWarRes += $npcWarGeneral->getVar($resName);
}
$avgNpcWarRes /= max(1, count($npcWarGenerals));
$avgNpcCivilRes = 0;
foreach ($npcCivilGenerals as $npcCivilGeneral) {
$avgNpcCivilRes += $npcCivilGeneral->getVar($resName);
}
$avgNpcCivilRes /= max(1, count($npcCivilGenerals));
//금쌀이 부족한 '유저장' 먼저 포상
while ($nation[$resName] > ($resName == 'gold' ? 1 : 2) * 3000 && $userGenerals) {
$isWarUser = null;
foreach ($userGenerals as $compUser) {
if ($compUser->getLeadership() >= 50) {
$isWarUser = true;
break;
}
if (Util::randBool(0.2)) {
$isWarUser = false;
break;
}
}
if ($isWarUser === null) {
break;
}
$compRes = $compUser->getVar($resName);
$work = false;
if (!$isWarUser) {
$work = false;
} else if ($compRes < $avgNpcWarRes * 3) {
$work = true;
} elseif ($compRes < $avgNpcCivilRes * 4) {
$work = true;
}
if ((($isWarUser || $resName == 'gold') && $compUser->getVar($resName) < 21000) || ($compUser->getVar($resName) < 5000)) {
if ($work) {
//TODO: 새로 구현한 코드로 이전
$amount = min(GameConst::$maxResourceActionAmount, intdiv(($nation[$resName] - ($resName == 'rice' ? (GameConst::$baserice) : (GameConst::$basegold))), 3000) * 1000 + 1000);
$commandList[] = [['che_포상', [
'destGeneralID' => $userGenerals[0]->no,
'isGold' => $resName == 'gold',
'amount' => $amount
]], 10]; // 금,쌀 1000단위 포상
} else {
$amount = min(GameConst::$maxResourceActionAmount, intdiv(($nation[$resName] - ($resName == 'rice' ? (GameConst::$baserice) : (GameConst::$basegold))), 5000) * 1000 + 1000);
$commandList[] = [['che_포상', [
'destGeneralID' => $userGenerals[0]->no,
'isGold' => $resName == 'gold',
'amount' => $amount
]], 1]; // 금,쌀 1000단위 포상
}
}
break;
}
$minRes = $env['develcost'] * 24 * $tech;
if ($nation[$resName] < ($resName == 'gold' ? 1 : 2) * 3000) { // 몰수
// 몰수 대상
$compUser = Util::array_last($userGenerals);
$compNpcWar = Util::array_last($npcWarGenerals);
$compNpcCivil = Util::array_last($npcCivilGenerals);
$compUserRes = $compUser ? $compUser->getVar($resName) : 0;
$compNpcWarRes = $compNpcWar ? $compNpcWar->getVar($resName) * 5 : 0;
$compNpcCivilRes = $compNpcCivil ? $compNpcCivil->getVar($resName) * 10 : 0;
[$compRes, $targetGeneral] = max(
[$compNpcCivilRes, $compNpcCivil],
[$compNpcWarRes, $compNpcWar],
[$compUserRes, $compUser]
);
if ($targetGeneral) {
if ($targetGeneral === $compNpcCivil) {
$amount = Util::round($targetGeneral->getVar($resName) - $minRes * 3, -2);
} else {
$amount = min(10000, intdiv($targetGeneral->getVar($resName), 5000) * 1000 + 1000);
}
if ($amount > 0) {
$commandList[] = [['che_몰수', [
'destGeneralID' => $targetGeneral->no,
'isGold' => $resName == 'gold',
'amount' => $amount
]], 3];
}
}
} else { // 포상
/** @var General */
$compNpcWar = Util::array_first($npcWarGenerals);
$compNpcCivil = null;
foreach ($npcCivilGenerals as $npcCivil) {
if ($npcCivil->npc == 5) {
continue;
}
$compNpcCivil = $npcCivil;
break;
}
if ($compNpcWar && $compNpcWar->getVar($resName) < 21000) {
$amount = min(100, intdiv(($nation[$resName] - ($resName == 'rice' ? (GameConst::$baserice) : (GameConst::$basegold))), 5000) * 10 + 10) * 100;
$commandList[] = [['che_몰수', [
'destGeneralID' => $compNpcWar->no,
'isGold' => $resName == 'gold',
'amount' => $amount
]], 3];
}
if ($compNpcCivil && $compNpcCivil->getVar($resName) < $minRes) {
$amount = intdiv($minRes + 99, 100);
$commandList[] = [['che_몰수', [
'destGeneralID' => $compNpcCivil->no,
'isGold' => $resName == 'gold',
'amount' => $amount
]], 2];
}
}
//고립 도시 장수 발령
foreach ($lostGenerals as $lostGeneral) {
if ($lostGeneral->npc < 2) {
//고립 유저 장수는 이미 세팅했음
continue;
}
if (in_array($this->dipState, [self::d직전, self::d전쟁]) && $frontCitiesID) {
$selCityID = Util::choiceRandom($frontCitiesID);
} else {
$selCityID = Util::choiceRandom($supplyCitiesID);
}
//고립된 장수가 많을 수록 발령 확률 증가
$commandList[] = [['che_발령', [
'destGeneralID' => $lostGeneral->no,
'destCityID' => $selCityID
]], sqrt(count($lostGenerals)) * 10];
}
// 평시엔 균등 발령만
if (in_array($this->dipState, [self::d평화, self::d선포]) && count($supplyCitiesID) > 1) {
$targetCity = null;
$minCity = null;
$maxCity = null;
$maxDevCity = null;
foreach ($nationCities as $nationCity) {
if ($nationCity['dev'] >= 95) {
continue;
}
if ($nationCity['supply']) {
$minCity = $nationCity;
break;
}
}
//reverse_order T_T
$maxCity = end($nationCities);
if (!$minCity) {
$minCity = $maxCity;
}
while ($maxCity['city'] !== $minCity['city']) {
if ($nationCity['supply']) {
break;
}
$maxCity = prev($nationCities);
}
foreach ($nationCities as $nationCity) {
if ($nationCity['city'] == $maxCity['city']) {
break;
}
if (!$nationCity['supply']) {
continue;
}
if ($nationCity['dev'] < 70) {
$targetCity = $nationCity;
break;
}
}
foreach ($nationCities as $nationCity) {
if (!$nationCity['supply']) {
continue;
}
if (count($nationCity['generals']) == 0) {
continue;
}
if ($maxDevCity === null || $maxDevCity['dev'] < $nationCity['dev']) {
$maxDevCity = $nationCity;
}
}
if ($targetCity === null || (count($targetCity['generals']) >= count($maxCity['generals']) - 1)) {
$targetCity = $minCity;
}
if ($maxDevCity['dev'] >= 95 && $targetCity['city'] != $maxDevCity['city'] && $targetCity['dev'] <= 70) {
$targetGeneral = Util::choiceRandom($maxDevCity['generals']);
if ($targetGeneral->troop != 0) {
$commandList[] = [['che_발령', [
'destGeneralID' => $targetGeneral->no,
'destCityID' => $targetCity['city']
]], 2];
}
}
if (count($targetCity['generals']) < count($maxCity['generals']) - 2) {
//세명 이상 차이나야 함
$targetGeneral = Util::choiceRandom($maxCity['generals']);
if ($targetGeneral->npc == 5) {
} else if ($targetGeneral->npc >= 2 || $maxCity['dev'] >= 95 && $targetGeneral->troop == 0) {
//유저장은 의도가 있을 것이므로 삽나지 않는 이상 발령 안함!
$commandList[] = [['che_발령', [
'destGeneralID' => $targetGeneral->no,
'destCityID' => $targetCity['city']
]], 5];
}
}
}
// 병사있고 쌀있고 후방에 있는 장수
if ($frontCitiesID) {
$workRemain = 3;
foreach ($nationGenerals as $nationGeneral) {
$generalCity = $nationCities[$nationGeneral->city] ?? null;
if (!$generalCity) {
continue;
}
if ($nationGeneral->crew < 2000) {
continue;
}
if ($nationGeneral->rice < 700 * $tech) {
continue;
}
if ($generalCity['front']) {
continue;
}
if ($nationGeneral->train * $nationGeneral->atmos < 75 * 75) {
continue;
}
if ($nationGeneral->troop != 0) {
continue;
}
$score = 5;
if ($nationGeneral->npc < 2) {
$score *= 4;
}
if (Util::randBool(0.3) && $frontImportantCitiesID) {
$targetCityID = Util::choiceRandom($frontImportantCitiesID);
} else {
$targetCityID = Util::choiceRandom($frontCitiesID);
}
$command = ['che_발령', [
'destGeneralID' => $nationGeneral->no,
'destCityID' => $targetCityID
]];
if ($nationGeneral->npc < 2 && ($workRemain & 2)) {
$workRemain ^= 2;
$commandList[] = [$command, $score];
} else if ($nationGeneral->npc >= 2 && ($workRemain & 1)) {
$workRemain ^= 1;
$commandList[] = [$command, $score];
}
if ($workRemain <= 0) {
break;
}
}
}
//병사 없고 인구없는 전방에 있는 장수
if ($frontCitiesID && $backupCitiesID) {
$workRemain = 3;
foreach ($nationGenerals as $nationGeneral) {
$generalCity = $nationCities[$nationGeneral->city] ?? null;
if (!$generalCity) {
continue;
}
if ($nationGeneral->crew >= 1000) {
continue;
}
if ($nationGeneral->rice < 700 * $tech) {
continue;
}
if (!$generalCity['front']) {
continue;
}
if ($generalCity['pop'] - 33000 > $nationGeneral->getLeadership()) {
continue;
}
if ($nationGeneral->troop != 0) {
continue;
}
$score = 5;
if ($nationGeneral->npc < 2) {
$score *= 4;
}
$popTrial = 5;
for ($popTrial = 0; $popTrial < 5; $popTrial++) {
$targetCity = $nationCities[Util::choiceRandom($backupCitiesID)];
if ($targetCity['pop'] < 33000 + $nationGeneral->getLeadership()) {
continue;
}
if (Util::randBool($targetCity['pop'] / $targetCity['pop_max'])) {
break;
}
}
$command = ['che_발령', [
'destGeneralID' => $nationGeneral->no,
'destCityID' => $targetCity['city']
]];
if ($nationGeneral->npc < 2 && ($workRemain & 2)) {
$workRemain ^= 2;
$commandList[] = [$command, $score];
} else if ($nationGeneral->npc >= 2 && ($workRemain & 1)) {
$workRemain ^= 1;
$commandList[] = [$command, $score];
}
if ($workRemain <= 0) {
break;
}
}
}
if (!$commandList) return ['휴식', null];
return Util::choiceRandomUsingWeightPair($commandList);
}
public function chooseNeutralTurn(): array
{
$general = $this->getGeneralObj();
$city = $this->city;
$env = $this->env;
$db = DB::db();
$arg = null;
// 오랑캐는 바로 임관
if ($general->getVar('npc') == 9) {
$rulerNation = $db->queryFirstField(
'SELECT nation FROM general WHERE `level`=12 AND npc=9 and nation not in %li ORDER BY RAND() limit 1',
$general->getAuxVar('joinedNations') ?? [0]
);
if ($rulerNation) {
return ['che_임관', ['destNationID' => $rulerNation]];
}
return ['che_견문', null];
}
switch (Util::choiceRandomUsingWeight([
'임관' => 11.4,
'거병_견문' => 40,
'이동' => 20,
'기타' => 28.6
])) {
//임관
case '임관':
$available = true;
if ($env['startyear'] + 3 > $env['year']) {
//초기 임관 기간에서는 국가가 적을수록 임관 시도가 적음
$nationCnt = $db->queryFirstField('SELECT count(nation) FROM nation');
$notFullNationCnt = $db->queryFirstField('SELECT count(nation) FROM nation WHERE gennum < %i', GameConst::$initialNationGenLimit);
if ($nationCnt == 0 || $notFullNationCnt == 0) {
$available = false;
} else if (Util::randBool(pow(1 / $nationCnt / pow($notFullNationCnt, 3), 1 / 4))) {
//국가가 1개일 경우에는 '임관하지 않음'
$available = false;
}
}
if ($general->getVar('affinity') == 999 || !$available) {
$command = 'che_견문'; //견문
} else {
//랜임 커맨드 입력.
$command = 'che_랜덤임관';
$arg = [
'destNationIDList' => []
];
}
break;
case '거병_견문': //거병이나 견문
// 초반이면서 능력이 좋은놈 위주로 1.4%확률로 거병
$prop = Util::randF() * (GameConst::$defaultStatNPCMax + GameConst::$chiefStatMin) / 2;
$ratio = ($general->getVar('leadership') + $general->getVar('strength') + $general->getVar('intel')) / 3;
if ($env['startyear'] + 2 > $env['year'] && $prop < $ratio && Util::randBool(0.014) && $general->getVar('makelimit') == 0) {
//거병
$command = 'che_거병';
} else {
//견문
$command = 'che_견문';
}
break;
case '이동': //이동
$paths = array_keys(CityConst::byID($city['city'])->path);
$command = 'che_이동';
$arg = ['destCityID' => Util::choiceRandom($paths)];
break;
default:
$command = 'che_견문';
break;
}
return [$command, $arg];
}
public function old_chooseGeneralTurn($command, $arg): array
{
$general = $this->getGeneralObj();
$city = $this->city;
$nation = $this->nation;
$env = $this->env;
$cityID = $general->getCityID();
$nationID = $general->getNationID();
$genType = $this->genType;
$leadership = $this->leadership;
$strength = $this->strength;
$intel = $this->intel;
$startYear = $env['startyear'];
$year = $env['year'];
$month = $env['month'];
$db = DB::db();
if ($general->getVar('npc') == 5) {
if ($nationID == 0 && $general->getVar('killturn') > 1) {
$command = '휴식'; //휴식
$arg = null;
$general->setVar('killturn', 1);
} else if ($general->getVar('level') == 12) {
$command = 'che_선양';
$arg = [
'destGeneralID' => $db->queryFirstField('SELECT `no` FROM general WHERE nation = %i AND npc != 5 ORDER BY RAND() LIMIT 1', $general->getNationID())
];
} else {
$command = 'che_집합'; //집합
$arg = [];
$general->setVar('killturn', rand(70, 75));
//NOTE: 부대 편성에 보여야 하므로 이것만 DB에 직접 접근함.
$db->update('general_turn', [
'action' => 'che_집합',
'arg' => '{}',
'brief' => '집합'
], 'general_ID=%i AND turn_idx < 6', $general->getID());
}
return [$command, $arg];
}
//특별 메세지 있는 경우 출력 하루 4번
$term = $env['turnterm'];
if ($general->getVar('npcmsg') && Util::randBool($term / (6 * 60))) {
$src = new MessageTarget(
$general->getID(),
$general->getVar('name'),
$general->getVar('nation'),
$nation['name'],
$nation['color'],
GetImageURL($general->getVar('imgsvr'), $general->getVar('picture'))
);
$msg = new Message(
Message::MSGTYPE_PUBLIC,
$src,
$src,
$general->getVar('npcmsg'),
new \DateTime(),
new \DateTime('9999-12-31'),
[]
);
$msg->send();
}
if ($command && $command != '휴식') {
return [$command, $arg];
}
if ($general->getVar('level') == 0) {
return $this->chooseNeutralTurn();
}
$techCost = getTechCost($nation['tech']);
if ($general->getVar('defence_train') < 80) {
$general->setVar('defence_train', 80);
}
if ($general->getVar('level') == 12) {
$turn = $this->processLordTurn();
if ($turn !== null) {
return $turn;
}
}
if ($general->getVar('killturn') < 5) {
return $this->chooseEndOfNPCTurn();
}
if ($general->getVar('injury') > 10) {
return ['che_요양', null];
}
if ($nation['level'] == 0) {
//아직 건국을 안했다.
if ($startYear + 3 <= $year) {
return ['che_하야', null];
}
if (Util::randBool(0.2)) {
return ['che_견문', null];
} else {
return ['che_물자조달', null];
}
}
if ($city['nation'] != $nationID || !$city['supply']) {
return ['che_귀환', null];
}
[$baseArmGold, $baseArmRice] = $this->getBaseArmCost();
if ($nation['rice'] < 2000) {
if (($genType & self::t통솔장) && $general->getVar('rice') > $baseArmRice) {
return ['che_헌납', ['isGold' => false, 'amount' => Util::toInt(($general->getVar('rice') - $baseArmRice) / 2)]];
} else if (!($genType & self::t통솔장)) {
return ['che_헌납', ['isGold' => false, 'amount' => Util::toInt($general->getVar('rice') / 2)]];
}
}
if ($genType & self::t통솔장) {
$warTurn = $this->processWar();
if ($warTurn !== null) {
return $warTurn;
}
if ($general->getVar('rice') < $baseArmRice && $general->getVar('gold') >= $baseArmGold * 3) {
return [
'che_군량매매',
[
'buyRice' => true,
'amount' => Util::toInt($general->getVar('gold') - $baseArmGold)
]
];
}
if ($general->getVar('gold') < $baseArmGold && $general->getVar('gold') >= $baseArmRice * 3) {
return [
'che_군량매매',
[
'buyRice' => false,
'amount' => Util::toInt($general->getVar('rice') - $baseArmRice)
]
];
}
} else {
if ($general->getVar('rice') < $this->baseDevelCost && $general->getVar('gold') >= $this->baseDevelCost * 3) {
return [
'che_군량매매',
[
'buyRice' => true,
'amount' => Util::toInt($general->getVar('gold') - $this->baseDevelCost)
]
];
}
if ($general->getVar('gold') < $this->baseDevelCost && $general->getVar('gold') >= $this->baseDevelCost * 3) {
return [
'che_군량매매',
[
'buyRice' => false,
'amount' => Util::toInt($general->getVar('rice') - $this->baseDevelCost)
]
];
}
}
$cityFull = false;
$developTurn = $this->chooseDevelopTurn($cityFull);
if ($cityFull && Util::randBool(0.2)) {
$moveRawCities = $db->query('SELECT city,front,(pop/10+agri+comm+secu+def+wall)/(pop_max/10+agri_max+comm_max+secu_max+def_max+wall_max)*100 as dev, officer3 FROM city WHERE nation=%i', $nationID);
$moveCities = [];
foreach ($moveRawCities as $moveCity) {
$moveCityID = $moveCity['city'];
if ($moveCity['dev'] > 99) {
continue;
}
$score = 1 / $moveCity['dev'];
if ($moveCity['officer3']) {
$score *= 1.3;
}
$moveCities[$moveCityID] = $score;
}
if ($moveCities) {
return ['che_NPC능동', [
'optionText' => '순간이동',
'destCityID' => Util::choiceRandomUsingWeight($moveCities),
]];
}
}
return $developTurn;
}
protected function getBaseArmCost()
{
//총 통솔의 절반을 징병하는 것을 기준으로 함
$general = $this->getGeneralObj();
$tech = $this->nation['tech'];
$crewType = $general->getCrewTypeObj();
$baseArmCost = $crewType->costWithTech(
$tech,
$general->getLeadership(false) * 50
); //기본 병종
$baseArmCost = $general->onCalcDomestic('징병', 'cost', $baseArmCost);
$baseArmRice = $general->getLeadership(false) / 2 * $crewType->rice * getTechCost($tech);
if ($general->getRankVar('deathcrew') > 500 && $general->getRankVar('killcrew') > 500) {
$baseArmRice *= $general->getRankVar('killcrew') / $general->getRankVar('deathcrew');
}
return [$baseArmCost, $baseArmRice];
}
protected function processWar(): ?array
{
$db = DB::db();
$general = $this->getGeneralObj();
if (!$this->attackable && $this->dipState == self::d평화) {
if ($this->dipState == self::d평화 && $general->getVar('crew') >= 1000 && Util::randBool(0.25)) {
return ['che_소집해제', null];
}
return null;
}
$city = $this->city;
$nation = $this->nation;
$cityID = $city['city'];
$nationID = $nation['nation'];
$env = $this->env;
[$baseArmGold, $baseArmRice] = $this->getBaseArmCost();
if ($general->getVar('rice') <= $baseArmRice) {
return null;
}
if (
($city['front'] > 0 && $city['trust'] < 60) ||
($city['front'] == 0 && $city['trust'] < 95)
) {
return ['che_주민선정', null];
}
if ($general->getVar('crew') < 1000) {
if ($general->getVar('gold') <= $baseArmGold) {
//반징도 불가? 내정
return null;
}
$sumLeadershipInCity = $db->queryFirstField('SELECT sum(leadership) FROM general WHERE nation = %i AND city = %i AND leadership > %i', $nationID, $cityID, $this->nationPolicy->minNPCWarLeadership);
if (
$sumLeadershipInCity > 0 &&
$city['pop'] > 30000 + $general->getLeadership(false) * 100 * 1.3 &&
Util::randBool(($city['pop'] - 30000) / $sumLeadershipInCity * 100)
) {
return $this->chooseRecruitCrewType();
}
$recruitableCityList = $db->queryAllLists(
'SELECT city, (pop - 30000) as relPop FROM city WHERE nation = %i AND pop > %i AND supply = 1',
$nationID,
30000 + $general->getLeadership(false) * 100
);
if (!$recruitableCityList) {
//징병 가능한 도시가 없구려
return ['che_정착장려', null];
}
return ['che_NPC능동', [
'optionText' => '순간이동',
'destCityID' => Util::choiceRandomUsingWeightPair($recruitableCityList),
]];
}
if (
$general->getVar('train') >= 90 &&
$general->getVar('atmos') >= 90
) {
//출병 가능!
if (
$this->attackable &&
$env['year'] >= $env['startyear'] + 3 &&
$city['front'] >= 2 &&
$nation['war'] == 0
) {
return $this->processAttack();
}
if ($city['front'] > 0) {
//전방에 훈사까지 완료되어있으면 내정을 해야..
return null;
}
$frontCities = $db->query('SELECT city, front, officer4 FROM city WHERE nation=%i AND front > 0 AND supply = 1', $nationID);
if (!$frontCities) {
//접경이 아직 없음
return null;
}
$nearCities = [];
$attackableCities = [];
foreach ($frontCities as $frontCity) {
$frontCityID = $frontCity['city'];
$score = 0.2;
if ($frontCity['front'] > 1 && $frontCity['officer4']) {
$score += 3;
}
$attackableCities[$frontCityID] = $score;
foreach (array_keys(CityConst::byID($frontCityID)->path) as $nearCity) {
if (!key_exists($nearCity, $nearCities)) {
$nearCities[$nearCity] = [$frontCityID];
} else {
$nearCities[$nearCity][] = $frontCityID;
}
}
}
foreach ($db->query(
'SELECT city FROM city WHERE nation IN %li AND city IN %li',
array_keys($this->warTargetNation),
array_keys($nearCities)
) as $targetCity) {
$targetCityID = $targetCity['city'];
foreach ($nearCities[$targetCityID] as $attackableCity) {
$attackableCities[$attackableCity] += 1;
}
}
if ($attackableCities) {
return ['che_NPC능동', [
'optionText' => '순간이동',
'destCityID' => Util::choiceRandomUsingWeight($attackableCities),
]];
}
}
if ($general->getVar('train') < 90) {
$turnObj = buildGeneralCommandClass('che_훈련', $general, $env, null);
[$reqGold, $reqRice] = $turnObj->getCost();
if ($general->getVar('gold') >= $reqGold && $general->getVar('rice') >= $reqRice) {
return ['che_훈련', null];
}
}
if ($general->getVar('atmos') < 90) {
$turnObj = buildGeneralCommandClass('che_사기진작', $general, $env, null);
[$reqGold, $reqRice] = $turnObj->getCost();
if ($general->getVar('gold') >= $reqGold && $general->getVar('rice') >= $reqRice) {
return ['che_사기진작', null];
}
}
//훈사할 금조차도 없다고..? 아마 쌀을 팔아야 할 것.
return null;
}
protected function proceessNeutralLordTurn(): array
{
$db = DB::db();
$general = $this->getGeneralObj();
$city = $this->city;
$nation = $this->nation;
$env = $this->env;
$startYear = $env['startyear'];
$year = $env['year'];
$month = $env['month'];
if ($startYear + 2 <= $year) {
return ['che_해산', null];
}
if ($city['nation'] == 0 && ($city['level'] == 5 || $city['level'] == 6)) {
$nationType = Util::choiceRandom(GameConst::$availableNationType);
$nationColor = Util::choiceRandom(array_keys(GetNationColors()));
return ['che_건국', [
'nationName' => "㉿" . mb_substr($general->getName(), 1),
'nationType' => $nationType,
'colorType' => $nationColor
]];
}
//모든 공백지 조사
$lordCities = $db->queryFirstColumn('SELECT city.city FROM general LEFT JOIN city ON general.city = city.city WHERE general.level = 12 AND city.nation != 0');
$nationCities = $db->queryFirstColumn('SELECT city FROM city WHERE nation != 0');
$occupiedCities = [];
foreach ($lordCities as $tCityId) {
$occupiedCities[$tCityId] = 2;
}
foreach ($nationCities as $tCityId) {
$occupiedCities[$tCityId] = 1;
}
$targetCity = [];
//NOTE: 최단 거리가 현재 도시에서 '어떻게 가야' 가장 짧은지 알 수가 없으므로, 한칸 간 다음 계산하기로
foreach (array_keys(CityConst::byID($general->getVar('city'))->path) as $nearCityID) {
if (CityConst::byID($nearCityID)->level < 4) {
$targetCity[$nearCityID] = 0.5;
} else if (!key_exists($nearCityID, $occupiedCities)) {
$targetCity[$nearCityID] = 2;
} else {
$targetCity[$nearCityID] = 0;
}
$distanceFrom = searchDistance($nearCityID, 4, true);
foreach ($distanceFrom as $distance => $distCities) {
foreach ($distCities as $distCity) {
if (key_exists($distCity, $occupiedCities)) {
continue;
}
if (CityConst::byID($distCity)->level < 4) {
continue;
}
$targetCity[$nearCityID] += 1 / (2 ** $distance);
}
}
}
return ['che_이동', ['destCityID' => Util::choiceRandomUsingWeight($targetCity)]];
}
protected function processLordTurn(): ?array
{
$general = $this->getGeneralObj();
$city = $this->city;
$nation = $this->nation;
$env = $this->env;
$year = $env['year'];
$month = $env['month'];
if ($general->getVar('npc') == 9 && $this->dipState == self::d평화 && !$this->attackable) {
if ($nation['level'] == 0) {
return ['che_해산', null];
} else {
return ['che_방랑', null];
}
}
if (in_array($month, [1, 4, 7, 10])) {
$this->calcPromotion();
} else if ($month == 12) {
$this->calcTexRate();
$this->calcGoldBillRate();
} else if ($month == 6) {
$this->calcTexRate();
$this->calcRiceBillRate();
}
if ($nation['level'] == 0) {
return $this->proceessNeutralLordTurn();
}
return null;
}
protected function getNationDevelopedRate()
{
if ($this->devRate !== null) {
return $this->devRate;
}
$db = DB::db();
$nationID = $this->nation['nation'];
$this->devRate = $db->queryFirstRow(
'SELECT sum(pop)/sum(pop_max) as pop_p,(sum(agri)+sum(comm)+sum(secu)+sum(def)+sum(wall))/(sum(agri_max)+sum(comm_max)+sum(secu_max)+sum(def_max)+sum(wall_max)) as all_p from city where nation=%i',
$nationID
);
return $this->devRate;
}
protected function findWarTarget(): ?int
{
$db = DB::db();
$nation = $this->nation;
$nationID = $nation['nation'];
SetNationFront($nationID);
$frontCount = $db->queryFirstField('SELECT count(city) FROM city WHERE nation=%i AND front>0', $nationID);
if ($frontCount > 0) {
return null;
}
$devRate = $this->getNationDevelopedRate();
if (($devRate['pop_p'] + $devRate['all_p']) / 2 < 0.8) {
return null;
}
$nations = [];
foreach ($db->queryAllLists('SELECT nation, power FROM nation WHERE level>0') as [$destNationID, $destNationPower]) {
if (!isNeighbor($nationID, $destNationID)) {
continue;
}
$nations[$destNationID] = 1 / sqrt($destNationPower + 1);
}
if (!$nations) {
return null;
}
return Util::choiceRandomUsingWeight($nations);
}
protected function calcPromotion()
{
$db = DB::db();
$nation = $this->nation;
$nationID = $nation['nation'];
$minChiefLevel = getNationChiefLevel($nation['level']);
$minKillturn = $this->env['killturn'] - Util::toInt(30 / $this->env['turnterm']);
$chiefCandidate = [];
//이 함수를 부르는건 군주 AI이므로, 군주는 세지 않아도 됨
$userChief = [];
$db->update('general', [
'permission' => 'ambassador',
], 'nation=%i AND npc < 2 AND level > 4', $nationID);
foreach ($db->query(
'SELECT no, npc, level, killturn FROM general WHERE nation = %i AND 12 > level AND level > 4',
$nationID
) as $chief) {
if ($chief['npc'] < 2 && $chief['killturn'] < $minKillturn) {
$chiefCandidate[$chief['level']] = $chief['no'];
$userChief[$chief['no']] = $chief['level'];
}
}
$db->update('general', [
'level' => 1
], 'level < 12 AND level > 4 AND nation = %i', $nationID);
$maxBelong = $db->queryFirstField('SELECT max(belong) FROM `general` WHERE nation=%i', $nationID);
$maxBelong = min($maxBelong - 1, 3);
if (!$userChief) {
$candUserChief = $db->queryFirstField(
'SELECT no FROM general WHERE nation = %i AND level = 1 AND killturn > %i AND npc < 2 AND belong >= %i ORDER BY leadership DESC LIMIT 1',
$nationID,
$minKillturn,
$maxBelong
);
if ($candUserChief) {
$userChief[$candUserChief] = 11;
$chiefCandidate[11] = $candUserChief;
}
}
$promoted = $userChief;
if (!key_exists(11, $chiefCandidate)) {
$candChiefHead = $db->queryFirstField(
'SELECT no FROM general WHERE nation = %i AND level = 1 AND npc >= 2 AND belong >= %i ORDER BY leadership DESC LIMIT 1',
$nationID,
$maxBelong
);
if ($candChiefHead) {
$chiefCandidate[11] = $candChiefHead;
$promoted[$candChiefHead] = 11;
}
}
if ($minChiefLevel < 11) {
//무장 수뇌 후보
$candChiefStrength = $db->queryFirstColumn(
'SELECT no FROM general WHERE nation = %i AND strength >= %i AND level = 1 AND belong >= %i ORDER BY strength DESC LIMIT %i',
$nationID,
GameConst::$chiefStatMin,
$maxBelong,
12 - $minChiefLevel
);
//지장 수뇌 후보
$candChiefIntel = $db->queryFirstColumn(
'SELECT no FROM general WHERE nation = %i AND intel >= %i AND level = 1 AND belong >= %i ORDER BY intel DESC LIMIT %i',
$nationID,
GameConst::$chiefStatMin,
$maxBelong,
12 - $minChiefLevel
);
//무력, 지력이 모두 높은 장수를 고려하여..
$iterCandChiefStrength = new \ArrayIterator($candChiefStrength);
$iterCandChiefIntel = new \ArrayIterator($candChiefIntel);
foreach (range(10, $minChiefLevel, -1) as $chiefLevel) {
if (key_exists($chiefLevel, $chiefCandidate)) {
continue;
}
/** @var \ArrayIterator $iterCurrentType */
$iterCurrentType = ($chiefLevel % 2 == 0) ? $iterCandChiefStrength : $iterCandChiefIntel;
$candidate = $iterCurrentType->current();
while (key_exists($candidate, $promoted)) {
$iterCurrentType->next();
if (!$iterCurrentType->valid()) {
break;
}
$candidate = $iterCurrentType->current();
}
if ($candidate) {
$chiefCandidate[$chiefLevel] = $candidate;
$promoted[$candidate] = $chiefLevel;
}
}
foreach ($chiefCandidate as $chiefLevel => $chiefID) {
$db->update('general', [
'level' => $chiefLevel
], 'no=%i', $chiefID);
}
}
}
protected function calcTexRate(): int
{
$db = DB::db();
$nation = $this->nation;
$env = $this->env;
$nationID = $nation['nation'];
//도시
$cityCount = $db->queryFirstField('SELECT count(*) FROM city WHERE nation = %i', $nationID);
if ($cityCount == 0) {
$db->update('nation', [
'war' => 0,
'rate' => 15
], 'nation=%i', $nationID);
return 15;
} else {
$devRate = $this->getNationDevelopedRate();
$avg = ($devRate['pop_p'] + $devRate['all_p']) / 2;
if ($avg > 0.95) $rate = 25;
elseif ($avg > 0.70) $rate = 20;
elseif ($avg > 0.50) $rate = 15;
else $rate = 10;
$db->update('nation', [
'war' => 0,
'rate' => $rate
], 'nation=%i', $nationID);
return $rate;
}
}
protected function calcGoldBillRate(): int
{
$db = DB::db();
$nation = $this->nation;
$env = $this->env;
$nationID = $nation['nation'];
$cityList = $db->query('SELECT * FROM city WHERE nation=%i', $nationID);
if (!$cityList) {
return 20;
}
$dedicationList = $db->query('SELECT dedication FROM general WHERE nation=%i AND npc!=5', $nationID);
$goldIncome = getGoldIncome($nation['nation'], $nation['level'], $nation['rate'], $nation['capital'], $nation['type'], $cityList);
$warIncome = getWarGoldIncome($nation['type'], $cityList);
$income = $goldIncome + $warIncome;
$outcome = getOutcome(100, $dedicationList);
$bill = intval($income / $outcome * 80); // 수입의 80% 만 지급
if ($bill < 20) {
$bill = 20;
}
if ($bill > 200) {
$bill = 200;
}
$db->update('nation', [
'bill' => $bill,
], 'nation=%i', $nationID);
return $bill;
}
protected function calcRiceBillRate(): int
{
$db = DB::db();
$nation = $this->nation;
$env = $this->env;
$nationID = $nation['nation'];
$cityList = $db->query('SELECT * FROM city WHERE nation=%i', $nationID);
if (!$cityList) {
return 20;
}
$dedicationList = $db->query('SELECT dedication FROM general WHERE nation=%i AND npc!=5', $nationID);
$riceIncome = getRiceIncome($nation['nation'], $nation['level'], $nation['rate'], $nation['capital'], $nation['type'], $cityList);
$wallIncome = getWallIncome($nation['nation'], $nation['level'], $nation['rate'], $nation['capital'], $nation['type'], $cityList);
$income = $riceIncome + $wallIncome;
$outcome = getOutcome(100, $dedicationList);
$bill = intval($income / $outcome * 80); // 수입의 80% 만 지급
if ($bill < 20) {
$bill = 20;
}
if ($bill > 200) {
$bill = 200;
}
$db->update('nation', [
'bill' => $bill,
], 'nation=%i', $nationID);
return $bill;
}
}