1789 lines
63 KiB
PHP
1789 lines
63 KiB
PHP
<?php
|
|
namespace sammo;
|
|
|
|
class GeneralAI{
|
|
/**
|
|
* @var General $general
|
|
*/
|
|
protected $general;
|
|
protected $city;
|
|
protected $nation;
|
|
protected $genType;
|
|
protected $env;
|
|
|
|
protected $leadership;
|
|
protected $power;
|
|
protected $intel;
|
|
|
|
protected $dipState;
|
|
protected $warTargetNation;
|
|
protected $attackable;
|
|
|
|
protected $devRate = null;
|
|
|
|
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']);
|
|
$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,level,tech,gold,rice,rate,type,color,name,war FROM nation WHERE nation = %i',
|
|
$generalObj->getNationID()
|
|
)??[
|
|
'nation'=>0,
|
|
'level'=>0,
|
|
'tech'=>0,
|
|
'gold'=>0,
|
|
'rice'=>0,
|
|
'type'=>GameConst::$neutralNationType,
|
|
'color'=>'#000000',
|
|
'name'=>'재야',
|
|
];
|
|
|
|
$this->calcGenType();
|
|
|
|
$this->leadership = $general->getLeadership(true, true, true, true);
|
|
$this->power = $general->getPower(true, true, true, true);
|
|
$this->intel = $general->getIntel(true, true, true, true);
|
|
|
|
$this->calcDiplomacyState();
|
|
|
|
}
|
|
|
|
public function getGeneralObj():General{
|
|
return $this->general;
|
|
}
|
|
|
|
protected function calcGenType(){
|
|
$leadership = $this->leadership;
|
|
$power = $this->power;
|
|
$intel = $this->intel;
|
|
|
|
//무장
|
|
if ($power >= $intel) {
|
|
$genType = self::t무장;
|
|
if ($intel >= $power * 0.8) { //무지장
|
|
if(Util::randBool($intel / $power / 2)){
|
|
$genType |= self::t지장;
|
|
}
|
|
|
|
}
|
|
//지장
|
|
} else {
|
|
$genType = self::t지장;
|
|
if ($power >= $intel * 0.8 && Util::randBol(0.2)) { //지무장
|
|
if(Util::randBool($power / $intel / 2)){
|
|
$genType |= self::t무장;
|
|
}
|
|
}
|
|
}
|
|
|
|
//통솔
|
|
if ($leadership >= 40) {
|
|
$genType |= self::t통솔장;
|
|
}
|
|
$this->genType = $genType;
|
|
}
|
|
|
|
protected function calcDiplomacyState(){
|
|
$db = DB::db();
|
|
$nationID = $this->general->getNationID();
|
|
$env = $this->env;
|
|
|
|
$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 WHERE me = %i AND (state = 1 OR (state = 0 AND term < 5))',
|
|
$nationID
|
|
);
|
|
|
|
$warTargetNation = [];
|
|
foreach($warTarget as [$warNationID, $warState]){
|
|
$warTargetNation[$warNationID] = $warState==0?2: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(in_array($this->dipState, [self::d평화, self::d선포]) && $env['startyear'] + 2 == $env['year'] && $env['month'] > 5){
|
|
$this->dipState = self::d징병;
|
|
}
|
|
|
|
if($this->attackable){
|
|
//전쟁으로 인한 attackable인가?
|
|
$onWar = $db->queryFirstField('SELECT you FROM diplomacy WHERE me = %i AND state=0 LIMIT 1', $nationID) !== null;
|
|
if($onWar){
|
|
$this->dipState = self::d전쟁;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function chooseDevelopTurn(bool &$cityFull):array{
|
|
$general = $this->general;
|
|
$city = $this->city;
|
|
$nation = $this->nation;
|
|
$env = $this->env;
|
|
|
|
$genType = $this->genType;
|
|
$leadership = $this->leadership;
|
|
$power = $this->power;
|
|
$intel = $this->intel;
|
|
|
|
$cityFull = false;
|
|
|
|
$develRate = [
|
|
'trust'=>$city['trust'],
|
|
'pop'=>$city['pop']/$city['pop2'],
|
|
'agri'=>$city['agri']/$city['agri2'],
|
|
'comm'=>$city['comm']/$city['comm2'],
|
|
'secu'=>$city['secu']/$city['secu2'],
|
|
'def'=>$city['def']/$city['def2'],
|
|
'wall'=>$city['wall']/$city['wall2'],
|
|
];
|
|
|
|
// 우선 선정
|
|
if($develRate['trust'] < 0.95 && Util::randBool($leadership / 40)){
|
|
return ['che_선정', null];
|
|
}
|
|
|
|
$commandList = [];
|
|
|
|
if($genType & self::t무장){
|
|
if($develRate['secu'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_치안강화', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_치안강화'] = $power / 3;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_치안강화'] /= 2;
|
|
}
|
|
}
|
|
}
|
|
if($develRate['def'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_수비강화', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_수비강화'] = $power / 3;
|
|
}
|
|
}
|
|
if($develRate['wall'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_성벽보수', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_성벽보수'] = $power / 3;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_성벽보수'] /= 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if($genType & self::t지장){
|
|
if($develRate['agri'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_농지개간', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_농지개간'] = $intel / 2;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_농지개간'] /= 2;
|
|
}
|
|
}
|
|
}
|
|
if($develRate['comm'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_상업투자', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_상업투자'] = $intel / 2;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_상업투자'] /= 2;
|
|
}
|
|
}
|
|
}
|
|
if(!TechLimit($env['startyear'], $env['year'], $nation['tech'])){
|
|
$commandObj = buildGeneralCommandClass('che_기술연구', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_기술연구'] = $intel / 4;
|
|
}
|
|
}
|
|
}
|
|
if($genType & self::t통솔장){
|
|
if($develRate['trust'] < 1){
|
|
$commandObj = buildGeneralCommandClass('che_주민선정', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_주민선정'] = $leadership / 2;
|
|
}
|
|
}
|
|
if($develRate['pop'] < 0.99){
|
|
$commandObj = buildGeneralCommandClass('che_정착장려', $general, $env);
|
|
if($commandObj->isRunnable()){
|
|
$commandList['che_정착장려'] = $leadership / 2;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_정착장려'] *= 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!$commandList){
|
|
$cityFull = true;
|
|
}
|
|
|
|
$genCount = $db->queryFirstField('SELECT count(no) FROM general');
|
|
$commandList['che_인재탐색'] = 500 / $genCount * 10;
|
|
if(in_array($city['front'], [1,3])){
|
|
$commandList['che_인재탐색'] /= 2;
|
|
}
|
|
|
|
$commandList['che_물자조달'] = (
|
|
(GameConst::$minNationalGold + GameConst::$minNationalRice + 10000) /
|
|
Util::valueFit($nation['gold'] + $nation['rice'], 1000)
|
|
) * 10;
|
|
|
|
return [Util::choiceRandomUsingWeight($commandList), null];
|
|
}
|
|
|
|
public function chooseRecruitCrewType():array{
|
|
$general = $this->getGeneralObj();
|
|
$city = $this->city;
|
|
$nation = $this->nation;
|
|
$env = $this->env;
|
|
|
|
$genType = $this->genType;
|
|
$leadership = $this->leadership;
|
|
$power = $this->power;
|
|
$intel = $this->intel;
|
|
|
|
$tech = $nation['tech'];
|
|
|
|
$db = DB::db();
|
|
|
|
$raw = $general->getRaw();
|
|
|
|
$dex = [
|
|
GameUnitConst::T_FOOTMAN=>sqrt($general->getVar('dex0') + 500),
|
|
GameUnitConst::T_ARCHER=>sqrt($general->getVar('dex10') + 500),
|
|
GameUnitConst::T_CAVALRY=>sqrt($general->getVar('dex20') + 500),
|
|
GameUnitConst::T_WIZARD=>sqrt($general->getVar('dex30') + 500),
|
|
GameUnitConst::T_SIEGE=>sqrt($general->getVar('dex40') + 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[GameConst::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[GameConst::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[GameConst::T_CAVALRY] * $crewtype->pickScore($tech);
|
|
$types[] = [$crewtype->id, $dex[GameConst::T_CAVALRY] + 500];
|
|
}
|
|
}
|
|
foreach($types as [$crewtype, $score]){
|
|
$typesAll[] = [$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[GameConst::T_WIZARD] * $crewtype->pickScore($tech);
|
|
$types[] = [$crewtype->id, $score];
|
|
}
|
|
}
|
|
foreach($types as [$crewtype, $score]){
|
|
$typesAll[] = [$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 -= $obj훈련->getCost()[0] * 2;
|
|
$gold -= $obj사기진작->getCost()[0] * 2;
|
|
|
|
$cost = getCost($type) * getTechCost($tech);
|
|
$cost = $general->onCalcDomestic('징병', 'cost', $cost);
|
|
|
|
$crew = intdiv($gold, $cost);
|
|
if($leader < $crew) { $crew = $leader; }
|
|
$arg = [
|
|
'crewType'=>$type,
|
|
'amountCrew'=>$crew
|
|
];
|
|
return ['che_징병', $arg];
|
|
}
|
|
|
|
public function getPayTurnCandidates(string $resName):array{
|
|
//무지장 포상 여부
|
|
if($npcCivilGenerals && $npcCivilGenerals[0]->$resName < $baseDevelCost){
|
|
if($remainResource[$resName] >= $baseDevelCost / 2){
|
|
$candidateCommand[] = [
|
|
['che_포상', [
|
|
'destGeneralID' => $npcCivilGenerals[0]->id,
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$baseDevelCost
|
|
]],
|
|
1
|
|
];
|
|
}
|
|
|
|
}
|
|
|
|
if($npcWarGenerals){
|
|
$targetNpcWarGeneral = Util::array_first($npcWarGenerals);
|
|
$crewtype = GameUnitConst::byID($targetNpcWarGeneral->crewtype);
|
|
//징병 2바퀴?
|
|
$reqMoney = $crewtype->costWithTech($tech, $targetNpcWarGeneral->leadership) * 2.2;
|
|
$enoughMoney = $reqMoney * 2;
|
|
|
|
if($targetNpcWarGeneral->$resName < $reqMoney){
|
|
//국고 1/5과 '충분한 금액'의 기하평균
|
|
$payAmount = sqrt($enoughMoney - $reqMoney, $remainResource[$resName] / 5);
|
|
if ($remainResource[$resName] >= $payAmount / 2) {
|
|
$candidateCommand[] = [
|
|
['che_포상', [
|
|
'destGeneralID' => $targetNpcWarGeneral->id,
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$payAmount
|
|
]],
|
|
2
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if($userGenerals){
|
|
$targetUserGeneral = Util::array_first($userGenerals);
|
|
$reqMoney = $crewtype->costWithTech($tech, $targetNpcWarGeneral->leadership) * 2*4*1.1;
|
|
if($env['year'] > $env['startyear'] + 5){
|
|
$reqMoney = max($reqMoney, 21000);
|
|
}
|
|
$enoughMoney = $reqMoney * 1.5;
|
|
|
|
if($targetNpcWarGeneral->$resName < $reqMoney){
|
|
//국고와 '충분한 금액'의 기하평균
|
|
$payAmount = sqrt($enoughMoney - $reqMoney, $remainResource[$resName]);
|
|
|
|
if($remainResource[$resName] >= $payAmount / 2){
|
|
$candidateCommand[] = [
|
|
['che_포상', [
|
|
'destGeneralID' => $targetNpcWarGeneral->id,
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$payAmount
|
|
]],
|
|
2
|
|
];
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if($npcCivilGenerals){
|
|
$targetNpcCivilGeneral = Util::array_last($npcCivilGenerals);
|
|
$tooMuchMoney = $baseDevelCost * 2.5;
|
|
$enoughMoney = $baseDevelCost * 1.5;
|
|
|
|
if($targetNpcCivilGeneral->$resName >= $tooMuchMoney){
|
|
$returnAmount = $npcCivilGenerals - $enoughMoney;
|
|
$candidateCommand[] = [
|
|
['che_몰수', [
|
|
'destGeneralID' => $targetNpcCivilGeneral->id,
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$returnAmount
|
|
]],
|
|
3
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
public function 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($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['pop2'];
|
|
$dev /= 50;
|
|
|
|
$nationCity['dev'] = $dev;
|
|
|
|
$nationCities[$cityID] = $nationCity;
|
|
|
|
if($nationCity['supply']){
|
|
$supplyCitiesID[] = $cityID;
|
|
if($nationCity['front']){
|
|
$frontCitiesID[] = $cityID;
|
|
if($nationCity['gen1']){
|
|
$frontImportantCitiesID[] = $cityID;
|
|
}
|
|
}
|
|
else{
|
|
$backupCitiesID[] = $cityID;
|
|
}
|
|
}
|
|
}
|
|
Util::shuffle_assoc($nationCities);
|
|
shuffle($frontCitiesID);
|
|
shuffle($supplyCitiesID);
|
|
|
|
$nationGenerals = [];
|
|
$lostGeneralsID = [];
|
|
|
|
$userGeneralsID = [];
|
|
$npcWarGeneralsID = [];
|
|
$npcCivilGeneralsID = [];
|
|
|
|
$commandList = [];
|
|
|
|
foreach($db->query('SELECT `no`, nation, city, npc, `gold`, `rice`, leader, `power`, intel, killturn, crew, train, atmos, `level`, troop FROM general WHERE nation = %i', $nationID) as $nationGeneral) {
|
|
$cityID = $nationGeneral['city'];
|
|
$generalID = $nationGeneral['no'];
|
|
|
|
if($generalID == $chiefID){
|
|
continue;
|
|
}
|
|
|
|
if(key_exists($cityID, $nationCities)){
|
|
$nationCities[$cityID]['generals'][] = $generalID;
|
|
if(!$nationCities[$cityID]['supply']){
|
|
$lostGeneralsID[] = $generalID;
|
|
}
|
|
}
|
|
else{
|
|
$lostGeneralsID[] = $generalID;
|
|
}
|
|
|
|
if($nationGeneral['npc']<2 && $nationGeneral['killturn'] >= 5){
|
|
$userGeneralsID[] = $generalID;
|
|
}
|
|
else if($nationGeneral['leader']>=40 && $nationGeneral['killturn'] >= 5){
|
|
$npcWarGeneralsID[] = $generalID;
|
|
}
|
|
else{
|
|
//삭턴이 몇 안남은 장수는 '내정장 npc'로 처리
|
|
$npcCivilGeneralsID[] = $generalID;
|
|
}
|
|
|
|
$nationGenerals[$generalID] = $nationGeneral;
|
|
}
|
|
Util::shuffle_assoc($nationGenerals);
|
|
shuffle($lostGeneralsID);
|
|
|
|
uasort($nationCities, function($lhs, $rhs){
|
|
//키 순서를 지키지 않지만, 원래부터 random order를 목표로 하므로 크게 신경쓰지 않는다.
|
|
return count($lhs['generals']) - count($rhs['generals']);
|
|
});
|
|
|
|
//타 도시에 있는 '유저장' 발령
|
|
foreach($lostGeneralsID as $lostGeneralID){
|
|
$lostGeneral = $nationGenerals[$lostGeneralID];
|
|
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'=>$lostGeneralID, 'destCityID'=>$selCityID]], 200];
|
|
}
|
|
}
|
|
|
|
$resName = Util::choiceRandom(['gold', 'rice']);
|
|
|
|
usort($userGeneralsID, function($lhs, $rhs) use ($nationGenerals, $resName){
|
|
return $nationGenerals[$lhs][$resName] - $nationGenerals[$rhs][$resName];
|
|
});
|
|
|
|
usort($npcWarGeneralsID, function($lhs, $rhs) use ($nationGenerals, $resName){
|
|
return $nationGenerals[$lhs][$resName] - $nationGenerals[$rhs][$resName];
|
|
});
|
|
|
|
usort($npcCivilGeneralsID, function($lhs, $rhs) use ($nationGenerals, $resName){
|
|
return $nationGenerals[$lhs][$resName] - $nationGenerals[$rhs][$resName];
|
|
});
|
|
|
|
$avgUserRes = 0;
|
|
foreach ($userGeneralsID as $id){
|
|
$avgUserRes += $nationGenerals[$id][$resName];
|
|
}
|
|
$avgUserRes /= max(1, count($userGeneralsID));
|
|
|
|
$avgNpcWarRes = 0;
|
|
foreach ($npcWarGeneralsID as $id){
|
|
$avgNpcWarRes += $nationGenerals[$id][$resName];
|
|
}
|
|
$avgNpcWarRes /= max(1, count($npcWarGeneralsID));
|
|
|
|
$avgNpcCivilRes = 0;
|
|
foreach ($npcCivilGeneralsID as $id){
|
|
$avgNpcCivilRes += $nationGenerals[$id][$resName];
|
|
}
|
|
$avgNpcCivilRes /= max(1, count($npcCivilGeneralsID));
|
|
|
|
//금쌀이 부족한 '유저장' 먼저 포상
|
|
while ($nation[$resName] > ($resName=='gold'?1:2)*3000 && $userGeneralsID) {
|
|
$isWarUser = null;
|
|
|
|
foreach($userGeneralsID as $userGeneralID){
|
|
$compUser = $nationGenerals[$userGeneralID];
|
|
if($compUser['leader'] >= 50){
|
|
$isWarUser = true;
|
|
break;
|
|
}
|
|
if(Util::randBool(0.2)){
|
|
$isWarUser = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if($isWarUser === null){
|
|
break;
|
|
}
|
|
|
|
$compRes = $compUser[$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[$resName] < 21000) || ($compUser[$resName] < 5000)){
|
|
if($work){
|
|
$amount = min(100, intdiv(($nation[$resName]-($resName=='rice'?(GameConst::$baserice):(GameConst::$basegold))), 3000)*10 + 10);
|
|
$commandList[] = [['che_포상', [
|
|
'destGeneralID'=>$userGeneralsID[0],
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$amount
|
|
]], 10]; // 금,쌀 1000단위 포상
|
|
}
|
|
else{
|
|
$amount = min(100, intdiv(($nation[$resName]-($resName=='rice'?(GameConst::$baserice):(GameConst::$basegold))), 5000)*10 + 10);
|
|
$commandList[] = [['che_포상', [
|
|
'destGeneralID'=>$userGeneralsID[0],
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$amount
|
|
]], 1]; // 금,쌀 1000단위 포상
|
|
}
|
|
|
|
}
|
|
break;
|
|
}
|
|
|
|
$minRes = $env['develcost'] * 24 * $tech;
|
|
|
|
if($nation[$resName] < ($resName=='gold'?1:2)*3000) { // 몰수
|
|
// 몰수 대상
|
|
$compUser = $userGeneralsID?$nationGenerals[end($userGeneralsID)]:null;
|
|
$compNpcWar = $npcWarGeneralsID?$nationGenerals[end($npcWarGeneralsID)]:null;
|
|
$compNpcCivil = $npcCivilGeneralsID?$nationGenerals[end($npcCivilGeneralsID)]:null;
|
|
|
|
$compUserRes = $compUser[$resName]??0;
|
|
$compNpcWarRes = $compNpcWar[$resName]*5??0;
|
|
$compNpcCivilRes = $compNpcCivil[$resName]*10??0;
|
|
|
|
[$compRes, $compGenID] = max(
|
|
[$compNpcCivilRes, $compNpcCivil['no']??null],
|
|
[$compNpcWarRes, $compNpcWar['no']??null],
|
|
[$compUserRes, $compUser['no']??null]
|
|
);
|
|
|
|
if($compGenID){
|
|
$targetGeneral = $nationGenerals[$compGenID];
|
|
if($compGenID === ($compNpcCivil['no']??null)){
|
|
$amount = intdiv($targetGeneral[$resName] - $minRes * 3);
|
|
}
|
|
else{
|
|
$amount = min(100, intdiv($targetGeneral[$resName], 5000)*10 + 10);
|
|
}
|
|
|
|
if($amount > 0){
|
|
$commandList[] = [['che_몰수', [
|
|
'destGeneralID'=>$compGenID,
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$amount
|
|
]], 3];
|
|
}
|
|
|
|
}
|
|
} else{ // 포상
|
|
$compNpcWar = $npcWarGeneralsID?$nationGenerals[$npcWarGeneralsID[0]]:null;
|
|
$compNpcCivil = null;
|
|
foreach($npcCivilGeneralsID??[] as $npcCivilID){
|
|
$npcCivil = $nationGenerals[$npcCivilID];
|
|
if($npcCivil['npc'] == 5){
|
|
continue;
|
|
}
|
|
$compNpcCivil = $npcCivil;
|
|
break;
|
|
}
|
|
|
|
if($compNpcWar && $compNpcWar[$resName] < 21000){
|
|
$amount = min(100, intdiv(($nation[$resName]-($resName=='rice'?(GameConst::$baserice):(GameConst::$basegold))), 5000)*10 + 10);
|
|
$commandList[] = [['che_몰수', [
|
|
'destGeneralID'=>$compNpcWar['no'],
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$amount
|
|
]], 3];
|
|
}
|
|
if($compNpcCivil && $compNpcCivil[$resName] < $minRes){
|
|
$amount = intdiv($minRes+99, 100);
|
|
$commandList[] = [['che_몰수', [
|
|
'destGeneralID'=>$compNpcCivil['no'],
|
|
'isGold'=>$resName=='gold',
|
|
'amount'=>$amount
|
|
]], 2];
|
|
}
|
|
}
|
|
|
|
//고립 도시 장수 발령
|
|
foreach($lostGeneralsID as $lostGeneralID){
|
|
$lostGeneral = $nationGenerals[$lostGeneralID];
|
|
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'=>$lostGeneralID,
|
|
'destCityID'=>$selCityID
|
|
]], sqrt(count($lostGeneralsID)) * 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 = $nationGenerals[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 = $nationGenerals[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
|
|
]], 5];
|
|
|
|
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['leader']){
|
|
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['leader']){
|
|
continue;
|
|
}
|
|
if (Util::randBool($targetCity['pop'] / $targetCity['pop2'])) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
$command = [['che_발령', [
|
|
'destGeneralID'=>$nationGeneral['no'],
|
|
'destCityID'=>$targetCity['city']
|
|
]], 5];
|
|
|
|
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();
|
|
$env = $this->env;
|
|
|
|
$db = DB::db();
|
|
|
|
|
|
// 오랑캐는 바로 임관
|
|
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',
|
|
Json::decode($general->getVar('nations'))
|
|
);
|
|
|
|
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 = ['destNationID'=>99];
|
|
}
|
|
break;
|
|
case '거병_견문': //거병이나 견문
|
|
// 초반이면서 능력이 좋은놈 위주로 1.4%확률로 거병
|
|
$prop = Util::randF() * (GameConst::$defaultStatNPCMax + GameConst::$chiefStatMin) / 2;
|
|
$ratio = ($general->getVar('leader') + $general->getVar('power') + $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];
|
|
}
|
|
|
|
protected function chooseEndOfNPCTurn($baseArmRice):array{
|
|
$general = $this->getGeneralObj();
|
|
$city = $this->city;
|
|
if($general->getVar('gold') + $general->getVar('rice') == 0){
|
|
return ['che_물자조달', null];
|
|
}
|
|
|
|
[$baseArmGold, $baseArmRice] = $this->getBaseArmCost();
|
|
|
|
if($this->dipState == self::d전쟁 &&
|
|
$general->getVar('killturn') > 2 &&
|
|
$general->getVar('gold') >= $baseArmGold / 3 &&
|
|
$general->getVar('rice') >= $baseArmRice &&
|
|
$general->getVar('crew') >= $general->getLeadership(false) / 3
|
|
){
|
|
//사망 직전 마지막 불꽃
|
|
$trainAndAtmos = $general->getVar('train') + $general->getVar('atmos');
|
|
if(
|
|
$trainAndAtmos >= 180 &&
|
|
$city['front'] >= 2 &&
|
|
$general->getVar('rice') >= $baseArmRice &&
|
|
$nation['war'] == 0
|
|
){
|
|
return $this->processAttack();
|
|
}
|
|
|
|
if(180 >= $trainAndAtmos && $trainAndAtmos >= 160){
|
|
if($general->getVar('train') < 80){
|
|
return ['che_훈련', null];
|
|
}
|
|
else{
|
|
return ['che_사기진작', null];
|
|
}
|
|
}
|
|
}
|
|
|
|
if($general->getVar('gold') >= $general->getVar('rice')){
|
|
return ['che_헌납', ['isGold'=>true, 'amount'=>10000]];
|
|
}
|
|
else{
|
|
return ['che_헌납', ['isGold'=>false, 'amount'=>10000]];
|
|
}
|
|
}
|
|
|
|
public function 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;
|
|
$power = $this->power;
|
|
$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;
|
|
$generalObj->setVar('killturn', 1);
|
|
}
|
|
else{
|
|
$command = 'che_집합'; //집합
|
|
$arg = [];
|
|
$generalObj->setVar('killturn', rand(70,75));
|
|
//NOTE: 부대 편성에 보여야 하므로 이것만 DB에 직접 접근함.
|
|
$db->update('general_turn', [
|
|
'action'=>'che_집합',
|
|
'arg'=>'{}'
|
|
], 'general_ID=%i AND turn_idx < 6', $generalID);
|
|
}
|
|
|
|
return [$command, $arg];
|
|
}
|
|
|
|
//특별 메세지 있는 경우 출력 하루 4번
|
|
$term = $env['turnterm'];
|
|
if($general->getVar('npcmsg') && Util::randBool($term / (6*60))) {
|
|
$src = new MessageTarget(
|
|
$generalID,
|
|
$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();
|
|
}
|
|
|
|
$baseDevelCost = $env['develcost'] * 12;
|
|
|
|
$techCost = getTechCost($nation['tech']);
|
|
|
|
if($general->getVar('mode') < 2) {
|
|
$general->setVar('mode', 2);
|
|
}
|
|
|
|
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'=>($general->getVar('rice') - $baseArmRice) / 2]];
|
|
}
|
|
else if(!($genType & self::t통솔장)){
|
|
return ['che_헌납', ['isGold'=>false, 'amount'=>$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'=> $general->getVar('gold') - $baseArmGold]];
|
|
}
|
|
if($general->getVar('gold') < $baseArmGold && $general->getVar('gold') >= $baseArmRice * 3){
|
|
return ['che_군량매매', ['buyRice'=>false, 'amount'=> $general->getVar('rice') - $baseArmRice]];
|
|
}
|
|
}
|
|
else{
|
|
if($general->getVar('rice') < $baseDevelCost && $general->getVar('gold') >= $baseDevelCost * 3){
|
|
return ['che_군량매매', ['buyRice'=>true, 'amount'=> $general->getVar('gold') - $baseDevelCost]];
|
|
}
|
|
if($general->getVar('gold') < $baseDevelCost && $general->getVar('gold') >= $baseDevelCost * 3){
|
|
return ['che_군량매매', ['buyRice'=>false, 'amount'=> $general->getVar('rice') - $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)/(pop2/10+agri2+comm2+secu2+def2+wall2)*100 as dev, gen2 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['gen2']){
|
|
$score *= 1.3;
|
|
}
|
|
$moveCities[$moveCityID] = $score;
|
|
|
|
}
|
|
return ['che_NPC능동', [
|
|
'optionText'=>'순간이동',
|
|
'destCityID'=>Util::choiceRandomUsingWeight($moveCities),
|
|
]];
|
|
}
|
|
|
|
return $developTurn;
|
|
|
|
}
|
|
|
|
protected function getBaseArmCost(){
|
|
//총 통솔의 절반을 징병하는 것을 기준으로 함
|
|
$general = $this->getGeneralObj();
|
|
$tech = $this->nation['tech'];
|
|
$crewType = GameUnitConst::byID($general->getVar('crewtype'));
|
|
$baseArmCost = $crewType->costWithTech(
|
|
$tech,
|
|
$general->getLeadership(false) / 2
|
|
);//기본 병종
|
|
$baseArmCost = $general->onCalcDomestic('징병', 'cost', $baseArmCost);
|
|
$baseArmRice = $general->getLeadership(false) / 2 * $unit->rice * getTechCost($tech);
|
|
if($general->getVar('deathcrew') > 500 && $general->getVar('killcrew') > 500){
|
|
$baseArmRice *= $general->getVar('killcrew') / $general->getVar('deathcrew');
|
|
}
|
|
|
|
return [$baseArmCost, $baseArmRice];
|
|
}
|
|
|
|
protected function processWar():?array{
|
|
$general = $this->getGeneralObj();
|
|
if(!$this->attackable && in_array($this->dipState, [self::d평화, 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();
|
|
|
|
|
|
$baseDevelCost = $env['develcost'] * 12;
|
|
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(leader) FROM general WHERE nation = %i AND city = %i AND leader > 40', $nationID, $cityID);
|
|
if(
|
|
$city['pop'] > 30000 + $general->getLeadership(false) * 100 * 1.3 &&
|
|
Util::bool(($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, gen1 FROM city WHERE nation=%i AND front > 0 AND supply = 1');
|
|
|
|
if(!$frontCities){
|
|
//접경이 아직 없음
|
|
return null;
|
|
}
|
|
|
|
$nearCities = [];
|
|
$attackableCities = [];
|
|
foreach($frontCities as $frontCity){
|
|
$frontCityID = $frontCity['city'];
|
|
|
|
$score = 0.2;
|
|
if($frontCity['front'] > 1 && $frontCity['gen1']){
|
|
$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;
|
|
|
|
}
|
|
}
|
|
|
|
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('rice') < 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 processAttack():array{
|
|
$general = $this->getGeneralObj();
|
|
$city = $this->city;
|
|
$nation = $this->nation;
|
|
|
|
$cityID = $city['city'];
|
|
$nationID = $nation['nation'];
|
|
|
|
$db = DB::db();
|
|
|
|
if($city['front'] <= 1){
|
|
throw new \RuntimeException('출병 불가');
|
|
}
|
|
|
|
$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
|
|
);
|
|
|
|
return ['che_출병', ['destCityID'=>Util::choiceRandom($attackableCities)]];
|
|
}
|
|
|
|
protected function proceessNeutralLordTurn():array{
|
|
$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(array_keys(getNationTypeList()));
|
|
$nationColor = Util::choiceRandom(array_keys(GetNationColors()));
|
|
return ['che_건국', [
|
|
'nationName'=>"㉿".mb_substr($general->getName(), 1),
|
|
'nationType'=>$nationType,
|
|
'nationColor'=>$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(CityConst::byID($general->getVar('city'))->path as $nearCityID){
|
|
if(CityConst::byID($nearCityID)->level < 4){
|
|
$targetCity[$nearCityID] = 0.5;
|
|
}
|
|
else if(!key_exists($nearCities, $occupiedCities)){
|
|
$targetCity[$nearCityID] = 2;
|
|
}
|
|
else{
|
|
$targetCity[$nearCityID] = 0;
|
|
}
|
|
|
|
$nearCities = searchDistance($nearCityID, 4, true);
|
|
foreach($nearCities 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(pop2)*100 as pop,(sum(agri)+sum(comm)+sum(secu)+sum(def)+sum(wall))/(sum(agri2)+sum(comm2)+sum(secu2)+sum(def2)+sum(wall2))*100 as all from city where nation=%i',
|
|
$nationID
|
|
);
|
|
return $this->devRate;
|
|
}
|
|
|
|
protected function findWarTarget():?int{
|
|
$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'] + $devRate['all']) / 2 < 80){
|
|
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 = [];
|
|
|
|
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[$cheif['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 leader 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 leader DESC LIMIT 1',
|
|
$nationID,
|
|
$maxBelong
|
|
);
|
|
if($candChiefHead){
|
|
$chiefCandidate[11] = $candChiefHead;
|
|
$promoted[$candChiefHead] = 11;
|
|
}
|
|
}
|
|
|
|
if($minChiefLevel < 11){
|
|
//무장 수뇌 후보
|
|
$candChiefPower = $db->queryFirstColumn(
|
|
'SELECT no FROM general WHERE nation = %i AND power >= %i AND level = 1 AND belong >= %i ORDER BY power 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
|
|
);
|
|
//무력, 지력이 모두 높은 장수를 고려하여..
|
|
|
|
$iterCandChiefPower = new \ArrayIterator($candChiefPower);
|
|
$iterCandChiefIntel = new \ArrayIterator($candChiefIntel);
|
|
|
|
foreach(range(10, $minChiefLevel, -1) as $cheifLevel){
|
|
if(key_exists($cheifLevel, $chiefCandidate)){
|
|
continue;
|
|
}
|
|
|
|
/** @var \ArrayIterator $iterCurrentType */
|
|
$iterCurrentType = ($cheifLevel % 2 == 0)?$iterCandChiefPower:$iterCandChiefIntel;
|
|
$candidate = $iterCurrentType->current();
|
|
|
|
while(key_exist($candidate, $promoted)){
|
|
$iterCurrentType->next();
|
|
}
|
|
|
|
$chiefCandidate[$cheifLevel] = $candidate;
|
|
$promoted[$candidate] = $cheifLevel;
|
|
}
|
|
|
|
foreach($chiefCandidate as $chiefLevel=>$chiefID){
|
|
$db->update('general', [
|
|
'level'=>$cheifLevel
|
|
], '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'] + $devRate['all']) / 2;
|
|
|
|
if($avg > 95) $rate = 25;
|
|
elseif($avg > 70) $rate = 20;
|
|
elseif($avg > 50) $rate = 15;
|
|
else $rate = 10;
|
|
|
|
$db->update('nation', [
|
|
'war'=>0,
|
|
'rate'=>$rate
|
|
]);
|
|
return $rate;
|
|
}
|
|
}
|
|
|
|
protected function calcGoldBillRate():int{
|
|
$db = DB::db();
|
|
$nation = $this->nation;
|
|
$env = $this->env;
|
|
|
|
$nationID = $nation['nation'];
|
|
|
|
$incomeList = getGoldIncome($nation['nation'], $nation['rate'], $env['gold_rate'], $env['type']);
|
|
$income = $gold + $incomeList[0] + $incomeList[1];
|
|
$outcome = getGoldOutcome($nation['nation'], 100); // 100%의 지급량
|
|
$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 caclRiceBillRate():int{
|
|
$db = DB::db();
|
|
$nation = $this->nation;
|
|
$env = $this->env;
|
|
|
|
$nationID = $nation['nation'];
|
|
|
|
$incomeList = getRiceIncome($nation['nation'], $nation['rate'], $env['gold_rate'], $env['type']);
|
|
$income = $rice + $incomeList[0] + $incomeList[1];
|
|
$outcome = getRiceOutcome($nation['nation'], 100); // 100%의 지급량
|
|
$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;
|
|
}
|
|
} |