From 8519481f968df389b460bf929e893b094c51fc46 Mon Sep 17 00:00:00 2001 From: hide_d Date: Wed, 8 Apr 2020 03:30:40 +0900 Subject: [PATCH] =?UTF-8?q?AI=20=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/sammo/Command/BaseCommand.php | 5 + hwe/sammo/Command/Nation/che_발령.php | 1 + hwe/sammo/General.php | 148 +++++++++++++++++-- hwe/sammo/GeneralAI.php | 204 ++++++++++++++++++++++---- hwe/sammo/LazyVarUpdater.php | 19 +-- 5 files changed, 330 insertions(+), 47 deletions(-) diff --git a/hwe/sammo/Command/BaseCommand.php b/hwe/sammo/Command/BaseCommand.php index 0a08f96d..c125c632 100644 --- a/hwe/sammo/Command/BaseCommand.php +++ b/hwe/sammo/Command/BaseCommand.php @@ -213,6 +213,11 @@ abstract class BaseCommand{ abstract protected function init(); abstract protected function argTest():bool; + + public function getArg():?array{ + return $this->arg; + } + public function getBrief():string{ return static::getName(); diff --git a/hwe/sammo/Command/Nation/che_발령.php b/hwe/sammo/Command/Nation/che_발령.php index 3f02c3c0..762872f2 100644 --- a/hwe/sammo/Command/Nation/che_발령.php +++ b/hwe/sammo/Command/Nation/che_발령.php @@ -131,6 +131,7 @@ class che_발령 extends Command\NationCommand{ $josaUl = JosaUtil::pick($destGeneralName, '을'); $josaRo = JosaUtil::pick($destCityName, '로'); $destGeneral->getLogger()->pushGeneralActionLog("{$generalName}에 의해 {$destCityName}{$josaRo} 발령됐습니다. <1>$date"); + $destGeneral->setAuxVar('last발령', $general->getTurnTime()); $logger->pushGeneralActionLog("{$destGeneralName}{$josaUl} {$destCityName}{$josaRo} 발령했습니다. <1>$date"); $this->setResultTurn(new LastTurn(static::getName(), $this->arg)); diff --git a/hwe/sammo/General.php b/hwe/sammo/General.php index 6eb86535..3210c7b8 100644 --- a/hwe/sammo/General.php +++ b/hwe/sammo/General.php @@ -269,29 +269,76 @@ class General implements iAction{ } function calcRecentWarTurn(int $turnTerm):int{ + $cacheKey = "recent_war_turn_{$turnTerm}"; + if(key_exists($cacheKey, $this->calcCache)){ + return $this->calcCache[$cacheKey]; + } if(!$this->getVar('recent_war')){ - return 12*1000; + $result = 12*1000; + $this->calcCache[$cacheKey] = $result; + return $result; } $recwar = new \DateTimeImmutable($this->getVar('recent_war')); $turnNow = new \DateTimeImmutable($this->getVar('turntime')); $secDiff = TimeUtil::DateIntervalToSeconds($recwar->diff($turnNow)); if($secDiff <= 0){ + $this->calcCache[$cacheKey] = 0; return 0; } - return intdiv($secDiff, 60 * $turnTerm); + $result = intdiv($secDiff, 60 * $turnTerm); + $this->calcCache[$cacheKey] = $result; + return $result; } - function getReservedTurn(int $turnIdx, array $env):?GeneralCommand{ + function getReservedTurn(int $turnIdx, array $env):GeneralCommand{ $db = DB::db(); $rawCmd = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id = %i AND turn_idx = %i', $this->getID(), $turnIdx); if(!$rawCmd){ - return null; + return buildGeneralCommandClass(null, $this, $env); } return buildGeneralCommandClass($rawCmd['action'], $this, $env, $rawCmd['arg']); } + /** + * @param General[] $generalList + * @param int $turnIdxFrom [$turnIdxFrom, $turnIdxTo) + * @param int $turnIdxTo [$turnIdxFrom, $turnIdxTo) + * @param array $env + * @return GeneralCommand[] + */ + public function getReservedTurnList(int $turnIdxFrom, int $turnIdxTo, array $env){ + if($turnIdxFrom < 0 || $turnIdxFrom >= GameConst::$maxTurn){ + throw new \OutOfRangeException('$turnIdxFrom 범위 초과'.$turnIdxFrom); + } + + if($turnIdxTo <= $turnIdxFrom || GameConst::$maxTurn < $turnIdxTo){ + throw new \OutOfRangeException('$turnIdxTo 범위 초과'.$turnIdxTo); + } + + $db = DB::db(); + + $generalID = $this->getID(); + + $result = []; + + $rawCmds = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id = %i AND %i <= turn_idx AND turn_idx < %i ORDER BY turn_idx ASC', $generalID, $turnIdxFrom, $turnIdxTo); + + if(!$rawCmds){ + foreach(range($turnIdxFrom, $turnIdxTo-1) as $turnIdx){ + $result[$turnIdx] = buildGeneralCommandClass(null, $this, $env); + } + return $result; + } + + foreach($rawCmds as $turnIdx=>$rawCmd){ + $result[$turnIdx] = buildGeneralCommandClass($rawCmd['action'], $this, $env, $rawCmd['arg']); + } + + return $result; + } + /** * 장수의 스탯을 계산해옴 * @@ -398,13 +445,12 @@ class General implements iAction{ } function updateVar(string $key, $value){ - if(!key_exists($key, $this->updatedVar)){ - $this->updatedVar[$key] = $this->raw[$key]; - $this->calcCache = []; - } if($this->raw[$key] === $value){ return; } + if(!key_exists($key, $this->updatedVar)){ + $this->updatedVar[$key] = true; + } $this->raw[$key] = $value; $this->calcCache = []; } @@ -413,8 +459,6 @@ class General implements iAction{ * @param \MeekroDB $db */ function kill($db){ - $gameStor = KVStorage::getStorage($db, 'game_env'); - $generalID = $this->getID(); $logger = $this->getLogger(); @@ -908,4 +952,88 @@ class General implements iAction{ return $general; } + + /** + * @param General[] $generalList + * @param int $turnIdx + * @param array $env + * @return GeneralCommand[] + */ + static public function getReservedTurnByGeneralList(array $generalList, int $turnIdx, array $env){ + if(!$generalList){ + return []; + } + + $generalIDList = array_map(function(General $general){ + return $general->getID(); + }, $generalList); + + $db = DB::db(); + $result = []; + $rawCmds = Util::convertArrayToDict($db->queryFirstRow('SELECT * FROM general_turn WHERE general_id IN %i AND turn_idx = %i', $generalIDList, $turnIdx), 'general_id'); + foreach($generalList as $general){ + $generalID = $general->getID(); + if(!key_exists($generalID, $rawCmds)){ + $result[$generalID] = buildGeneralCommandClass(null, $general, $env); + continue; + } + $rawCmd = $rawCmds[$generalID]; + $result[$generalID] = buildGeneralCommandClass($rawCmd['action'], $general, $env, $rawCmd['arg']); + } + return $result; + } + + /** + * @param General[] $generalList + * @param int $turnIdxFrom [$turnIdxFrom, $turnIdxTo) + * @param int $turnIdxTo [$turnIdxFrom, $turnIdxTo) + * @param array $env + * @return GeneralCommand[][] + */ + static public function getReservedTurnListByGeneralList(array $generalList, int $turnIdxFrom, int $turnIdxTo, array $env){ + if(!$generalList){ + return []; + } + + if($turnIdxFrom < 0 || $turnIdxFrom >= GameConst::$maxTurn){ + throw new \OutOfRangeException('$turnIdxFrom 범위 초과'.$turnIdxFrom); + } + + if($turnIdxTo <= $turnIdxFrom || GameConst::$maxTurn < $turnIdxTo){ + throw new \OutOfRangeException('$turnIdxTo 범위 초과'.$turnIdxTo); + } + + $generalIDList = array_map(function(General $general){ + return $general->getID(); + }, $generalList); + + $db = DB::db(); + + $rawCmds = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id IN %i AND %i <= turn_idx AND turn_idx < %i ORDER BY general_id ASC, turn_idx ASC', $generalIDList, $turnIdxFrom, $turnIdxTo); + $orderedRawCmds = []; + foreach($rawCmds as $rawCmd){ + $generalID = $rawCmd['general_id']; + $turnIdx = $rawCmd['turn_idx']; + if(!key_exists($generalID, $orderedRawCmds)){ + $orderedRawCmds[$generalID] = []; + } + $orderedRawCmds[$generalID][$turnIdx] = $rawCmd; + } + + $result = []; + foreach($generalList as $general){ + $generalID = $general->getID(); + $result[$generalID] = []; + if(!key_exists($generalID, $orderedRawCmds)){ + foreach(range($turnIdxFrom, $turnIdxTo-1) as $turnIdx){ + $result[$generalID][$turnIdx] = buildGeneralCommandClass(null, $general, $env); + } + continue; + } + foreach($orderedRawCmds[$generalID] as $turnIdx=>$rawCmd){ + $result[$generalID][$turnIdx] = buildGeneralCommandClass($rawCmd['action'], $general, $env, $rawCmd['arg']); + } + } + return $result; + } } \ No newline at end of file diff --git a/hwe/sammo/GeneralAI.php b/hwe/sammo/GeneralAI.php index 5eac503e..b01050a6 100644 --- a/hwe/sammo/GeneralAI.php +++ b/hwe/sammo/GeneralAI.php @@ -61,14 +61,14 @@ class GeneralAI protected $npcWarGenerals; /** @var General[] */ protected $userGenerals; - /** @var General[] */ + /** @var General[] (전쟁 가능한 기간만 따져서) 최근 12턴 내에 전투를 수행한, 또는 전투를 수행할 수 있는 유저장 목록*/ protected $userWarGenerals; /** @var General[] */ protected $userCivilGenerals; /** @var General[] */ protected $lostGenerals; - /** @var General[] */ + /** @var General[] 이번 턴에 '집합'하는 부대장 목록 */ protected $troopLeaders; const t무장 = 1; @@ -237,6 +237,9 @@ class GeneralAI protected function do부대전방발령(LastTurn $lastTurn): ?NationCommand { + if(!$this->frontCities){ + return null; + } $this->calcWarRoute(); $troopCandidate = []; @@ -320,6 +323,10 @@ class GeneralAI protected function do부대후방발령(LastTurn $lastTurn): ?NationCommand { + if(!$this->frontCities){ + return null; + } + $troopCandidate = []; foreach($this->troopLeaders as $troopLeader){ $leaderID = $troopLeader->getID(); @@ -392,7 +399,7 @@ class GeneralAI $generalCadidates = []; $db = DB::db(); - $chiefTurnTime = new \DateTimeImmutable($this->general->getVar('turntime')); + $chiefTurnTime = new \DateTimeImmutable($this->general->getTurnTime()); foreach($this->userWarGenerals as $userGeneral){ $generalID = $userGeneral->getID(); @@ -403,10 +410,18 @@ class GeneralAI if(!key_exists($generalID, $this->supplyCities)){ continue; } - if($userGeneral->getVar('troop') === 0){ + $troopLeaderID = $userGeneral->getVar('troop'); + if(!$troopLeaderID || !key_exists($troopLeaderID, $this->troopLeaders)){ continue; } - if($userGeneral->getVar('troop') === $userGeneral->getID()){ + if($troopLeaderID === $userGeneral->getID()){ + continue; + } + $troopLeader = $this->nationGenerals[$troopLeaderID]; + if($troopLeader->getCityID() !== $userGeneral->getCityID()){ + continue; + } + if(!key_exists($troopLeader->getCityID(), $this->supplyCities)){ continue; } if($city['pop'] / $city['pop_max'] >= $this->nationPolicy->safeRecruitCityPopulationRatio){ @@ -416,9 +431,9 @@ class GeneralAI continue; } - $generalTurnTime = new \DateTimeImmutable($userGeneral->getVar('turntime')); + $generalTurnTime = new \DateTimeImmutable($userGeneral->getTurnTime()); $troopLeader = $this->nationGenerals[$userGeneral->getVar('troop')]; - $troopTurnTime = new \DateTimeImmutable($troopLeader->getVar('turntime')); + $troopTurnTime = new \DateTimeImmutable($troopLeader->getTurnTime()); if($chiefTurnTime < $generalTurnTime && $generalTurnTime < $troopTurnTime){ $generalCadidates[$generalID] = $userGeneral; @@ -429,16 +444,16 @@ class GeneralAI 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){ + $turnList = General::getReservedTurnByGeneralList($generalCadidates, 0, $this->env); + $generalCadidates = array_filter($generalCadidates, function(General $general)use($turnList){ $generalID = $general->getID(); - if(in_array($rawTurn[$generalID]??null, ['che_징병', 'che_모병'])){ + if($turnList[$generalID] instanceof Command\General\che_징병){ return true; } else{ return false; } - }, ARRAY_FILTER_USE_KEY); + }); if(count($this->supplyCities) == 1){ return null; @@ -548,12 +563,30 @@ class GeneralAI protected function do유저장구출발령(LastTurn $lastTurn): ?NationCommand { + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } + //고립 도시 장수 발령 $args = []; foreach ($this->lostGenerals as $lostGeneral) { if ($lostGeneral->getVar('npc') >= 2) { continue; } + + $troopID = $lostGeneral->getVar('troop'); + if($troopID && key_exists($troopID, $this->troopLeaders)){ + $troopLeader = $this->troopLeaders[$troopID]; + + if( + key_exists($troopLeader->getCityID(), $this->supplyCities) && + $this->troopLeaders[$troopID]->getTurnTime() < $lostGeneral->getTurnTime() + ){ + //이미 탈출 가능한 부대를 탔다 + continue; + } + } + if (in_array($this->dipState, [self::d직전, self::d전쟁]) && count($this->frontCities) > 2) { $selCity = Util::choiceRandom($this->frontCities); } else { @@ -577,6 +610,40 @@ class GeneralAI protected function do유저장전방발령(LastTurn $lastTurn): ?NationCommand { + if(!$this->frontCities){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } + + + + $generalCandidates = []; + foreach($this->userWarGenerals as $userGeneral){ + $generalID = $userGeneral->getID(); + $cityID = $userGeneral->getCityID(); + if(key_exists($cityID, $this->frontCities)){ + continue; + } + if($userGeneral->getVar('crew') < $this->nationPolicy->minWarCrew){ + continue; + } + if($userGeneral->getVar('troop')){ + continue; + } + + $train = $userGeneral->getVar('train'); + $atmos = $userGeneral->getVar('atmos'); + + if(max($train, $atmos) < $this->nationPolicy->properWarTrainAtmos){ + continue; + } + + $generalCandidates[$generalID] = $userGeneral; + } + + return null; } @@ -591,6 +658,12 @@ class GeneralAI protected function doNPC후방발령(LastTurn $lastTurn): ?NationCommand { + if(!$this->frontCities){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } return null; } @@ -622,6 +695,12 @@ class GeneralAI protected function doNPC전방발령(LastTurn $lastTurn): ?NationCommand { + if(!$this->frontCities){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } return null; } @@ -1101,7 +1180,7 @@ class GeneralAI 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')]; + $nationStor->last천도Trial = [$general->getVar('level'), $general->getTurnTime()]; return $cmd; } } @@ -1111,7 +1190,7 @@ class GeneralAI [$lastTrialLevel, $lastTrialTurnTime] = $lastTrial; $timeDiffSeconds = TimeUtil::DateIntervalToSeconds( date_create_immutable($lastTrialTurnTime)->diff( - date_create_immutable($general->getVar('turntime')) + date_create_immutable($general->getTurnTime()) ) ); if($timeDiffSeconds < $turnTerm * 30 && $lastTrialLevel !== $general->getVar('level')){ //0.5Turn @@ -1222,7 +1301,7 @@ class GeneralAI } - $nationStor->last천도Trial = [$general->getVar('level'), $general->getVar('turntime')]; + $nationStor->last천도Trial = [$general->getVar('level'), $general->getTurnTime()]; return $cmd; } @@ -1540,6 +1619,13 @@ class GeneralAI protected function do징병(GeneralCommand $reservedCommand): ?GeneralCommand { + if (!$this->attackable){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } + $general = $this->getGeneralObj(); $nation = $this->nation; $env = $this->env; @@ -1694,6 +1780,13 @@ class GeneralAI protected function do출병(GeneralCommand $reservedCommand): ?GeneralCommand { + if (!$this->attackable) { + return null; + } + if($this->dipState !== self::d전쟁){ + return null; + } + $general = $this->getGeneralObj(); $city = $this->city; $nation = $this->nation; @@ -1745,11 +1838,25 @@ class GeneralAI protected function do후방워프(GeneralCommand $reservedCommand): ?GeneralCommand { + if(!$this->attackable){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } + return null; } protected function do전방워프(GeneralCommand $reservedCommand): ?GeneralCommand { + if(!$this->attackable){ + return null; + } + if (in_array($this->dipState, [self::d평화, self::d선포])) { + return null; + } + return null; } @@ -1761,12 +1868,28 @@ class GeneralAI protected function do귀환(GeneralCommand $reservedCommand): ?GeneralCommand { - return null; + $cityID = $this->general->getCityID(); + if(key_exists($cityID, $this->supplyCities)){ + return null; + } + + $cmd = buildGeneralCommandClass('che_귀환', $this->general, $this->env); + if(!$cmd->isRunnable()){ + return null; + } + return $cmd; } protected function do집합(GeneralCommand $reservedCommand): ?GeneralCommand { - return null; + $cmd = $reservedCommand; + $general = $this->general; + $generalID = $general->getID(); + + $cmd = buildGeneralCommandClass('che_집합', $general, $this->env); + _setGeneralCommand($generalID, range(0, GameConst::$maxTurn - 1), $cmd->getRawClassName(), $cmd->getArg(), $cmd->getBrief()); + + return $cmd; } protected function do방랑군이동(GeneralCommand $reservedCommand): ?GeneralCommand @@ -2027,7 +2150,20 @@ class GeneralAI $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){ + $nationGenerals = General::createGeneralObjListFromDB($generalIDList, null, 2); + + $lastWar = \PHP_INT_MAX; + foreach($nationGenerals as $nationGeneral){ + $recentWar = $nationGeneral->calcRecentWarTurn($this->env['turnterm']); + if($recentWar >= $nationGeneral->getVar('belong') * 12){ + //임관전 전투는 제외 + continue; + } + + $lastWar = min($lastWar, $recentWar); + } + + foreach($nationGenerals as $nationGeneral){ $generalID = $nationGeneral->getID(); $cityID = $nationGeneral->getCityID(); $npcType = $nationGeneral->getVar('npc'); @@ -2045,13 +2181,25 @@ class GeneralAI //부대장임 $troopLeaders[$generalID] = $nationGeneral; } + else if($nationGeneral->getVar('troop') === $generalID && $nationGeneral->getReservedTurn(0, $this->env)->getName() === 'che_집합'){ + //비 NPC부대장임 + $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){ + if($nationGeneral->calcRecentWarTurn($this->env['turnterm']) <= $lastWar + 12){ + $userWarGenerals[$generalID] = $nationGeneral; + } + else if( + $this->dipState !== self::d평화 && + $nationGeneral->getVar('crew') >= $this->nationPolicy->minWarCrew && + $nationGeneral->getVar('train') >= $nationGeneral->getVar('defence_train') && + $nationGeneral->getVar('atmos') >= $nationGeneral->getVar('defence_train') + ){ $userWarGenerals[$generalID] = $nationGeneral; } else{ @@ -2064,6 +2212,7 @@ class GeneralAI } } + $this->nationGenerals = $nationGenerals; $this->userGenerals = $userGenerals; $this->userCivilGenerals = $userCivilGenerals; $this->userWarGenerals = $userWarGenerals; @@ -2071,7 +2220,6 @@ class GeneralAI $this->npcCivilGenerals = $npcCivilGenerals; $this->npcWarGenerals = $npcWarGenerals; $this->troopLeaders = $troopLeaders; - } @@ -2134,17 +2282,21 @@ class GeneralAI } public function chooseGeneralTurn(GeneralCommand $reservedCommand): GeneralCommand{ + $general = $this->general; + $npcType = $general->getVar('npc'); + $nationID = $general->getNationID(); + + if($npcType == 5){ + return $this->do집합($reservedCommand); + } + 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){ + + + if($npcType == 2 && $nationID == 0){ $result = $this->do거병($reservedCommand); if($result !== null){ return $result; diff --git a/hwe/sammo/LazyVarUpdater.php b/hwe/sammo/LazyVarUpdater.php index 5808d910..ba65aa41 100644 --- a/hwe/sammo/LazyVarUpdater.php +++ b/hwe/sammo/LazyVarUpdater.php @@ -36,7 +36,7 @@ trait LazyVarUpdater{ if(!key_exists('aux', $this->raw)){ throw new \RuntimeException('aux is not set'); } - $this->raw['auxVar'] = Json::decode($this->raw['aux']); + $this->raw['auxVar'] = Json::decode($this->raw['aux']??'{}'); } } @@ -52,16 +52,13 @@ trait LazyVarUpdater{ function setAuxVar(string $key, $var){ $oldVar = $this->getAuxVar($key); - if($var === null){ - if($oldVar === null){ - return; - } - unset($this->auxVar[$key]); - $this->auxUpdated = true; + if($oldVar === $var){ return; } - if($oldVar === $var){ + if($var === null){ + unset($this->auxVar[$key]); + $this->auxUpdated = true; return; } $this->raw['auxVar'][$key] = $var; @@ -69,12 +66,12 @@ trait LazyVarUpdater{ } function updateVar(string $key, $value){ - if(!key_exists($key, $this->updatedVar)){ - $this->updatedVar[$key] = $this->raw[$key]; - } if($this->raw[$key] === $value){ return; } + if(!key_exists($key, $this->updatedVar)){ + $this->updatedVar[$key] = true; + } $this->raw[$key] = $value; }