Skip to content

Commit

Permalink
Merge pull request #1 from warrenca/ai-vs-ai
Browse files Browse the repository at this point in the history
v2
  • Loading branch information
warrenca authored Mar 24, 2018
2 parents 58a6984 + 3379657 commit 56549c4
Show file tree
Hide file tree
Showing 11 changed files with 565 additions and 219 deletions.
3 changes: 2 additions & 1 deletion main.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env php
<?php
// Let the game begin!

Expand All @@ -16,7 +17,7 @@

$gameMode = readline("Enter 1, 2 or 3: ");

if (!in_array($gameMode, [1,2,3]))
if (!in_array($gameMode, [1, 2, 3]))
{
printError("Invalid selection.");
goto chooseGameMode;
Expand Down
66 changes: 34 additions & 32 deletions php-di/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,66 @@

return [
// Board Instantiation
'connect4.view.board' => \DI\create(Board::class),
'connect4.view.board' => \DI\create(Board::class),
// MoveStore Instantiation
'connect4.store.movesStore' => \DI\create(MovesStore::class),
// Human 1 Player Instantiation
'connect4.player.human' => function(\Psr\Container\ContainerInterface $c) {
$player = new \Connect4\Player\HumanPlayer();
$player->setName("Human 👤");
$player->setMovesStore($c->get('connect4.store.movesStore'));
return $player;
'connect4.player.human' => function (\Psr\Container\ContainerInterface $c) {
$player = new \Connect4\Player\HumanPlayer();
$player->setName("Human 👤");
$player->setMovesStore($c->get('connect4.store.movesStore'));
return $player;
},
// Human 2 Player Instantiation
'connect4.player.human2' => function(\Psr\Container\ContainerInterface $c) {
'connect4.player.human2' => function (\Psr\Container\ContainerInterface $c) {
$player = new \Connect4\Player\HumanPlayer();
$player->setName("Human 2 👤");
$player->setMovesStore($c->get('connect4.store.movesStore'));
return $player;
},
// AI 1 Player Instantiation
'connect4.player.ai' => function(\Psr\Container\ContainerInterface $c)
{
'connect4.player.ai' => function (\Psr\Container\ContainerInterface $c) {
$player = new \Connect4\Player\AiPlayer\DumbAiPlayer();
$player->setName("Robot 🤖");
$player->setMovesStore($c->get('connect4.store.movesStore'));
return $player;
},
// AI 2 Player Instantiation, used in the test
'connect4.player.smarterAi' => function(\Psr\Container\ContainerInterface $c)
{
'connect4.player.smarterAi' => function (\Psr\Container\ContainerInterface $c) {
$player = new \Connect4\Player\AiPlayer\SmarterAiPlayer();
$player->setName("Smarter Robot 🤖");
$player->setMovesStore($c->get('connect4.store.movesStore'));
return $player;
},
// Game Instantiation for Human v AI
'connect4.game.human.vs.ai' => function(\Psr\Container\ContainerInterface $c)
'connect4.game' => function(\Psr\Container\ContainerInterface $c)
{
return new \Connect4\Game( $c->get('connect4.view.board'),
$c->get('connect4.player.human'),
$c->get('connect4.player.smarterAi'),
$c->get('connect4.store.movesStore')
);
return new \Connect4\Game($c->get('connect4.view.board'), $c->get('connect4.store.movesStore'));
},
// Game Instantiation for Human v AI
'connect4.game.human.vs.ai' => function (\Psr\Container\ContainerInterface $c) {
/** @var \Connect4\Game $game */
$game = $c->get('connect4.game');
$game->setPlayerOne($c->get('connect4.player.human'));
$game->setPlayerTwo($c->get('connect4.player.smarterAi'));

return $game;
},
// Game Instantiation for Human v Human
'connect4.game.human.vs.human' => function(\Psr\Container\ContainerInterface $c)
{
return new \Connect4\Game( $c->get('connect4.view.board'),
$c->get('connect4.player.human'),
$c->get('connect4.player.human2'),
$c->get('connect4.store.movesStore')
);
'connect4.game.human.vs.human' => function (\Psr\Container\ContainerInterface $c) {
/** @var \Connect4\Game $game */
$game = $c->get('connect4.game');
$game->setPlayerOne($c->get('connect4.player.human'));
$game->setPlayerTwo($c->get('connect4.player.human2'));

return $game;
},
// Game for testing Instantiation
'connect4.game.ai.vs.ai' => function(\Psr\Container\ContainerInterface $c)
{
return new \Connect4\Game( $c->get('connect4.view.board'),
$c->get('connect4.player.ai'),
$c->get('connect4.player.smarterAi'),
$c->get('connect4.store.movesStore')
);
'connect4.game.ai.vs.ai' => function (\Psr\Container\ContainerInterface $c) {
/** @var \Connect4\Game $game */
$game = $c->get('connect4.game');
$game->setPlayerOne($c->get('connect4.player.ai'));
$game->setPlayerTwo($c->get('connect4.player.smarterAi'));

return $game;
}
];
29 changes: 29 additions & 0 deletions src/Connect4/CellsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Connect4;


trait CellsTrait
{
/**
* Contains all the information about the token positions in the board
* @var array
*/
private $cells = [];

/**
* @return array
*/
public function getCells()
{
return $this->cells;
}

/**
* @param array $cells
*/
public function setCells($cells)
{
$this->cells = $cells;
}
}
86 changes: 46 additions & 40 deletions src/Connect4/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

namespace Connect4;

// Make a warning as an exception so it can be catched
// https://airbrake.io/blog/php-exception-handling/errorexception
error_reporting(E_WARNING);
set_error_handler(function($severity, $message, $file, $line){
throw new \ErrorException($message, 0, $severity, $file, $line);
});


use Connect4\Player\PlayerInterface;
use Connect4\Store\MovesStore;
Expand Down Expand Up @@ -44,15 +51,11 @@ class Game
/**
* Game constructor.
* @param Board $board
* @param PlayerInterface $playerOne
* @param PlayerInterface $playerTwo
* @param MovesStore $movesStore
*/
public function __construct(Board $board, PlayerInterface $playerOne, PlayerInterface $playerTwo, MovesStore $movesStore)
public function __construct(Board $board, MovesStore $movesStore)
{
$this->setBoard($board);
$this->setPlayerOne($playerOne);
$this->setPlayerTwo($playerTwo);
$this->setMovesStore($movesStore);
$this->setMaximumTurns(Board::COLUMNS * Board::ROWS);
}
Expand All @@ -67,20 +70,20 @@ public function setup()
$this->getPlayerTwo()->setToken(Board::TOKEN_PLAYER_TWO);

printInfo(
"Hey! Welcome to Connect4 game.\n" .
"It is a turn-based game between two players.\n" .
"Each player simply needs to enter a column number \n".
"where they want to drop their token.\n" .
"To win, they must Connect4 tokens of their own either\n" .
"horizontally, vertically or diagonally.\n" .
"No one wins when neither player Connect's 4 token.\n" .
"--------------------------------------\n" .
"The players are...\n" .
sprintf("Player One: Name %s, Token %s\n", $this->playerOne->getName(), $this->playerOne->getToken()) .
sprintf("Player Two: Name %s, Token %s\n", $this->playerTwo->getName(), $this->playerTwo->getToken()) .
Board::TOKEN_EMPTY_CELL . " indicates an empty cell and a valid drop point.\n" .
"Press Ctrl+C anytime to exit the game.\n".
"Have fun!\n\n"
"Hey! Welcome to Connect4 game.\n" .
"It is a turn-based game between two players.\n" .
"Each player simply needs to enter a column number \n" .
"where they want to drop their token.\n" .
"To win, they must Connect4 tokens of their own either\n" .
"horizontally, vertically or diagonally.\n" .
"No one wins when neither player Connect's 4 token.\n" .
"--------------------------------------\n" .
"The players are...\n" .
sprintf("Player One: Name %s, Token %s\n", $this->playerOne->getName(), $this->playerOne->getToken()) .
sprintf("Player Two: Name %s, Token %s\n", $this->playerTwo->getName(), $this->playerTwo->getToken()) .
Board::TOKEN_EMPTY_CELL . " indicates an empty cell and a valid drop point.\n" .
"Press Ctrl+C anytime to exit the game.\n" .
"Have fun!\n\n"
);

// Initialise the board and print it on the screen
Expand All @@ -102,10 +105,8 @@ public function start()
$turn = 0;

// While there's no winner or the maximum turns hasn't been reached
while (!$this->getWinner() && $turn < $this->getMaximumTurns())
{
if ($turn % 2)
{
while (!$this->getWinner() && $turn < $this->getMaximumTurns()) {
if ($turn % 2) {
// If mod is 1 or true
// It's Player 2's turn
$this->setCurrentPlayer($this->getPlayerTwo());
Expand All @@ -119,18 +120,22 @@ public function start()

$this->initiateMove();

if ($this->getMovesStore()->checkWinningPatterns($this->getCurrentPlayer()))
{
$hasAWinner = $this->getMovesStore()->checkWinningPatterns($this->getCurrentPlayer());
$this->board->setCells($this->getMovesStore()->getCells());

if ($hasAWinner) {
$this->setWinner($this->getCurrentPlayer());
$this->board->draw();
// End the game since there's already a winner
break;
}

$this->board->draw();

$turn++;
}

if ($this->getWinner())
{
if ($this->getWinner()) {
// There's a winner!
printSuccess("Congratulations! The winner is " . $this->getWinner()->getName());
} else {
Expand Down Expand Up @@ -162,36 +167,37 @@ public function getMaximumTurns()
*/
private function initiateMove($tries = 1)
{
$columnIndex = $this->getCurrentPlayer()->enterColumn() - 1;
try {
$columnIndex = $this->getCurrentPlayer()->enterColumn() - 1;
} catch (\Exception $e)
{
$validColumns = $this->getMovesStore()->getValidColumns();
$columnIndex = $validColumns[array_rand($validColumns, 1)];
echo "\n";
printError("--- Time elapsed. Your move was autoselected --- ");
}

// This is just a cheap fix for memory exhaustion due to AIs
// choosing of column that is already full multiple times.
if (!$this->getCurrentPlayer()->isHuman() && $tries > 1000)
{
if (!$this->getCurrentPlayer()->isHuman() && $tries > 1000) {
printError("Stalemate. There is no winner.");
exit;
}

// Drop the token to the designated column
if (!$this->getMovesStore()->dropToken($columnIndex, $this->getCurrentPlayer()->getToken()))
{
if (!$this->getMovesStore()->dropToken($columnIndex, $this->getCurrentPlayer()->getToken())) {
// Invalid dropping...
if ($this->getCurrentPlayer()->isHuman())
{
if ($this->getCurrentPlayer()->isHuman()) {
// Show only the errors to human and ignore error for robot
printError($this->getMovesStore()->getError());
}

// Ask to make another move since there's an error
$this->initiateMove($tries+1);
$this->initiateMove($tries + 1);
} else {
// There's a valid move so we'll print that information
$humanReadableColumn = $columnIndex + 1;
printInfo( sprintf('%s %s move is in the column %s', $this->getCurrentPlayer()->getName(), $this->getCurrentPlayer()->getToken(), $humanReadableColumn) );

// Set the cells to the board and draw it
$this->getBoard()->setCells($this->getMovesStore()->getCells());
$this->getBoard()->draw();
printInfo(sprintf('%s %s move is in the column %s', $this->getCurrentPlayer()->getName(), $this->getCurrentPlayer()->getToken(), $humanReadableColumn));
}
}

Expand Down
5 changes: 0 additions & 5 deletions src/Connect4/Player/AiPlayer/AiPlayerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@

interface AiPlayerInterface
{
/**
* @return mixed
*/
public function enterColumn();

/**
* @return mixed
*/
Expand Down
21 changes: 20 additions & 1 deletion src/Connect4/Player/HumanPlayer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class HumanPlayer extends PlayerAbstract
{
const IS_HUMAN = true;
const TIME_PER_MOVE_IN_SECONDS = 15;

public function __construct()
{
Expand All @@ -24,6 +25,24 @@ public function __construct()
*/
public function enterColumn()
{
return readline(sprintf("%s%s) Enter a column number: ", $this->getName(), $this->getToken()));
printInfo(sprintf("%s%s) Enter a column number: ", $this->getName(), $this->getToken()), false);
return $this->readlineTimeout(self::TIME_PER_MOVE_IN_SECONDS, "");
}

/**
* Timeout for readline, only works in linux
* http://php.net/manual/en/function.readline.php#91643
*
* @param $sec
* @param $def
* @return string
*/
public function readlineTimeout($sec, $def)
{
return trim(shell_exec('bash -c ' .
escapeshellarg('phprlto=' .
escapeshellarg($def) . ';' .
'read -t ' . ((int)$sec) . ' phprlto;' .
'echo "$phprlto"')));
}
}
Loading

0 comments on commit 56549c4

Please sign in to comment.