Skip to content

Instantly share code, notes, and snippets.

@BinaryMuse
Last active December 7, 2015 15:59
Show Gist options
  • Save BinaryMuse/ba33f8afbbb0edfe5b21 to your computer and use it in GitHub Desktop.
Save BinaryMuse/ba33f8afbbb0edfe5b21 to your computer and use it in GitHub Desktop.

Tic-Tac-Toe implemented in Elm.

You can generate an index.html file with elm make Main.elm.

{
"version": "1.0.0",
"summary": "Tic-Tac-Toe!",
"repository": "https://gist.github.com/BinaryMuse/ba33f8afbbb0edfe5b21",
"license": "MIT",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-html": "4.0.2 <= v < 5.0.0",
"evancz/start-app": "2.0.2 <= v < 3.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}
module Game where
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Array exposing (Array)
import Maybe exposing (withDefault, andThen)
-- Model
type Player
= X
| O
type GameState
= InProgress
| Over
type alias Square = Maybe Player -- `Nothing` means the square is blank
type alias Board = Array (Square)
type alias Model =
{ turn : Player
, gameState : GameState
, winner : Maybe Player -- `Nothing` means nobody's won yet, or there was a tie
, board : Board
}
init : Model
init =
{ turn = X
, gameState = InProgress
, winner = Nothing
, board = Array.repeat 9 Nothing
}
-- Update
type Action
= Play Int
| Reset
update : Action -> Model -> Model
update action model =
case action of
Play index ->
if canPlayAtIndex model index then
let
nextBoard = setSquare index (Just model.turn) model.board
in
{ model | board = nextBoard
, gameState = getNextGameState model.gameState nextBoard
, winner = detectWinner nextBoard
, turn = getNextTurn model.turn }
else
model
Reset ->
init
-- Determines if the any play at the given Board index is valid or not
canPlayAtIndex : Model -> Int -> Bool
canPlayAtIndex model index =
case model.gameState of
Over -> False
_ ->
case getSquare model.board index of
Nothing -> True
Just X -> False
Just O -> False
-- Returns a new Board with the space at `index` replaced with `Square`
setSquare : Int -> Square -> Board -> Board
setSquare index square board =
Array.set index square board
-- With an in-range index, returns a `Just Square` for the Square at that board index.
-- For out of range, returns `Nothing`
getSquare : Board -> Int -> Square
getSquare board index =
Array.get index board |> withDefault Nothing
-- Given a current GameState and a Board, returns what the appropriate
-- next GameState should be.
getNextGameState : GameState -> Board -> GameState
getNextGameState gameState board =
case gameState of
Over -> Over
InProgress ->
case detectWinner board of
Just _ ->
Over
Nothing ->
case boardFull board of
True -> Over
False -> InProgress
-- True if the entire Board is filled with non-Nothing Squares, false otherwise.
boardFull : Board -> Bool
boardFull board =
board |> Array.toList |> List.all (\square -> square /= Nothing)
-- Given a Player, returns the Player that would play next
getNextTurn : Player -> Player
getNextTurn player =
case player of
X -> O
O -> X
-- Figures out which Player is the winner given a Board configuration
detectWinner : Board -> Maybe Player
detectWinner board =
let
getSquareAtIndex = getSquare board
-- the list of Squares at each set of indicies that could possibly indicate a win
squaresPerPossibleWinCombo = possibleWinCombos |> List.map (List.map getSquareAtIndex)
-- the first set of indicies that has a set of Squares that actually indicate a win
firstWin = List.filter isWinningSquareCombo squaresPerPossibleWinCombo |> List.head
in
case firstWin of
Just [Just X, Just X, Just X] -> Just X
Just [Just O, Just O, Just O] -> Just O
_ -> Nothing
isWinningSquareCombo : List (Square) -> Bool
isWinningSquareCombo squares =
case squares of
[Just X, Just X, Just X] -> True
[Just O, Just O, Just O] -> True
_ -> False
-- A list of the possible combinations of indicies that must contain
-- the same non-Nothing Square for someone to win a game.
possibleWinCombos : List (List (Int))
possibleWinCombos =
[ [ 0, 1, 2 ]
, [ 3, 4, 5 ]
, [ 6, 7, 8 ]
, [ 0, 3, 6 ]
, [ 1, 4, 7 ]
, [ 2, 5, 8 ]
, [ 0, 4, 8 ]
, [ 2, 4, 6 ]
]
-- View
view : Signal.Address Action -> Model -> Html
view address model =
div [ style appStyle ] [
h2 [ style headerStyle ] [ text "Tic-Tac-Toe!" ],
renderBoard address model.board,
node "hr" [] [],
renderTurn model,
div [ style buttonStyle ] [
button [ onClick address Reset ] [ text "Reset" ]
]
]
renderBoard : Signal.Address Action -> Board -> Html
renderBoard address board =
div [ id "ttt-board", style boardStyle ](board |> Array.toList |> List.indexedMap (renderBoardSquare address))
renderBoardSquare : Signal.Address Action -> Int -> Square -> Html
renderBoardSquare address index square =
div [
class "ttt-board-square",
style (boardSquareStyle ++ getBoardSquareBorderStyleForIndex index),
attribute "data-index" (index |> toString),
onClick address (Play index)
] [
text (getTextForSquare square)
]
getTextForSquare : Square -> String
getTextForSquare square =
case square of
Just X -> "X"
Just O -> "O"
Nothing -> ""
renderTurn : Model -> Html
renderTurn model =
div [ style labelStyle ] [
span [] [ text (getTurnText model) ]
]
getTurnText : Model -> String
getTurnText model =
case model.gameState of
Over ->
case model.winner of
Just X -> "X wins!"
Just O -> "O wins!"
Nothing -> "It's a tie!"
InProgress ->
case model.turn of
X -> "X's turn"
O -> "O's turn"
type alias Stylesheet = List (String, String)
appStyle : Stylesheet
appStyle =
[ ( "width", "300px" )
, ( "margin", "0 auto" )
]
headerStyle : Stylesheet
headerStyle =
[ ( "text-align", "center" )
]
labelStyle : Stylesheet
labelStyle =
[ ( "text-align", "center" )
, ( "font-weight", "bold" )
, ( "padding", "10px 0" )
]
buttonStyle : Stylesheet
buttonStyle =
[ ( "text-align", "center" )
]
boardStyle : Stylesheet
boardStyle =
[ ( "width", "300px" )
, ( "height", "320px" )
]
boardSquareStyle : Stylesheet
boardSquareStyle =
[ ( "width", "98px" )
, ( "height", "70px" )
, ( "padding-top", "30px" )
, ( "display", "inline-block" )
, ( "text-align", "center" )
, ( "vertical-align", "middle" )
, ( "cursor", "pointer" )
, ( "font-size", "30px" )
]
borderRight = ( "border-right", "1px solid black" )
borderBottom = ( "border-bottom", "1px solid black" )
getBoardSquareBorderStyleForIndex : Int -> Stylesheet
getBoardSquareBorderStyleForIndex index =
if index == 0 || index == 1 || index == 3 || index == 4 then
[ borderRight, borderBottom ]
else if index == 2 || index == 5 then
[ borderBottom ]
else if index == 6 || index == 7 then
[ borderRight ]
else
[]
module Main where
import Game exposing (init, update, view)
import StartApp.Simple exposing (start)
main =
start { model = init, update = update, view = view }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment