Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="container"></div>
              
            
!

CSS

              
                * {
  -webkit-box-sizing: border-box; 
  -moz-box-sizing: border-box;    
  box-sizing: border-box; 
  color: #484848;
  font-family: 'Titillium Web', sans-serif;
}

html, #container {
  background-color: #eee;
}

              
            
!

JS

              
                /* 
 * I got help implementing AI part from the post : https://mostafa-samir.github.io/Tic-Tac- 
 * Toe-AI/. It was implemented in vanila javascript mostly, I refactored in React. 
 */


const styled = styled.default;

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      onStart: false,
      userMark: 'X',
      comMark: 'O',
      difficulty: '',
    };
    this.startGame = this.startGame.bind(this);
    this.reset = this.reset.bind(this);
    this.clickBlock = this.clickBlock.bind(this);
    this.userPick = this.userPick.bind(this);
  }

  startGame() {
    const turn = this.state.userMark == 'X' ? 'user' : 'com';
    this.setState({
      onStart: true,
      startTurn: turn,
    });
    const selectedDiffeculty = 'master';
    var aiPlayer = new AI(selectedDiffeculty);
    globals.game = new Game(aiPlayer);
    aiPlayer.plays(globals.game);
    if (turn == 'user') globals.game.start();
    else globals.game.start('com', true);
  }

  clickBlock(e) {
    const {
      startTurn
    } = this.state;
    const indx = e.target.value;
    this.setState({
      clicked: 'on'
    });
    //Display mark
    if (startTurn == 'user' && globals.game.status === "running" && globals.game.currentState.turn === "X") {
      var next = new State(globals.game.currentState);
      next.board[indx] = "X";

      //Advances the turn in the state
      next.advanceTurn();

      globals.game.advanceTo(next);
    }
    if (startTurn == 'com' && globals.game.status === "running" && globals.game.currentState.turn === "O") {
      var next = new State(globals.game.currentState);
      next.board[indx] = "O";

      //Advances the turn in the state
      next.advanceTurn();

      globals.game.advanceTo(next, startTurn);
    }

  }

  userPick(e) {
    const user = e.target.value,
      com = user == 'X' ? 'O' : 'X';
    this.setState({
      userMark: user,
      comMark: com
    })
  }

  reset() {
    const selectedDiffeculty = 'master';
    var aiPlayer = new AI(selectedDiffeculty);
    globals.game = new Game(aiPlayer);
    aiPlayer.plays(globals.game);
    globals.game.start();
    this.setState({
      onStart: false,
    });
  }

  render() {
    const {
      onStart,
      player,
      computer,
      userMark,
      comMark
    } = this.state;
    return (
      <AppContainer>
        { onStart ? (
          <AppConsole 
            clickBlock={this.clickBlock}
            player={player}
            computer={computer}
            userMark={userMark}
            comMark={comMark}
            reset={this.reset}
           />
        ) : (
          <AppHeader 
            startGame={this.startGame}
            userMark={userMark}
            userPick={this.userPick}  
           />
        ) 
          } 
      </AppContainer>
    );
  }
}

function AppHeader({
  startGame,
  userMark,
  userPick
}) {
  return (
    <div>
      <Title> Tic <br /> Tac <br /> Toe </Title>
      <StartButton
        onClick={startGame}
      >
        Start
      </StartButton>
      <SelectMark>
        <label>
          <input 
            type='radio'
            value='X'
            checked={userMark == 'X'}
            onChange={userPick}
          />        
          X 
        </label>
           
        <label>
          <input 
            type='radio'
            value='O'
            checked={userMark == 'O'}
            onChange={userPick}
          />
           O 
        </label>
      </SelectMark>
    </div>
  );
}

function AppConsole({
  reset,
  clickBlock
}) {
  let blocks = [];
  const board = globals.game.currentState.board,
    dashboard = globals.game.dashboard;
  console.log(globals.game);
  for (let i = 0; i <= 8; i++) {
    if (board[i] != 'E') {
      blocks.push(
        <Block value={i} onClick={()=>{return false;}}>
          {board[i]}
        </Block>
      );
    } else {
      blocks.push(<Block value={i} onClick={clickBlock}></Block>);
    }
  }
  return (
    <AppStage>
      {blocks}
      <ResetButton
        onClick={reset}>
        Reset
      </ResetButton>
      <Dashboard>{dashboard}</Dashboard>
    </AppStage>
  )
}

const AppContainer = styled.div `
  width: 300px;
  height: 300px;
  margin: 100px auto;
  border: none;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
`;

const AppStage = styled.div `
  display: flex;
  flex-wrap: wrap;
  width: 230px;
  height: 230px;
  margin: auto;
  padding: 8px;
  padding-top: 19px;
  background-color: #fff;
`;

