diff --git a/hwe/func.php b/hwe/func.php index 926fbe91..abd7462f 100644 --- a/hwe/func.php +++ b/hwe/func.php @@ -1838,6 +1838,76 @@ function searchDistance(int $from, int $maxDist=99, bool $distForm = false) { } } +/** + * $from 으로 지정한 도시부터 $to 도시까지의 최단 거리를 계산해 줌 + * @param $from 기준 도시 코드 + * @param $to 대상 도시 코드 + * @param array|null $linkNationList 경로에 해당하는 국가 리스트, null인 경우 제한 없음 + * @return null|int 거리. + */ +function calcCityDistance(int $from, int $to, ?array $linkNationList):?int{ + $queue = new \SplQueue(); + + $cities = []; + + if($linkNationList === []){ + return null; + } + + if($linkNationList !== null){ + $db = DB::db(); + //TODO: 도시-국가 캐싱이 있으면 쓸모 있지 않을까 + $allowedCityList = []; + foreach($db->queryAllLists( + 'SELECT city FROM city WHERE nation IN %li', + $linkNationList + ) as [$cityID, $nationID]){ + $allowedCityList[$cityID] = $nationID; + } + + + } + else{ + $allowedCityList = CityConst::all(); + } + + if(!key_exists($to, $allowedCityList)){ + return null; + } + + if($from === $to){ + return 0; + } + + $queue->enqueue([$from, 0]); + + while(!$queue->isEmpty()){ + list($cityID, $dist) = $queue->dequeue(); + if(key_exists($cityID, $cities)){ + continue; + } + + $cities[$cityID] = $dist; + + if($cityID === $to){ + return $dist; + } + + foreach(array_keys(CityConst::byID($cityID)->path) as $connCityID){ + if(!key_exists($connCityID, $allowedCityList)){ + continue; + } + if(key_exists($connCityID, $cities)){ + continue; + } + $queue->enqueue([$connCityID, $dist+1]); + + } + } + + //길이 없음 + return null; +} /** * $from 으로 지정한 도시의 인접 도시와 $to 도시의 최단 거리를 계산해 줌 * @param $from 기준 도시 코드 diff --git a/hwe/func_gamerule.php b/hwe/func_gamerule.php index 21e6dad5..6e41002a 100644 --- a/hwe/func_gamerule.php +++ b/hwe/func_gamerule.php @@ -230,7 +230,6 @@ function updateQuaterly() { //천도 제한 해제, 관직 변경 제한 해제 $db->update('nation', [ - 'capset'=>0, 'l12set'=>0, 'l11set'=>0, 'l10set'=>0, diff --git a/hwe/sammo/Command/General/che_출병.php b/hwe/sammo/Command/General/che_출병.php index 77c20122..0f1e4b56 100644 --- a/hwe/sammo/Command/General/che_출병.php +++ b/hwe/sammo/Command/General/che_출병.php @@ -61,7 +61,7 @@ class che_출병 extends Command\GeneralCommand{ ConstraintHelper::ReqGeneralCrew(), ConstraintHelper::ReqGeneralRice($reqRice), ConstraintHelper::AllowWar(), - ConstraintHelper::HasRoute(), + ConstraintHelper::HasRouteWithEnemy(), ]; } diff --git a/hwe/sammo/Command/Nation/che_천도.php b/hwe/sammo/Command/Nation/che_천도.php new file mode 100644 index 00000000..2c615590 --- /dev/null +++ b/hwe/sammo/Command/Nation/che_천도.php @@ -0,0 +1,245 @@ +arg === null){ + return false; + } + + if(!key_exists('destCityID', $this->arg)){ + return false; + } + if(CityConst::byID($this->arg['destCityID']) === null){ + return false; + } + $destCityID = $this->arg['destCityID']; + + $this->arg = [ + 'destCityID'=>$destCityID, + ]; + return true; + } + + protected function init(){ + $general = $this->generalObj; + + $env = $this->env; + + $this->setCity(); + $this->setNation(['capset', 'gold', 'rice']); + $this->setDestCity($this->arg['destCityID'], null); + + [$reqGold, $reqRice] = $this->getCost(); + + if($this->getDistance() === null){ + $this->runnableConstraints[ + ConstraintHelper::AlwaysFail('천도 대상으로 도달할 방법이 없습니다.') + ]; + return; + } + + $this->runnableConstraints=[ + ConstraintHelper::OccupiedCity(), + ConstraintHelper::OccupiedDestCity(), + ConstraintHelper::BeChief(), + ConstraintHelper::SuppliedCity(), + ConstraintHelper::SuppliedDestCity(), + ConstraintHelper::NotSameDestCity(), + ConstraintHelper::ReqNationGold(GameConst::$basegold+$reqGold), + ConstraintHelper::ReqNationRice(GameConst::$baserice+$reqRice), + ]; + } + + public function getCommandDetailTitle():string{ + $name = $this->getName(); + + $amount = number_format($this->env['develcost'] * 5); + + return "{$name}/1+거리×2턴(금쌀 {$amount}×2^거리)"; + } + + public function getCost():array{ + $amount = $this->env['develcost'] * 5; + $amount *= 2**$this->getDistance()??50; + + return [$amount, $amount]; + } + + private function getDistance():?int{ + if($this->cachedDist !== null){ + return $this->cachedDist; + } + $srcCityID = $this->nation['capital']; + $destCityID = $this->arg['destCityID']; + $nationID = $this->nation['nation']; + $distance = \sammo\calcCityDistance($srcCityID, $destCityID, [$nationID])??50; + $this->cachedDist = $distance; + + return $distance; + } + + public function getPreReqTurn():int{ + return 1 + $this->getDistance()*2; + } + + public function getPostReqTurn():int{ + return 0; + } + + public function addTermStack():bool{ + $lastTurn = $this->getLastTurn(); + $commandName = $this->getName(); + if($lastTurn->getCommand() != $commandName){ + $this->setResultTurn(new LastTurn( + $commandName, + $this->arg, + 1, + $this->nation['capset'] + )); + return false; + } + + if($lastTurn->getSeq() < $this->nation['capset']){ + //NOTE: 최근에 천도, 증축이 일어났으면 리셋됨 + $this->setResultTurn(new LastTurn( + $commandName, + $this->arg, + 1, + $this->nation['capset'] + )); + return false; + } + + if($lastTurn->getTerm() < $this->getPreReqTurn()){ + $this->setResultTurn(new LastTurn( + $commandName, + $this->arg, + $lastTurn->getTerm() + 1, + $this->nation['capset'] + )); + return false; + } + + return true; + } + + public function getBrief():string{ + $commandName = $this->getName(); + $destCityName = CityConst::byID($this->arg['destCityID'])->name; + $josaRo = JosaUtil::pick($destCityName, '로'); + return "【{$destCityName}】{$josaRo} {$commandName}"; + } + + public function run():bool{ + if(!$this->isRunnable()){ + throw new \RuntimeException('불가능한 커맨드를 강제로 실행 시도'); + } + + $db = DB::db(); + + $general = $this->generalObj; + $generalID = $general->getID(); + $generalName = $general->getName(); + $date = $general->getTurnTime($general::TURNTIME_HM); + + $year = $this->env['year']; + $month = $this->env['month']; + + $destCity = $this->destCity; + $destCityID = $destCity['city']; + $destCityName = $destCity['name']; + + $nationID = $general->getNationID(); + $nationName = $this->nation['name']; + + $josaRo = JosaUtil::pick($destCityName, '로'); + + $logger = $general->getLogger(); + + + $general->increaseVar( + 'experience', + $general->onCalcStat($general, + 'experience', 5 * ($this->getPreReqTurn() + 1) + )); + $general->increaseVar( + 'dedication', + $general->onCalcStat($general, + 'dedication', 5 * ($this->getPreReqTurn() + 1) + )); + + $josaYi = JosaUtil::pick($generalName, '이'); + $josaYiNation = JosaUtil::pick($nationName, '이'); + + $db->update('nation', [ + 'capital' => $destCityID, + 'capset' => $db->sqleval('capset + 1'), + ], 'nation=%i', $nationID); + + $logger->pushGeneralActionLog("{$destCityName}{$josaRo} 천도했습니다. <1>$datepushGeneralHistoryLog("{$destCityName}{$josaRo} 천도명령"); + $logger->pushNationalHistoryLog("{$generalName}{$josaYi} {$destCityName}{$josaRo} 천도 명령"); + $logger->pushGlobalActionLog("{$generalName}{$josaYi} {$destCityName}{$josaRo} 천도를 명령하였습니다."); + $logger->pushGlobalHistoryLog("【천도】{$nationName}{$josaYiNation} {$destCityName}{$josaRo} 천도하였습니다."); + + $general->setResultTurn(new LastTurn($this->getName(), $this->arg, 0)); + $general->applyDB($db); + + return true; + } + + public function getJSFiles(): array + { + return [ + 'js/defaultSelectCityByMap.js' + ]; + } + + + public function getForm(): string + { + ob_start(); +?> +
+선택된 도시로 천도합니다.
+현재 수도에서 연결된 도시만 가능하며, 1+2×거리만큼의 턴이 필요합니다.
+목록을 선택하거나 도시를 클릭하세요.
+
+
+arg['isGold']){ - $this->runnableConstraints[] = ConstraintHelper::ReqNationGold(1); + $this->runnableConstraints[] = ConstraintHelper::ReqNationGold(1+GameConst::$basegold); } else{ $this->runnableConstraints[] = ConstraintHelper::ReqNationRice(1+GameConst::$baserice); diff --git a/hwe/sammo/Constraint/ConstraintHelper.php b/hwe/sammo/Constraint/ConstraintHelper.php index 19af96e9..7623c280 100644 --- a/hwe/sammo/Constraint/ConstraintHelper.php +++ b/hwe/sammo/Constraint/ConstraintHelper.php @@ -108,6 +108,10 @@ class ConstraintHelper{ return [__FUNCTION__]; } + static function HasRouteWithEnemy():array{ + return [__FUNCTION__]; + } + static function MustBeNPC():array{ return [__FUNCTION__]; } @@ -236,6 +240,10 @@ class ConstraintHelper{ return [__FUNCTION__, [$key, $keyNick, $comp, $reqVal, $errMsg]]; } + static function ReqNationAuxValue($key, string $keyNick, string $comp, $reqVal, ?string $errMsg=null):array{ + return [__FUNCTION__, [$key, $keyNick, $comp, $reqVal, $errMsg]]; + } + static function ReqTroopMembers():array{ return [__FUNCTION__]; } diff --git a/hwe/sammo/Constraint/HasRoute.php b/hwe/sammo/Constraint/HasRoute.php index 8d6afac4..784c319b 100644 --- a/hwe/sammo/Constraint/HasRoute.php +++ b/hwe/sammo/Constraint/HasRoute.php @@ -31,9 +31,7 @@ class HasRoute extends Constraint{ $db = DB::db(); - $allowedNationList = $db->queryFirstColumn('SELECT you FROM diplomacy WHERE state = 0 AND me = %i', $this->general['nation']); - $allowedNationList[] = $this->general['nation']; - $allowedNationList[] = 0; + $allowedNationList = [$this->general['nation']]; $distanceList = \sammo\searchDistanceListToDest($this->general['city'], $this->destCity['city'], $allowedNationList); if(!$distanceList){ diff --git a/hwe/sammo/Constraint/HasRouteWithEnemy.php b/hwe/sammo/Constraint/HasRouteWithEnemy.php new file mode 100644 index 00000000..0b140039 --- /dev/null +++ b/hwe/sammo/Constraint/HasRouteWithEnemy.php @@ -0,0 +1,46 @@ +general)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require city in general"); + } + + if(!key_exists('nation', $this->general)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require nation in general"); + } + + return true; + } + + public function test():bool{ + $this->checkInputValues(); + $this->tested = true; + + $db = DB::db(); + + $allowedNationList = $db->queryFirstColumn('SELECT you FROM diplomacy WHERE state = 0 AND me = %i', $this->general['nation']); + $allowedNationList[] = $this->general['nation']; + $allowedNationList[] = 0; + + $distanceList = \sammo\searchDistanceListToDest($this->general['city'], $this->destCity['city'], $allowedNationList); + if(!$distanceList){ + $this->reason = "경로에 도달할 방법이 없습니다."; + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/hwe/sammo/Constraint/ReqNationAuxValue.php b/hwe/sammo/Constraint/ReqNationAuxValue.php new file mode 100644 index 00000000..73b18ac7 --- /dev/null +++ b/hwe/sammo/Constraint/ReqNationAuxValue.php @@ -0,0 +1,163 @@ +arg) == 5){ + [$this->key, $this->keyNick, $comp, $this->reqVal, $this->errMsg] = $this->arg; + + if(!in_array($comp, ['>', '>=', '==', '<=', '<', '!=', '===', '!=='])){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("invalid comparator"); + } + } + else{ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require key, keyNick, comp, reqVal[, errMsg]"); + } + + $this->comp = $comp; + + $this->maxKey = $this->key.'_max'; + + if(!key_exists('aux', $this->nation)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require aux in nation"); + } + + if($this->nation['aux'] === null){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("invalid aux in nation(null)"); + } + + if(is_array($this->nation['aux'])){ + $this->auxVal = $this->nation['aux']; + } + else if(is_string($this->nation['aux'])){ + $this->auxVal = Json::decode($this->nation['aux']); + } + else{ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("inavlid aux in nation(type)"); + } + + + if(!key_exists($this->key, $this->auxVal)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require {$this->key} in nation['aux']"); + } + + if(is_numeric($this->reqVal)){ + $this->isPercent = false; + } + else if(is_string($this->reqVal)){ + $this->reqVal = Util::convPercentStrToFloat($this->reqVal); + if($this->reqVal === null){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require valid reqVal(percentStr|numeric) format"); + } + + if(!key_exists($this->maxKey, $this->auxVal)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("require {$this->maxKey} in nation['aux']"); + } + $this->isPercent = true; + } + + if($this->errMsg!==null && !is_string($this->errMsg)){ + if(!$throwExeception){return false; } + throw new \InvalidArgumentException("{$this->errMsg} must be string or null"); + } + + return true; + } + + public function test():bool{ + $this->checkInputValues(); + $this->tested = true; + $keyNick = $this->keyNick; + + if ($this->isPercent) { + $reqVal = $this->auxVal[$this->maxKey] * $this->reqVal; + } + else{ + $reqVal = $this->reqVal; + } + + $compList = [ + '<'=>function($target, $src){ + return ($target < $src)?true:'너무 많습니다.'; + }, + '<='=>function($target, $src){ + return ($target <= $src)?true:'너무 많습니다.'; + }, + '=='=>function($target, $src)use($keyNick){ + return ($target == $src)?true:"올바르지 않은 {$keyNick} 입니다."; + }, + '!='=>function($target, $src)use($keyNick){ + return ($target != $src)?true:"올바르지 않은 {$keyNick} 입니다."; + }, + '==='=>function($target, $src)use($keyNick){ + return ($target === $src)?true:"올바르지 않은 {$keyNick} 입니다."; + }, + '!=='=>function($target, $src)use($keyNick){ + return ($target !== $src)?true:"올바르지 않은 {$keyNick} 입니다."; + }, + '>='=>function($target, $src){ + if($target >= $src){ + return true; + } + if($src == 1){ + return '없습니다'; + } + return '부족합니다.'; + }, + '>'=>function($target, $src){ + return $target > $src; + if($src == 0){ + return '없습니다'; + } + return '부족합니다.'; + }, + ]; + + $comp = $compList[$this->comp]; + $result = ($comp)($this->auxVal[$this->key], $reqVal); + + if($result === true){ + return true; + } + + if($this->errMsg){ + $this->reason = $this->errMsg; + } + else{ + $josaYi = JosaUtil::pick($keyNick, '이'); + $this->reason = "{$keyNick}{$josaYi} {$result}"; + } + + return false; + } +} \ No newline at end of file diff --git a/hwe/sammo/LastTurn.php b/hwe/sammo/LastTurn.php index dbab336c..a17c49e0 100644 --- a/hwe/sammo/LastTurn.php +++ b/hwe/sammo/LastTurn.php @@ -5,12 +5,14 @@ class LastTurn{ protected $command = '휴식'; protected $arg = null; protected $term = null; + protected $seq = null; - function __construct(?string $command=null, ?array $arg=null, ?int $term=null) + function __construct(?string $command=null, ?array $arg=null, ?int $term=null, ?int $seq=null) { $this->setCommand($command); $this->setArg($arg); $this->setTerm($term); + $this->setSeq($term); } static function fromJson(?string $json):self{ @@ -43,7 +45,7 @@ class LastTurn{ function getArg():?array{ return $this->arg; - } + } function setTerm(?int $term){ $this->term = $term; @@ -53,6 +55,14 @@ class LastTurn{ return $this->term; } + function setSeq(?int $seq){ + $this->seq = $seq; + } + + function getSeq():?int{ + return $this->seq; + } + function toJson():string{ $result = [ 'command'=>$this->command diff --git a/hwe/sql/schema.sql b/hwe/sql/schema.sql index 72c6b471..63e03bdb 100644 --- a/hwe/sql/schema.sql +++ b/hwe/sql/schema.sql @@ -119,7 +119,7 @@ CREATE TABLE `nation` ( `onlinegen` VARCHAR(1024) NULL DEFAULT '', `msg` TEXT NULL DEFAULT '', `capital` INT(1) NULL DEFAULT '0', - `capset` INT(1) NULL DEFAULT '0', + `capset` INT(6) NULL DEFAULT '0', `gennum` INT(3) NULL DEFAULT '1', `gold` INT(8) NULL DEFAULT '0', `rice` INT(8) NULL DEFAULT '0',