diff --git a/packages/Models/Othello/Othello/Othello.php b/packages/Models/Othello/Othello/Othello.php index bbb2d55..d707622 100644 --- a/packages/Models/Othello/Othello/Othello.php +++ b/packages/Models/Othello/Othello/Othello.php @@ -9,46 +9,61 @@ class Othello { - // スキップの連続を許容する回数 - const MAX_CONTINUOUS_SKIP_COUNT = 1; - private function __construct( public readonly string $id, + private Status $status, private Turn $turn, - private int $skipCount - ) {} + ) { + if (!Str::isUuid($id)) throw new DomainException(); + } public static function init(): Othello { return new self( - id: Str::uuid(), - turn: Turn::init(), - skipCount: 0, + id: Str::uuid(), + status: Status::PLAYING, + turn: Turn::init(), ); } - public static function make(string $id, Turn $turn, int $skipCount): self + public static function make(string $id, Status $status, Turn $turn): self { return new self( - id: $id, - turn: $turn, - skipCount: $skipCount, + id: $id, + status: $status, + turn: $turn, ); } /** * ゲームの状態更新 - * 連続スキップ数:スキップ時+1。スキップしない場合は0にリセット */ public function apply(Action $action): void { - // ゲームが終了している場合 - if ($this->isOver()) throw new DomainException(); + // プレー中ではない場合 + if ($this->status !== Status::PLAYING) throw new DomainException(); + $turnBefore = $this->turn; + // ターンを更新する $this->turn = match ($action->actionType) { ActionType::SET_STONE => $this->turn->advance($action->data), ActionType::CONFIRM_SKIP => $this->turn->skip(), }; + + $this->status = $this->nextStatus($turnBefore, $this->turn); + } + + private function nextStatus(Turn $turnBefore, Turn $turnAfter): Status + { + if ($turnAfter->isLast()) return $this->status = Status::RESULTED; + if (self::mustInterrupt($turnBefore, $turnAfter)) return $this->status = Status::INTERRUPTED; + + return Status::PLAYING; + } + + private static function mustInterrupt(Turn $turnBefore, Turn $turnAfter) + { + return $turnBefore->mustSkip() && $turnAfter->mustSkip(); } /** @@ -60,23 +75,23 @@ public function apply(Action $action): void public function isOver(): bool { // ターンが進行不可(正常系)、もしくはスキップが連続してどちらも置く場所がなくなった(異常系)場合、終了 - $isInvalidSkipCount = $this->skipCount > self::MAX_CONTINUOUS_SKIP_COUNT; - return !$this->turn->isAdvanceable() || $isInvalidSkipCount; + return !$this->turn->isLast(); } /** - * @return Turn + * @return Status */ - public function getTurn(): Turn + public function getStatus(): Status { - return $this->turn; + return $this->status; } /** - * @return int + * @return Turn */ - public function getSkipCount(): int + public function getTurn(): Turn { - return $this->skipCount; + return $this->turn; } + } diff --git a/packages/Models/Othello/Othello/Status.php b/packages/Models/Othello/Othello/Status.php new file mode 100644 index 0000000..82ba28d --- /dev/null +++ b/packages/Models/Othello/Othello/Status.php @@ -0,0 +1,10 @@ +isAdvanceable()) throw new DomainException(); + if ($this->isLast()) throw new DomainException(); // スキップするしかない場合はコマを置けない if ($this->mustSkip()) throw new DomainException('このターンはスキップ以外できません。'); @@ -67,7 +67,7 @@ public function advance(Position $position): Turn public function skip(): Turn { // これ以上進めない場合 - if (!$this->isAdvanceable()) throw new DomainException(); + if ($this->isLast()) throw new DomainException(); // コマを置くことができる場合はスキップできない if (!$this->mustSkip()) throw new DomainException('コマを置くことができるマスがある場合、スキップはできません。'); @@ -82,10 +82,10 @@ public function skip(): Turn * 最終ターンが終了しているか判定 * @return bool */ - public function isAdvanceable(): bool + public function isLast(): bool { // 盤面がいっぱいになっていなかったら最終ターンに到達していない = 進行可能 - return !$this->board->isFulfilled(); + return $this->board->isFulfilled(); } public function mustSkip(): bool diff --git a/tests/Feature/Models/Othello/Othello/OthelloTest.php b/tests/Feature/Models/Othello/Othello/OthelloTest.php index 4af36e0..7dccd47 100644 --- a/tests/Feature/Models/Othello/Othello/OthelloTest.php +++ b/tests/Feature/Models/Othello/Othello/OthelloTest.php @@ -2,15 +2,14 @@ namespace Tests\Feature\Models\Othello\Othello; -use Packages\Models\Common\Matrix\Matrix; -use Packages\Models\Othello\Action\Action; -use Packages\Models\Othello\Action\ActionType; -use Packages\Models\Othello\Board\Board; +use Packages\Exceptions\DomainException; use Packages\Models\Othello\Board\Color\Color; -use Packages\Models\Othello\Board\Position\Position; use Packages\Models\Othello\Othello\Othello; +use Packages\Models\Othello\Othello\Status; use Packages\Models\Othello\Othello\Turn; -use Tests\Mock\Models\Othello\Action\FirstTurnActionMock; +use Tests\Mock\Models\Othello\Action\ActionMock; +use Tests\Mock\Models\Othello\Board\FulfilledBoard; +use Tests\Mock\Models\Othello\Board\OneLastActionBoard; use Tests\Mock\Models\Othello\Board\SkipBoardMock; use Tests\TestCase; @@ -24,29 +23,25 @@ public function オセロ初期化() // then: self::assertTrue(\Str::isUuid($othello->id)); - self::assertSame(0, $othello->getSkipCount(), 'スキップカウントは0'); + self::assertSame(Status::PLAYING, $othello->getStatus(), '初期状態はプレー中'); // 下記の値はそのまま設定される self::assertTrue(Turn::init() == $othello->getTurn()); } /** @test */ - public function 通常更新() + public function プレー中の場合はゲームを進めることができる() { // given: - $othello = Othello::make( - $id = \Str::uuid(), - $turn = Turn::init(), - 0, - ); // 1ターン目 - $action = FirstTurnActionMock::setStone(); + $othello = Othello::init(); + $turn = $othello->getTurn(); // when: + $action = ActionMock::setStone(); $othello->apply($action); // プレーを進める // then: - self::assertSame($id->toString(), $othello->id); + self::assertSame(Status::PLAYING, $othello->getStatus()); self::assertTrue($turn->advance($action->getData()) == $othello->getTurn()); - self::assertSame(0, $othello->getSkipCount()); } @@ -56,13 +51,13 @@ public function スキップせざるを得ないのにコマを置こうとし // given: $othello = Othello::make( \Str::uuid(), + Status::PLAYING, Turn::make(1, Color::white(), SkipBoardMock::get()), - 0 - ); // 1ターン目 + ); // when: - $action = FirstTurnActionMock::setStone(); // おけない場所 + $action = ActionMock::setStone(); // おけない場所 // then: - $this->expectException(\Exception::class); + $this->expectException(DomainException::class); $othello->apply($action); } @@ -72,113 +67,71 @@ public function 置ける場所があるのにスキップしようとした場 // given: $othello = Othello::init(); // 1ターン目 // when: - $skipAction = FirstTurnActionMock::skip(); + $skipAction = ActionMock::skip(); // then: - $this->expectException(\Exception::class); + $this->expectException(DomainException::class); // 置ける場所があるのに場所指定なしで更新した場合 $othello->apply($skipAction); } - // --------------------------------------- - // 終了条件系 - // --------------------------------------- /** @test */ - public function 盤面に空いているマスがなくなったら終了() + public function ゲームが終了しているのに進めようとしたら例外を出す() { // given: - $fullBoard = Board::make(Matrix::init(8, 8, Color::white()->toCode())->toArray()); - - // when: - $firstTurn = Turn::init(); - $lastTurn = Turn::make(20, Color::white(), $fullBoard, 0); - // then: - self::assertSame(true, $firstTurn->isAdvanceable()); - self::assertSame(false, $lastTurn->isAdvanceable()); - } - - /** @test */ - public function スキップが2ターン続いたら終了() - { - // given: - $position = Position::make([4, 6]); - - // when: - $turnSkip0 = Turn::make(20, Color::white(), Board::init(), 0); - $turnSkip1 = Turn::make(20, Color::white(), Board::init(), 1); - $turnSkip2 = Turn::make(20, Color::white(), Board::init(), 2); - + $interruptedOthello = Othello::make( + \Str::uuid(), + Status::INTERRUPTED, + Turn::make(1, Color::white(), FulfilledBoard::get()), + ); + $resultedOthello = Othello::make( + \Str::uuid(), + Status::RESULTED, + Turn::make(1, Color::white(), FulfilledBoard::get()), + ); // then: - // isContinuable()テスト - self::assertSame(true, $turnSkip0->isContinuable()); - self::assertSame(true, $turnSkip1->isContinuable()); - self::assertSame(false, $turnSkip2->isContinuable()); - // skipカウントとupdateのテスト - self::assertSame(true, !empty($turnSkip0->advance($position))); // next()が問題なく実行できればOK - self::assertSame(true, !empty($turnSkip1->advance($position))); // next()が問題なく実行できればOK - // スキップが2回続いたターンを更新しようとすると例外 - self::expectException(\Exception::class); - $turnSkip2->advance($position); + self::expectException(DomainException::class); + $interruptedOthello->apply(ActionMock::setStone()); + $interruptedOthello->apply(ActionMock::skip()); + $resultedOthello->apply(ActionMock::setStone()); + $resultedOthello->apply(ActionMock::skip()); } // --------------------------------------- - // スキップ系 + // 終了条件系 // --------------------------------------- /** @test */ - public function 盤面における場所がなかったらスキップカウントがアップ() + public function 盤面に空いているマスがなくなったら終了() { - // given: - // 白も黒も置ける場所がない盤面 - $board = SkipBoardMock::get(); - // when: - $turn1 = Turn::make(1, Color::black(), $board, 0); - $turn2 = $turn1->advance(); - $turn3 = $turn2->advance(); - + $othello = Othello::make( + \Str::uuid(), + Status::PLAYING, + Turn::make(1, Color::white(), OneLastActionBoard::get8行8列に白を置くとすべて埋まる盤面()), + ); // then: - // 2ターン目 - self::assertSame(1, $turn2->getSkipCount()); - self::assertSame(true, $turn2->mustSkip()); - self::assertSame(true, $turn2->isContinuable()); - // 3ターン目 - self::assertSame(2, $turn3->getSkipCount()); - self::assertSame(true, $turn2->mustSkip()); - self::assertSame(true, $turn2->isContinuable()); + $othello->apply(ActionMock::setStone(8, 8)); + self::assertSame(Status::RESULTED, $othello->getStatus()); } /** @test */ - public function スキップされなかった時、カウントは0に戻る() + public function スキップが2ターン続いたら終了() { // given: - $w = Color::white()->toCode(); - $b = Color::black()->toCode(); - // 黒がスキップする盤面 - $board = [ - [0,0,0,0,0,0,0,0,], - [0,0,0,0,0,0,0,0,], - [0,0,0,0,0,0,0,0,], - [0,0,$b,$w,$b,0,0,0,], - [0,0,0,0,0,0,0,0,], - [0,0,0,0,0,0,0,0,], - [0,0,0,0,0,0,0,0,], - [0,0,0,0,0,0,0,0,], - ]; - $board = Board::make($board); - // when: - // 黒のターンとして生成 - $turn1 = Turn::make(1, Color::black(), $board, 0); - // 黒が行動(スキップ) - $turn2 = $turn1->advance(); - // 白が行動 - $turn3 = $turn2->advance(Position::make([4, 2])); - + // 両者ともスキップしかできない盤面 + $othello = Othello::make( + \Str::uuid(), + Status::PLAYING, + Turn::make(1, Color::white(), SkipBoardMock::白を4行5列に置くと両色ともスキップせざるを得なくなる盤面()), + ); // then: - // 2ターン目(黒の行動後の白のターン) - self::assertSame(1, $turn2->getSkipCount(), 'スキップしたのでカウントアップ'); - // 3ターン目(白の行動後の黒のターン) - self::assertSame(0, $turn3->getSkipCount(), '色に関わらず行動できたときはリセット'); + self::assertSame(Status::PLAYING, $othello->getStatus(), 'このターンはプレー中'); + // 白の行動 + $othello->apply(ActionMock::setStone(4, 5)); + self::assertSame(Status::PLAYING, $othello->getStatus(), '白がコマを置くと黒にスキップを強制'); + // 黒がスキップ + $othello->apply(ActionMock::skip()); + self::assertSame(Status::INTERRUPTED, $othello->getStatus(), '黒がスキップした後、白もスキップせざるを得なくなるため試合終了'); } - } diff --git a/tests/Feature/Models/Othello/Turn/TurnTest.php b/tests/Feature/Models/Othello/Turn/TurnTest.php index e54ecbc..76476fd 100644 --- a/tests/Feature/Models/Othello/Turn/TurnTest.php +++ b/tests/Feature/Models/Othello/Turn/TurnTest.php @@ -3,11 +3,11 @@ namespace Tests\Feature\Models\Othello\Turn; use Packages\Exceptions\DomainException; -use Packages\Models\Common\Matrix\Matrix; use Packages\Models\Othello\Board\Board; use Packages\Models\Othello\Board\Color\Color; use Packages\Models\Othello\Board\Position\Position; use Packages\Models\Othello\Othello\Turn; +use Tests\Mock\Models\Othello\Board\FulfilledBoard; use Tests\Mock\Models\Othello\Board\SkipBoardMock; use Tests\TestCase; @@ -88,7 +88,6 @@ public function スキップをした場合ターン数増加とプレイヤー /** @test */ public function 置ける場所があるのにスキップしようとした場合は例外を出す() { - // given: // when: $turn = Turn::init(); // 1ターン目 // then: @@ -103,16 +102,11 @@ public function 置ける場所があるのにスキップしようとした場 /** @test */ public function 盤面に空いているマスがなくなった時が最後のターン() { - // given: - $fullBoard = Board::make(Matrix::init(8, 8, Color::white()->toCode())->toArray()); - // when: $firstTurn = Turn::init(); - $lastTurn = Turn::make(20, Color::white(), $fullBoard, 0); + $lastTurn = Turn::make(20, Color::white(), FulfilledBoard::get()); // then: - self::assertSame(false, !$firstTurn->isAdvanceable()); - self::assertSame(true, !$lastTurn->isAdvanceable()); + self::assertFalse($firstTurn->isLast()); + self::assertTrue($lastTurn->isLast()); } - - } diff --git a/tests/Mock/Models/Othello/Action/FirstTurnActionMock.php b/tests/Mock/Models/Othello/Action/ActionMock.php similarity index 67% rename from tests/Mock/Models/Othello/Action/FirstTurnActionMock.php rename to tests/Mock/Models/Othello/Action/ActionMock.php index b323a99..09b89a7 100644 --- a/tests/Mock/Models/Othello/Action/FirstTurnActionMock.php +++ b/tests/Mock/Models/Othello/Action/ActionMock.php @@ -6,11 +6,11 @@ use Packages\Models\Othello\Action\ActionType; use Packages\Models\Othello\Board\Position\Position; -class FirstTurnActionMock +class ActionMock { - public static function setStone(): Action + public static function setStone($row = 4, $col = 6): Action { - $position = Position::make([4, 6]);// 先行プレイヤーが1ターン目に指す場所 + $position = Position::make([$row, $col]);// 先行プレイヤーが1ターン目に指す場所 return Action::make(ActionType::SET_STONE, $position); } diff --git a/tests/Mock/Models/Othello/Board/FulfilledBoard.php b/tests/Mock/Models/Othello/Board/FulfilledBoard.php new file mode 100644 index 0000000..622a72e --- /dev/null +++ b/tests/Mock/Models/Othello/Board/FulfilledBoard.php @@ -0,0 +1,21 @@ +toCode() + )->toArray() + ); + } +} diff --git a/tests/Mock/Models/Othello/Board/OneLastActionBoard.php b/tests/Mock/Models/Othello/Board/OneLastActionBoard.php new file mode 100644 index 0000000..12a3290 --- /dev/null +++ b/tests/Mock/Models/Othello/Board/OneLastActionBoard.php @@ -0,0 +1,24 @@ +toCode() + ); + $matrix->setData(Color::black()->toCode(), 8, 7); + $matrix->setData(Board::BOARD_EMPTY, 8, 8); + return Board::make( + $matrix->toArray() + ); + } +} diff --git a/tests/Mock/Models/Othello/Board/SkipBoardMock.php b/tests/Mock/Models/Othello/Board/SkipBoardMock.php index 487b01a..00c9c19 100644 --- a/tests/Mock/Models/Othello/Board/SkipBoardMock.php +++ b/tests/Mock/Models/Othello/Board/SkipBoardMock.php @@ -23,4 +23,21 @@ public static function get(): Board ]; return Board::make($board); } + + public static function 白を4行5列に置くと両色ともスキップせざるを得なくなる盤面(): Board + { + $w = Color::white()->toCode(); + $b = Color::black()->toCode(); + $board = [ + [0,0,0,0,0,0,0,0,], + [0,0,0,0,0,0,0,0,], + [0,0,0,0,0,0,0,0,], + [0,0,0,$w, 0,0,0,0,], + [0,0,0,$w,$b,0,0,0,], + [0,0,0, 0,$w,0,0,0,], + [0,0,0,0,0,0,0,0,], + [0,0,0,0,0,0,0,0,], + ]; + return Board::make($board); + } }