{- Simulation of the Snake game. Copyright (C) 2020 Max Schröder This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . These are the rules: - A snake is moving on a fixed-sized board. - The snake follows the directions of the strategy. - The snake can continue to move in the same direction or turn left/right but it cannot turn over. - If the strategy suggests an invalid direction, the snake keeps the previous direction. - Food is randomly generated on empty cells on the board. There is always exactly one piece of food on the board at a time. - The snake grows by one cell if it picks up a piece of food. - The game is immediately lost if the head of the snake collides with the border or the body of the snake. - The game is won if the snake has grown so long that it covers all cells. This module exports the following functions: - simulateGame: Simulates a snake game and returns all states of the game. - generateFood: Determines a random empty cell for food. -} module Wettbewerb.Snake (simulateGame, generateFood) where import Prelude hiding (Either(..)) import Data.Sequence (Seq((:|>)), (<|)) import System.Random (mkStdGen, randoms, randomR) import Wettbewerb.Types -- Simulates a snake game. simulateGame :: Board -- board dimensions -> State -- initial state -> StrategyParams a -- strategy -> Int -- random number -> [State] -- list of all states simulateGame board state (strategy, obj) seed -- finish if game is over | isGameOver board (parts $ snake state) nextCell = [state] -- simulate the following states | otherwise = state : simulateGame board nextState (strategy, obj') rand3 where (rand1:rand2:rand3:_) = randoms (mkStdGen seed) prevDir = direction state (strategyDir, obj') = strategy board state rand1 obj -- keep previous direction if strategy returns invalid direction dir = if isOpposite strategyDir prevDir then prevDir else strategyDir nextCell = getNextCell (snakeHead $ snake state) dir nextState = getNextState board state nextCell dir rand2 -- Checks whether the game is over, -- i. e. the snake will collide with the border or itself. isGameOver :: Board -- board dimensions -> Seq Position -- snake parts -> Position -- next cell the snake attempts to move to -> Bool -- true if the game is over isGameOver (width, height) snakeParts pos@(x, y) = isBorder || isSnakePart where isBorder = x < 0 || x >= width || y < 0 || y >= height isSnakePart = pos `elem` snakeParts -- Determines the next cell, given the current position and a direction. getNextCell :: Position -> Direction -> Position getNextCell (x, y) Left = (x - 1, y) getNextCell (x, y) Right = (x + 1, y) getNextCell (x, y) Up = (x, y - 1) getNextCell (x, y) Down = (x, y + 1) -- Determines the next state of the game. getNextState :: Board -- board dimensions -> State -- current state -> Position -- next cell the snake should move to -> Direction -- direction in which the snake should move -> Int -- random number for generating a random food position -> State -- next state getNextState board (State snake foodPos _) nextCell direction rand = State snake' foodPos' direction where -- make next cell the head of the snake snakeParts' @ (init :|> _) = nextCell <| parts snake -- Remove tail cell from the snake if the snake is not eating. -- Otherwise, let the snake grow. snakeParts'' | isEating snake = snakeParts' | otherwise = init snake' = Snake snakeParts'' (nextCell == foodPos) -- generate food if the snake ate foodPos' | nextCell == foodPos = generateFood board snakeParts'' rand | otherwise = foodPos -- Determines a random empty cell for food. generateFood :: Board -- board dimensions -> Seq Position -- snake parts -> Int -- random number -> Position -- positions of the next food cell generateFood (width, height) snakeParts rand | null emptyCells = (0, 0) -- the game is won, fall back to an arbitrary position | otherwise = emptyCells !! randIndex -- pick random empty cell where cells = [(x, y) | x <- [0..width - 1], y <- [0..height - 1]] emptyCells = filter (`notElem` snakeParts) cells randIndex = fst $ randomR (0, length emptyCells - 1) (mkStdGen rand)