Tic Tac Toe

by boazblake

Level: expert • Mithril.js Version: latest

In this example we can see and play the famous tic-tac-toe game against the computer. The game is using a central model for state management together with Mithril.js' own stream library. Besides that the 3rd-party library Ramda - A practical functional library for JavaScript programmers - is used. Well done and a very nice Mithril.js example.

Live Example

Dependencies

,,
Type Name URL
scriptmithril@latesthttps://unpkg.com/mithril@latest
scriptramda@0.26.1https://unpkg.com/ramda@0.26.1
scriptmithril-stream@2.0.0/stream.jshttps://unpkg.com/mithril-stream@2.0.0/stream.js

HTML

<link href="https://fonts.googleapis.com/css?family=Mansalva&display=swap" rel="stylesheet">

JavaScript

const Stream = m.stream

const calcWidth = mdl => {
  mdl.width(window.innerWidth)
  return mdl
}

const getRandomInt = (min, max) => Math.floor(Math.random() * (1 + max - min) + min)

const players = {
  true: {score:0, mark: 'X' },
  false: {score: 0, mark: 'O'},
}
const boardSizes = R.filter((n) => n % 3 == 0 ,R.range(0,60))


const getDiagAcross =(acc, set, idx) => {
  let r = acc.concat(set[idx])
  return r
}

const getDiagDown =(acc, set, idx) => {
  let r = acc.concat(set[set.length - (idx + 1)])
  return r
}

const winningSetBy = mdl => {
  if(mdl.size) {
    let spaces = R.range(1, (mdl.size * mdl.size + 1))
    let setsAcross = R.splitEvery(mdl.size, spaces)
    let setsDown = R.transpose(setsAcross)
    let setsDiagAcross = setsAcross.reduce(getDiagAcross, [])
    let setsDiagDown = setsDown.reduce(getDiagDown, [])
    let winnings = setsAcross.concat(setsDown).concat([setsDiagAcross].concat([setsDiagDown]))
    return winnings
  } else {
    restart(mdl)
  }
}

const mdl = {
  winnerSets:[],
  winner:null,
  turn: true,
  players,
  board:null,
  size: 0,
  width: Stream(800)
}

const markSelectedSpace = (mdl, key, mark) => {
  const space = R.filter(R.propEq('key', key),mdl.board)
  let updatedSpace = R.set(R.lensProp('value'),mark, R.head(space), mdl.board)
  let index = R.findIndex(R.propEq('key',key))(mdl.board)
  mdl.board =  R.insert(index,updatedSpace,R.without(space,mdl.board))
  return mdl
}

const markRandomSpace = mdl => {
  let emptySpaces = mdl.board.filter(R.propEq('value',''))
  let AISpace = emptySpaces[getRandomInt(0, emptySpaces.length - 1)]
  !mdl.winner && AISpace && markSpace(mdl)(AISpace)
  return mdl
}

const updateTurn = mdl => {
  mdl.turn = !mdl.turn
  return mdl
}

const isWinningSpace = (mdl, key) => {
  let value = R.prop('value',R.head(R.filter(R.propEq('key', key),mdl.board)))
  let sets = R.groupBy(c => c[1])(mdl.board.map(R.props(['key','value'])))
  let keys = R.keys(R.fromPairs(sets[value])).map(Number)
  let isWinner = mdl.winnerSets.map(set => set.every(key => keys.includes(key))).includes(true)
  return isWinner
}

const checkIfDraw = mdl => {
  if(!R.pluck('value',mdl.board).includes('')) {
    mdl.winner = 'No One'
    return mdl
   }
   return mdl
}

const markSpace = mdl => space => {
  let player = mdl.players[mdl.turn].mark
  if(isWinningSpace(markSelectedSpace(mdl, space.key,  player), space.key)) {
    mdl.players[mdl.turn].score ++
    mdl.winner = player
    return mdl
  }
  checkIfDraw(mdl)
  return mdl
}