const Block = styled.button `
  font-size: 56px;
  line-height: 54px;
  margin: 4px;
  width: 63px;
  height: 63px;
  border: 1px solid;
  background-color: #fff;
  cursor: pointer;
  outline: none;

`;

const StartButton = styled.button `
  position: relative;
  top: -97px;
  width: 100px;
  height: 50px;
  background-color: #fff;
  font-size: 21px;
  border: none;
  border-radius: 2px;
  box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
`;

const ResetButton = styled.button `
  position: relative;
  top: 20px;
  left: 147px;
  width: 63px;
  height: 25px;
  background-color: #fff;
  border: none;
  border-radius: 2px;
  box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
`;

const Title = styled.span `
  display: inline-block;
  font-size: 73px;
  line-height: 77px;
  margin: 25px;
`;

const SelectMark = styled.form `
  position: relative;
  top: -107px;
  left: 191px;
`;

const Dashboard = styled.span`
  position: relative;
  top: 21px;
  right: 55px;
  text-align: left;
`;

ReactDOM.render(
  <App />,
  document.getElementById('container')
)

var globals = {};

/*
 * Represents a state in the game
 * @param old [State]: old state to intialize the new state
 */
var State = function(old) {

  /*
   * public : the player who has the turn to player
   */
  this.turn = "";

  /*
   * public : the number of moves of the AI player
   */
  this.oMovesCount = 0;

  /*
   * public : the result of the game in this State
   */
  this.result = "still running";

  /*
   * public : the board configuration in this state
   */
  this.board = [];

  /* Begin Object Construction */
  if (typeof old !== "undefined") {
    // if the state is constructed using a copy of another state
    var len = old.board.length;
    this.board = new Array(len);
    for (var itr = 0; itr < len; itr++) {
      this.board[itr] = old.board[itr];
    }

    this.oMovesCount = old.oMovesCount;
    this.result = old.result;
    this.turn = old.turn;
  }
  /* End Object Construction */

  /*
   * public : advances the turn in a the state
   */
  this.advanceTurn = function() {
    this.turn = this.turn === "X" ? "O" : "X";
  }

  /*
   * public function that enumerates the empty cells in state
   * @return [Array]: indices of all empty cells
   */
  this.emptyCells = function() {
    var indxs = [];
    for (var itr = 0; itr < 9; itr++) {
      if (this.board[itr] === "E") {
        indxs.push(itr);
      }
    }
    return indxs;
  }

  /*
   * public  function that checks if the state is a terminal state or not
   * the state result is updated to reflect the result of the game
   * @returns [Boolean]: true if it's terminal, false otherwise
   */

  this.isTerminal = function() {
    var B = this.board;

    //check rows
    for (var i = 0; i <= 6; i = i + 3) {
      if (B[i] !== "E" && B[i] === B[i + 1] && B[i + 1] == B[i + 2]) {
        this.result = B[i] + "-won"; //update the state result
        return true;
      }
    }

    //check columns
    for (var i = 0; i <= 2; i++) {
      if (B[i] !== "E" && B[i] === B[i + 3] && B[i + 3] === B[i + 6]) {
        this.result = B[i] + "-won"; //update the state result
        return true;
      }
    }

    //check diagonals
    for (var i = 0, j = 4; i <= 2; i = i + 2, j = j - 2) {
      if (B[i] !== "E" && B[i] == B[i + j] && B[i + j] === B[i + 2 * j]) {
        this.result = B[i] + "-won"; //update the state result
        return true;
      }
    }

    var available = this.emptyCells();
    if (available.length == 0) {
      //the game is draw
      this.result = "draw"; //update the state result
      return true;
    } else {
      return false;
    }
  };

};

/*
 * Constructs a game object to be played
 * @param autoPlayer [AIPlayer] : the AI player to be play the game with
 */
var Game = function(autoPlayer) {

  //public : initialize the ai player for this game
  this.ai = autoPlayer;

  // public : initialize the game current state to empty board configuration
  this.currentState = new State();

  //"E" stands for empty board cell
  this.currentState.board = ["E", "E", "E",
    "E", "E", "E",
    "E", "E", "E"
  ];

  this.currentState.turn = "X"; //X plays first

  /*
   * initialize game status to beginning
   */
  this.status = "beginning";

  /*
   * public function that advances the game to a new state
   * @param _state [State]: the new state to advance the game to
   */
  this.advanceTo = function(_state, turn = 'user', startCom = false) {
    this.currentState = _state;

    if (_state.isTerminal()) {
      this.status = "ended";
      if (_state.result === "X-won")
        this.dashboard = (turn == 'user') ? 'You Won!' : 'You lost!';
      else if (_state.result === "O-won")
        this.dashboard = (turn == 'user') ? 'You Lost!' : 'You Won!';
      else
        this.dashboard = 'Draw!';
    } else {
      //the game is still running
      if (turn == 'user') {
        if (this.currentState.turn !== "X") this.ai.notify("O", turn);
      } else {
        if (this.currentState.turn === "X") this.ai.notify("X", turn, startCom);
      }
    }
  };

  /*
   * starts the game
   */
  this.start = function(turn = 'user', startCom = false) {
    if (this.status = "beginning") {
      //invoke advanceTo with the initial state
      if (turn == 'user') this.advanceTo(this.currentState);
      else this.advanceTo(this.currentState, turn, startCom);
      this.status = "running";
    }
  }

};

