Skip to content

Commit

Permalink
OthelloTestオールGreen #55
Browse files Browse the repository at this point in the history
  • Loading branch information
ebinase committed May 29, 2022
1 parent eb42896 commit b4c8605
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 143 deletions.
61 changes: 38 additions & 23 deletions packages/Models/Othello/Othello/Othello.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand All @@ -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;
}

}
10 changes: 10 additions & 0 deletions packages/Models/Othello/Othello/Status.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Packages\Models\Othello\Othello;

enum Status
{
case PLAYING; // プレー中
case RESULTED; // 決着(引き分け含む)
case INTERRUPTED; // (強制終了)
}
8 changes: 4 additions & 4 deletions packages/Models/Othello/Othello/Turn.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static function make(int $turnNumber, Color $playableColor, Board $board)
public function advance(Position $position): Turn
{
// これ以上進めない場合
if (!$this->isAdvanceable()) throw new DomainException();
if ($this->isLast()) throw new DomainException();
// スキップするしかない場合はコマを置けない
if ($this->mustSkip()) throw new DomainException('このターンはスキップ以外できません。');

Expand All @@ -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('コマを置くことができるマスがある場合、スキップはできません。');

Expand All @@ -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
Expand Down
159 changes: 56 additions & 103 deletions tests/Feature/Models/Othello/Othello/OthelloTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());

}

Expand All @@ -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);
}

Expand All @@ -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(), '黒がスキップした後、白もスキップせざるを得なくなるため試合終了');
}


}
Loading

0 comments on commit b4c8605

Please sign in to comment.