const nextTurn =  (mdl, space) => {
  return R.compose(updateTurn,
    markRandomSpace,
    updateTurn,
    markSpace(mdl),
  )(space)
  return mdl
}

const restart = mdl => {
  mdl.winner = null
  mdl.size = 0
  mdl.board = null
  mdl.width(800)
}

const makeBoardWithSize = (mdl, size) => {
  mdl.size = size
  mdl.board = R.range(0,size * size).map(n => ({key: n + 1, value: ''}))
  mdl.winnerSets = winningSetBy(mdl)
}

const Space = {
  view: ({attrs:{mdl, key, space}}) =>
   m('.space', {
      style:{
        fontSize:`${(mdl.width()/mdl.size)/2}px`,
        height: `${(mdl.width()/mdl.size)/2}px`,
      flex:`1 1 ${mdl.width()/mdl.size}px`},
      onclick: e => !mdl.winner && !space.value && nextTurn(mdl, space)},
      space.value && m('.value', space.value))
}



const PlayerScore = {
    view : ({attrs:{player, mdl}}) => m('.score-card', [player.mark,':', player.score])
}

const Toolbar = {
  view: ({attrs:{mdl}}) =>
    m('.toolbar', [
      m('button.btn',{onclick: e => restart(mdl)}, 'New Game'),
    Object.keys(mdl.players).map((player, idx) =>
      m(PlayerScore, {key:idx, player: players[player], mdl}))
    ])
}

const Game = {
  view: () =>
    mdl.board ? m('.game', {style:{width:`${mdl.width()}px`}}, mdl.board.map((space) =>
      m(Space, {key:space.key, space, mdl}) ))
      : [ m('h1','select a board size'),
          m('select', {value:mdl.size, onchange: e => makeBoardWithSize(mdl, Number(e.target.value))},
          boardSizes.map(n => m('option', n)),
          mdl.size)]
}

const GameOver = {
  oncreate: () => window.scrollTo(0,0),
  view: ({attrs:{mdl}}) =>
    m('.game-over', {onclick:e => restart(mdl)}, `Game Over ${mdl.winner} is the winner!`)
}

const TicTacToe = {
  view:() => {
    calcWidth(mdl)
    return m('.', [
  m(Toolbar, {mdl}),
  mdl.winner && m(GameOver, {mdl}),
  m(Game, {mdl})
])}
}


m.mount(document.body, TicTacToe)

CSS


* {
  box-sizing: border-box;
  font-family: 'Mansalva';
}

select {
  width: 80px;
  height: 36px;
  font-size: 30px;
}

.toolbar {
  border: 1px solid green;
  display: flex;
  flex-flow: wrap;
  justify-content: space-between;
}

.score-card {
  padding: 10px;
  font-size: 30px
}

.game {
  display: flex;
  flex-flow: wrap;
}


.space {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid green;
  cursor:pointer;
}

.game-over {
  position: absolute;
  top: 15%;
  left: 15%;
  width: 500px;
  font-size: 90px;
  background: #bdc3c7;
  padding: 4px;
  cursor: pointer;
}

.value {
  font-size: inherit;
}

The snippet requires the latest version of Mithril.js framework. As an expert user, who is familiar with all the aspects of Mithril.js and JavaScript itself, you are able the follow the example easily.

In this example we can see some Mithril.js API methods like m.stream or m.mount, besides Mithril.js' basic m() hyperscript function. It is also showing the oncreate hook, which is one of several Mithril.js' lifecycle methods.

The code sample was authored by boazblake. It was last modified on 26 October 2021. Do you want to see another one written by boazblake? Then Click here.

Contribute

Do you see some improvements, that could be addressed here? Then let me know by opening an issue. As an alternative, you can fork the repository on GitHub, push your commits and send a pull request. To start your work, click on the edit link below. Thank you for contributing to this repo.

See more code examples  •  Edit this example on GitHub