/*
 * public static function that calculates the score of the x player in a given terminal state
 * @param _state [State]: the state in which the score is calculated
 * @return [Number]: the score calculated for the human player
 */
Game.score = function(_state) {
  if (_state.result === "X-won") {
    // the x player won
    return 10 - _state.oMovesCount;
  } else if (_state.result === "O-won") {
    //the x player lost
    return -10 + _state.oMovesCount;
  } else {
    //it's a draw
    return 0;
  }
}

/*
 * Constructs an action that the ai player could make
 * @param pos [Number]: the cell position the ai would make its action in
 * made that action
 */
var AIAction = function(pos) {

  // public : the position on the board that the action would put the letter on
  this.movePosition = pos;

  //public : the minimax value of the state that the action leads to when applied
  this.minimaxVal = 0;

  /*
   * public : applies the action to a state to get the next state
   * @param state [State]: the state to apply the action to
   * @return [State]: the next state
   */
  this.applyTo = function(state, turn = 'user') {
    var next = new State(state);

    //put the letter on the board
    next.board[this.movePosition] = state.turn;

    if (turn == 'user') {
      if (state.turn === "O")
        next.oMovesCount++;
    } else {
      if (state.turn === "X")
        next.oMovesCount++;
    }
    next.advanceTurn();
    return next;
  }
};

/*
 * public static function that defines a rule for sorting AIActions in ascending manner
 * @param firstAction [AIAction] : the first action in a pairwise sort
 * @param secondAction [AIAction]: the second action in a pairwise sort
 * @return [Number]: -1, 1, or 0
 */
AIAction.ASCENDING = function(firstAction, secondAction) {
  if (firstAction.minimaxVal < secondAction.minimaxVal)
    return -1; //indicates that firstAction goes before secondAction
  else if (firstAction.minimaxVal > secondAction.minimaxVal)
    return 1; //indicates that secondAction goes before firstAction
  else
    return 0; //indicates a tie
}

/*
 * public static function that defines a rule for sorting AIActions in descending manner
 * @param firstAction [AIAction] : the first action in a pairwise sort
 * @param secondAction [AIAction]: the second action in a pairwise sort
 * @return [Number]: -1, 1, or 0
 */
AIAction.DESCENDING = function(firstAction, secondAction) {
  if (firstAction.minimaxVal > secondAction.minimaxVal)
    return -1; //indicates that firstAction goes before secondAction
  else if (firstAction.minimaxVal < secondAction.minimaxVal)
    return 1; //indicates that secondAction goes before firstAction
  else
    return 0; //indicates a tie
}

/*
 * Constructs an AI player with a specific level of intelligence
 * @param level [String]: the desired level of intelligence
 */
var AI = function(level) {

  //private attribute: level of intelligence the player has
  var levelOfIntelligence = level;

  //private attribute: the game the player is playing
  var game = {};

  /*
   * private recursive function that computes the minimax value of a game state
   * @param state [State] : the state to calculate its minimax value
   * @returns [Number]: the minimax value of the state
   */
  function minimaxValue(state, turn = 'user') {
    if (state.isTerminal()) {
      //a terminal game state is the base case
      return Game.score(state);
    } else {
      var stateScore; // this stores the minimax value we'll compute

      if (state.turn === "X")
      // X wants to maximize --> initialize to a value smaller than any possible score
        stateScore = -1000;
      else
      // O wants to minimize --> initialize to a value larger than any possible score
        stateScore = 1000;

      var availablePositions = state.emptyCells();

      //enumerate next available states using the info form available positions
      var availableNextStates = availablePositions.map(function(pos) {
        var action = new AIAction(pos);

        var nextState = action.applyTo(state, turn);

        return nextState;
      });

      /* calculate the minimax value for all available next states
       * and evaluate the current state's value */
      availableNextStates.forEach(function(nextState) {
        var nextScore = minimaxValue(nextState);
        if (state.turn === "X") {
          // X wants to maximize --> update stateScore iff nextScore is larger
          if (nextScore > stateScore)
            stateScore = nextScore;
        } else {
          // O wants to minimize --> update stateScore iff nextScore is smaller
          if (nextScore < stateScore)
            stateScore = nextScore;
        }
      });

      return stateScore;
    }
  }

  /*
   * private function: make the ai player take a blind move
   * that is: choose the cell to place its symbol randomly
   * @param turn [String]: the player to play, either X or O
   */
  function takeABlindMove(turn) {
    var available = game.currentState.emptyCells();
    var randomCell = available[Math.floor(Math.random() * available.length)];
    var action = new AIAction(randomCell);

    var next = action.applyTo(game.currentState);

    //ui.insertAt(randomCell, turn);

    game.advanceTo(next);
  }

  /*
   * private function: make the ai player take a novice move,
   * that is: mix between choosing the optimal and suboptimal minimax decisions
   * @param turn [String]: the player to play, either X or O
   */
  function takeANoviceMove(turn) {
    var available = game.currentState.emptyCells();

    //enumerate and calculate the score for each available actions to the ai player
    var availableActions = available.map(function(pos) {
      var action = new AIAction(pos); //create the action object
      var nextState = action.applyTo(game.currentState); //get next state by applying the action

      action.minimaxVal = minimaxValue(nextState); //calculate and set the action's minimax value

      return action;
    });

    //sort the enumerated actions list by score
    if (turn === "X")
    //X maximizes --> sort the actions in a descending manner to have the action with maximum minimax at first
      availableActions.sort(AIAction.DESCENDING);
    else
    //O minimizes --> sort the actions in an ascending manner to have the action with minimum minimax at first
      availableActions.sort(AIAction.ASCENDING);

    /*
     * take the optimal action 40% of the time, and take the 1st suboptimal action 60% of the time
     */
    var chosenAction;
    if (Math.random() * 100 <= 40) {
      chosenAction = availableActions[0];
    } else {
      if (availableActions.length >= 2) {
        //if there is two or more available actions, choose the 1st suboptimal
        chosenAction = availableActions[1];
      } else {
        //choose the only available actions
        chosenAction = availableActions[0];
      }
    }
    var next = chosenAction.applyTo(game.currentState);

    //ui.insertAt(chosenAction.movePosition, turn);

    game.advanceTo(next);
  };

  /*
   * private function: make the ai player take a master move,
   * that is: choose the optimal minimax decision
   * @param turn [String]: the player to play, either X or O
   */
  function takeAMasterMove(turn, play = 'user', comStart = false) {
    var available = game.currentState.emptyCells();
    //enumerate and calculate the score for each avaialable actions to the ai player
    if (!comStart) {
      var availableActions = available.map(function(pos) {
        var action = new AIAction(pos); //create the action object
        var next = action.applyTo(game.currentState, play); //get next state by applying the action
        action.minimaxVal = minimaxValue(next); //calculate and set the action's minmax value
        return action;
      });
      //sort the enumerated actions list by score
      if (turn === "X")
      //X maximizes --> sort the actions in a descending manner to have the action with maximum minimax at first
        availableActions.sort(AIAction.DESCENDING);
      else
      //O minimizes --> sort the actions in an ascending manner to have the action with minimum minimax at first
        availableActions.sort(AIAction.ASCENDING);

      //take the first action as it's the optimal
      var chosenAction = availableActions[0];
    } else {
      var chosenAction = new AIAction(0);
    }

    var next = chosenAction.applyTo(game.currentState);
    console.log(chosenAction.movePosition);
    //ui.insertAt(chosenAction.movePosition, turn);

    game.advanceTo(next, play);
  }

  /*
   * public method to specify the game the ai player will play
   * @param _game [Game] : the game the ai will play
   */
  this.plays = function(_game) {
    game = _game;
  };

  /*
   * public function: notify the ai player that it's its turn
   * @param turn [String]: the player to play, either X or O
   */
  this.notify = function(turn, play = 'user', startCom = false) {
    if (play == 'user') {
      switch (levelOfIntelligence) {
        //invoke the desired behavior based on the level chosen
        case "blind":
          takeABlindMove(turn);
          break;
        case "novice":
          takeANoviceMove(turn);
          break;
        case "master":
          takeAMasterMove(turn);
          break;
      }
    } else {
      switch (levelOfIntelligence) {
        //invoke the desired behavior based on the level chosen
        case "blind":
          takeABlindMove(turn, 'com');
          break;
        case "novice":
          takeANoviceMove(turn, 'com');
          break;
        case "master":
          takeAMasterMove(turn, 'com', startCom);
          break;
      }
    }

  };
};

              
            
!
999px

Console