diff --git a/elm-examples/vier-gewinnt/.gitignore b/elm-examples/vier-gewinnt/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..aee981063c664d20bd7bed367b6ebbe69b44819e --- /dev/null +++ b/elm-examples/vier-gewinnt/.gitignore @@ -0,0 +1 @@ +/elm-stuff/ diff --git a/elm-examples/vier-gewinnt/README.md b/elm-examples/vier-gewinnt/README.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2e91c4a29b02deb913cccaf2fc62ef14a7e50420 100644 --- a/elm-examples/vier-gewinnt/README.md +++ b/elm-examples/vier-gewinnt/README.md @@ -0,0 +1,20 @@ +# VierGewinnt + +This is an elm implementation of the game "Vier gewinnt" aka "Connect Four" where 2 players place tokens in a 7 x 6 grid and try to connect four of their own tokens in a line. + +## Building + +This project can be build by running: + +``` sh +elm make src/Main.elm +``` + +The resulting file is `index.html` which lets you play the game in your web browser. + +## Project structure + +- `elm.json` contains basic elm settings and the needed dependencies +- `src/` + - `Main.elm` contains the entry point and is responsible for building the html response and handling input + - `VierGewinnt.elm` contains data types and functions for the game logic \ No newline at end of file diff --git a/elm-examples/vier-gewinnt/elm.json b/elm-examples/vier-gewinnt/elm.json new file mode 100644 index 0000000000000000000000000000000000000000..b8ff8c92a8adf2d3ab75bedd82f331a440f8eb6b --- /dev/null +++ b/elm-examples/vier-gewinnt/elm.json @@ -0,0 +1,24 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.1", + "elm/core": "1.0.2", + "elm/html": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} \ No newline at end of file diff --git a/elm-examples/vier-gewinnt/src/Main.elm b/elm-examples/vier-gewinnt/src/Main.elm new file mode 100644 index 0000000000000000000000000000000000000000..752579e03188839919804bd808f0ae019288c8b5 --- /dev/null +++ b/elm-examples/vier-gewinnt/src/Main.elm @@ -0,0 +1,128 @@ +import Browser +import Html exposing (Html, button, div, text) +import Html.Events exposing (onClick) +import Html.Attributes exposing (align, style, colspan, disabled) +import Tuple exposing (first, second) +import List exposing (length) +import VierGewinnt as VG + + +main = + Browser.sandbox { init = init, update = update, view = view } + + +-- MODEL + + +type alias Model = + { grid : VG.Grid + , active_player : VG.Player + , finishedState : VG.FinishedState + , history : List Int + } + +-- Initializes the game with an empty grid and Yellow as the starting player +init : Model +init = { grid = VG.empty_grid + , active_player = VG.Yellow + , finishedState = VG.Unfinished + , history = [] + } + + +-- UPDATE + + +type Message = PlaceToken Int | Undo | Restart + +-- Updates the model by placing the token at the desired position, +-- switching the player and checking, if the game is finished +update : Message -> Model -> Model +update message model = + case message of + PlaceToken column -> + if model.finishedState == VG.Unfinished + then case VG.place_token model.grid model.active_player column of + Just new_grid -> { grid = new_grid + , active_player = VG.other_player model.active_player + , finishedState = VG.getFinishedState new_grid + , history = column :: model.history + } + Nothing -> model + else model + Undo -> case model.history of + col :: his -> -- There are moves to undo + case VG.remove_token model.grid col of + Just old_grid -> { grid = old_grid + , active_player = VG.other_player model.active_player + , finishedState = VG.getFinishedState old_grid + , history = his + } + Nothing -> model -- Undo unsuccessful. This shouldn't occur. + [] -> model -- Nothing to undo + Restart -> init + + + + +-- VIEW + +-- Displays the current player (or an ending message, if the game is finished), +-- the grid and the buttons which can be used to place tokens +view : Model -> Html Message +view model = + div [] + [ div [] [ text (startText model)] + , Html.tbody [] + (List.map (\n -> Html.tr [] (gridRow model.grid n)) + (List.reverse (List.range 0 5)) + ++ [ Html.tr [] (List.map (placeButton model) (List.range 0 6)) ] + ++ [ Html.tr [] [ Html.br [] [] ] ] + ++ [ Html.tr [] + [ Html.td + [ colspan 4, align "left"] + [ button [ onClick Undo, disabled (List.isEmpty model.history) ] [ text "Undo" ] ] + , Html.td + [ colspan 3, align "right"] + [ button [ onClick Restart ] [ text "Restart" ] ] + ] + ] + ) + ] + + +-- Returns the text for the starting line (either the current player or an +-- ending message, if the game is finished) +startText : Model -> String +startText model = case model.finishedState of + VG.Unfinished -> playerText model.active_player ++ ", place your token" + VG.YellowWon -> "Yellow has won! Click on restart to start a new game" + VG.RedWon -> "Red has won! Click on restart to start a new game" + VG.Draw -> "Draw! Click on restart to start a new game" + +-- Returns a list containing table entries that represent a field in the grid row each +gridRow : VG.Grid -> Int -> List (Html Message) +gridRow grid position_in_column = + List.map + (\column -> + case VG.get_token [column] 0 position_in_column of + Just player -> Html.td [align "center", style "background-color" (playerText player) ] [ text "O" ] + Nothing -> Html.td [align "center", style "background-color" "White" ] [ text "O" ]) + grid + +-- Returns a table entry containing a button that creates a place token message for the given column number +-- The button is disabled, if the game is finished or the column is full +placeButton : Model -> Int -> Html Message +placeButton model column = + Html.td [] [ button [ onClick (PlaceToken column) + , disabled (model.finishedState /= VG.Unfinished || + VG.get_token model.grid column 5 /= Nothing) + ] + [ text "Place" ] + ] + +-- Return the player name as a String +playerText : VG.Player -> String +playerText player = + case player of VG.Yellow -> "Yellow" + VG.Red -> "Red" \ No newline at end of file diff --git a/elm-examples/vier-gewinnt/src/VierGewinnt.elm b/elm-examples/vier-gewinnt/src/VierGewinnt.elm new file mode 100644 index 0000000000000000000000000000000000000000..b19174b2e05616802e983715d767e59c70d9fd78 --- /dev/null +++ b/elm-examples/vier-gewinnt/src/VierGewinnt.elm @@ -0,0 +1,238 @@ +module VierGewinnt exposing (Player(..), Grid, FinishedState(..), other_player, empty_grid, place_token, remove_token, get_token, get_winner, getFinishedState) + +import List exposing (length) + + + + +-- TYPES + + +-- The color of a player +type Player = Yellow | Red + +-- The grid of the game +type alias Grid = List Column + +-- A column in a grid +-- A token that is represented by the player that owns it +type alias Column = List Player + +{- Denotes the state of a grid, i.e. if further tokens can be placed or how it has ended. + Should be + Unfinished, if there are no four connecting tokens and the game can still be played + YellowWon, if the player with the yellow tokens has won the game + RedWon, if the player with the red tokens has won the game + Draw, if there are no four connecting tokens and no further token can be placed +-} +type FinishedState = Unfinished | YellowWon | RedWon | Draw + + + + +-- CONSTRUCTION FUNCTIONS + + +-- Returns the player that is not the given player +other_player : Player -> Player +other_player player = + case player of + Yellow -> Red + Red -> Yellow + +-- Creates an empty grid with seven rows and without any tokens. +empty_grid : Grid +empty_grid = List.repeat 7 empty_column + +-- Creates an empty column without any tokens. +empty_column : Column +empty_column = [] + + + + +-- WORK WITH GRID + + +-- Places a token for a given player in a given column of a given grid. +-- Returns just the new grid on success or +-- nothing on failure +place_token : Grid -> Player -> Int -> Maybe Grid +place_token grid player column_number = -- Ensure column number is not negative + if column_number < 0 then Nothing else place_token2 grid player column_number +place_token2 : Grid -> Player -> Int -> Maybe Grid +place_token2 grid player column_number = + case grid of + [] -> Nothing -- Column number is too large + column :: columns -> + if column_number == 0 + then case place_token_in_column column player 0 of + Just new_column -> Just (new_column :: columns) + Nothing -> Nothing + else case place_token2 columns player (column_number - 1) of + Just new_columns -> Just (column :: new_columns) + Nothing -> Nothing + +-- Places a token for a given player in a given column. +-- Call with integer value 0. +-- Returns just the new column on success or +-- nothing on failure +place_token_in_column : Column -> Player -> Int -> Maybe Column +place_token_in_column column player position = + if position >= 6 + then Nothing -- Column is already full + else case column of + [] -> Just [player] + token :: tokens -> + case place_token_in_column tokens player (position + 1) of + Just col -> Just (token :: col) + Nothing -> Nothing + +-- Removes the top token from a given column of a given grid. +-- Returns just the new grid on success or +-- nothing on failure +remove_token : Grid -> Int -> Maybe Grid +remove_token grid column_number = -- Ensure column number is not negative + if column_number < 0 then Nothing else remove_token2 grid column_number +remove_token2 : Grid -> Int -> Maybe Grid +remove_token2 grid column_number = + case grid of + [] -> Nothing -- Column number is too large + column :: columns -> + if column_number == 0 + then case remove_token_from_column column of + Just new_column -> Just (new_column :: columns) + Nothing -> Nothing + else case remove_token2 columns (column_number - 1) of + Just new_columns -> Just (column :: new_columns) + Nothing -> Nothing + +-- Removes the top token from a given column. +-- Returns just the new column on success or +-- nothing on failure +remove_token_from_column : Column -> Maybe Column +remove_token_from_column column = + case column of + [] -> Nothing -- Column is already empty + token :: tokens -> + case remove_token_from_column tokens of + Nothing -> Just [] -- This was the top token + Just col -> Just (token :: col) + +-- Get token at a given position of a given grid. +-- The positions start with (0,0) at bottom left. +-- Returns just the player that owns the token or +-- nothing if there is no token at that position or the position is invalid +get_token : Grid -> Int -> Int -> Maybe Player +get_token grid column_number position_in_column = -- Ensure column number and position in column are not negative + if column_number < 0 || position_in_column < 0 then Nothing else get_token2 grid column_number position_in_column +get_token2 : Grid -> Int -> Int -> Maybe Player +get_token2 grid column_number position_in_column = + case grid of + [] -> Nothing + column :: columns -> + if column_number == 0 + then get_token_in_column column position_in_column + else get_token2 columns (column_number - 1) position_in_column + +-- Get token at a given position of a given column. +-- Returns just the player that owns the token or +-- nothing if there is no token at that position +get_token_in_column : Column -> Int -> Maybe Player +get_token_in_column column position = + case column of + [] -> Nothing + token :: tokens -> + if position == 0 + then Just token + else get_token_in_column tokens (position - 1) + + + + +-- WINNING CONDITION + + +-- Returns a list of all players that have four tokes in a line +get_winner : Grid -> List Player +get_winner grid = player_is_winner grid Yellow ++ player_is_winner grid Red + +-- Returns a list containing the given player if that player has four tokes in a line +player_is_winner : Grid -> Player -> List Player +player_is_winner grid player = + if player_wins_vertical grid player || + player_wins_horizontal grid player || + player_wins_diagonal grid player || + player_wins_diagonal (List.reverse grid) player + then [player] + else [] + +-- Returns True, if the given player has four connecting tokens, or false otherwise +player_is_winnerB : Grid -> Player -> Bool +player_is_winnerB grid player = not (List.isEmpty (player_is_winner grid player)) + + +-- Checks if a list of tokens contains four tokens of the given player without another token in between +four_in_a_line : Player -> List Player -> Int -> Bool +four_in_a_line player line counter = + case line of + [] -> False + token :: tokens -> + if token == player + then if counter >= 3 + then True -- We have four in a line + else four_in_a_line player tokens (counter + 1) + else four_in_a_line player tokens 0 -- Reset counter + +-- Checks if the given player has four tokes in a vertical line +player_wins_vertical : Grid -> Player -> Bool +player_wins_vertical grid player = + case grid of + [] -> False + column :: columns -> four_in_a_line player column 0 || player_wins_vertical columns player + +-- Checks if the given player has four tokes in a horizontal line +player_wins_horizontal : Grid -> Player -> Bool +player_wins_horizontal grid player = + if List.all List.isEmpty grid + then False -- No tokens in the grid + else let (first_row,shortened_grid) = -- Collect the bottom row of the grid + List.foldl (\column collection -> + let (fr, sg) = collection + in case column of + [] -> (other_player player :: fr, [] :: sg) + token :: tokens -> (token :: fr, tokens :: sg) + ) ([],[]) grid + in if four_in_a_line player first_row 0 + then True + else player_wins_horizontal shortened_grid player + +-- Checks if the given player has four tokes in a diagonal line (direction: down and right) +player_wins_diagonal : Grid -> Player -> Bool +player_wins_diagonal grid player = player_wins_horizontal (diagonal_to_horizontal grid (other_player player)) player + +-- Places tokens from the player under the columns to have all diagonal lines (direction: down and right) arranged horizontally +diagonal_to_horizontal : Grid -> Player -> Grid +diagonal_to_horizontal grid player = + case grid of + [] -> [] + column :: columns -> column :: diagonal_to_horizontal (List.map (\c -> player :: c) columns) player + +-- Checks, if the game is finished and returns the appropriate FinishedState +getFinishedState : Grid -> FinishedState +getFinishedState grid = + if player_is_winnerB grid Yellow + then YellowWon + else if player_is_winnerB grid Red + then RedWon + else if isFilled grid + then Draw + else Unfinished + +-- Checks, if the grid is completely filled (no further token can be placed) +isFilled : Grid -> Bool +isFilled grid = + case grid of + ps :: pss -> (length ps) >= 6 && isFilled pss + [] -> True + \ No newline at end of file