Initial demo for a guessing game (n ? it's more : it's less)
This commit is contained in:
commit
c2b4452df4
12 changed files with 336 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
dist*/
|
||||
.ghc.environment.*
|
||||
PlusOuMoins.hs
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Revision history for PlusOuMoins
|
||||
|
||||
## 0.1.0.0 -- YYYY-mm-dd
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
30
LICENSE
Normal file
30
LICENSE
Normal file
|
@ -0,0 +1,30 @@
|
|||
Copyright (c) 2019, Tissevert
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Tissevert nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
35
PlusOuMoins.cabal
Normal file
35
PlusOuMoins.cabal
Normal file
|
@ -0,0 +1,35 @@
|
|||
cabal-version: >=1.10
|
||||
-- Initial package description 'PlusOuMoins.cabal' generated by 'cabal
|
||||
-- init'. For further documentation, see
|
||||
-- http://haskell.org/cabal/users-guide/
|
||||
|
||||
name: PlusOuMoins
|
||||
version: 0.1.0.0
|
||||
synopsis: A game where one has to guess a number
|
||||
-- description:
|
||||
homepage: https://git.marvid.fr/Tissevert/PlusOuMoins
|
||||
-- bug-reports:
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Tissevert
|
||||
maintainer: tissevert+devel@marvid.fr
|
||||
-- copyright:
|
||||
category: Game
|
||||
build-type: Simple
|
||||
extra-source-files: CHANGELOG.md
|
||||
|
||||
executable PlusOuMoins
|
||||
main-is: Main.hs
|
||||
other-modules: Level
|
||||
, Menu
|
||||
, MainMenu
|
||||
, Mode
|
||||
, Utils
|
||||
-- other-extensions:
|
||||
build-depends: base >=4.11 && <4.12
|
||||
, ansi-terminal
|
||||
, mtl
|
||||
, random
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
2
Setup.hs
Normal file
2
Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
50
monolith.sh
Executable file
50
monolith.sh
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/bin/bash
|
||||
|
||||
SRCDIR=src
|
||||
MODULES=(Utils Menu Level Mode MainMenu)
|
||||
MAIN_MODULE=Main
|
||||
|
||||
PRAGMA_PATTERN='^{-# LANGUAGE [a-zA-Z]\+ #-}$'
|
||||
IMPORT_PATTERN='^import '
|
||||
|
||||
sourceFile()
|
||||
{
|
||||
printf "${SRCDIR}/${1}.hs"
|
||||
}
|
||||
|
||||
pragmas()
|
||||
{
|
||||
grep -rh "${PRAGMA_PATTERN}" "${SRCDIR}" | sort -u
|
||||
}
|
||||
|
||||
imports()
|
||||
{
|
||||
local modulesPattern="$(echo ${MODULES[@]} | sed 's: :\\|:g')"
|
||||
grep -rh "${IMPORT_PATTERN}" "${SRCDIR}" | grep -v "\(${modulesPattern}\)" | sort -u
|
||||
}
|
||||
|
||||
body()
|
||||
{
|
||||
grep -vh "${PRAGMA_PATTERN}" "$(sourceFile "${1}")" |\
|
||||
uncork |\
|
||||
grep -v "${IMPORT_PATTERN}" |\
|
||||
cat -s
|
||||
}
|
||||
|
||||
uncork()
|
||||
{
|
||||
LINE="null"
|
||||
while ([ -z "${LINE}" ] || [ -n "${LINE%%module*}" ]); do read LINE; done
|
||||
while [ -n "${LINE}" ]; do read LINE; done
|
||||
cat
|
||||
}
|
||||
|
||||
pragmas
|
||||
grep '^module' "$(sourceFile ${MAIN_MODULE})"
|
||||
printf "\n"
|
||||
imports
|
||||
for module in ${MODULES[@]}
|
||||
do
|
||||
body "${module}"
|
||||
done
|
||||
body "${MAIN_MODULE}"
|
21
src/Level.hs
Normal file
21
src/Level.hs
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Level (Level(..), bounds) where
|
||||
|
||||
import Menu (Menu(..), Promptable(..))
|
||||
|
||||
data Level = Easy | Normal | Hard deriving (Enum, Bounded)
|
||||
|
||||
instance Promptable Level where
|
||||
menu = Menu {
|
||||
title = "Choix du niveau de difficulte"
|
||||
, question = "Entrez le numero du mode qui vous interesse : "
|
||||
, invalidText = "Votre choix n'est pas valide, merci de taper 1, 2 ou 3 selon le niveau de difficulte que vous souhaitez activer."
|
||||
}
|
||||
|
||||
showOption Easy = "Facile : le nombre a trouver est compris entre 1 et 100"
|
||||
showOption Normal = "Moyen : le nombre a trouver est compris entre 1 et 1000"
|
||||
showOption Hard = "Difficile : le nombre a trouver est compris entre 1 et 10000"
|
||||
|
||||
bounds :: Level -> (Int, Int)
|
||||
bounds Easy = (1, 100)
|
||||
bounds Normal = (1, 1000)
|
||||
bounds Hard = (1, 10000)
|
101
src/Main.hs
Normal file
101
src/Main.hs
Normal file
|
@ -0,0 +1,101 @@
|
|||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
module Main where
|
||||
|
||||
import Control.Monad.IO.Class (MonadIO(..))
|
||||
import Control.Monad.State (StateT, evalStateT, gets, modify, put)
|
||||
import Level (Level(..), bounds)
|
||||
import Menu (formatTitle, select)
|
||||
import MainMenu (MainMenu(..))
|
||||
import Mode (Mode(..))
|
||||
import System.Console.ANSI (clearScreen)
|
||||
import System.Random (randomRIO)
|
||||
import Text.Printf (printf)
|
||||
import Utils (outputLn, prompt)
|
||||
|
||||
data Settings = Settings {
|
||||
level :: Level
|
||||
, mode :: Mode
|
||||
}
|
||||
|
||||
defaultSettings :: Settings
|
||||
defaultSettings = Settings {level = Easy, mode = OnePlayer}
|
||||
|
||||
type Game = StateT Settings IO
|
||||
|
||||
initPlay :: Settings -> Game Int
|
||||
initPlay (Settings {mode, level}) = do
|
||||
liftIO clearScreen
|
||||
case mode of
|
||||
OnePlayer -> initOnePlayer $ bounds level
|
||||
PlayerVsPlayer -> initPlayerVsPlayer $ bounds level
|
||||
|
||||
initOnePlayer :: (Int, Int) -> Game Int
|
||||
initOnePlayer range@(minNumber, maxNumber) = do
|
||||
mapM_ outputLn $ formatTitle "Mode un joueur, vous jouez contre moi."
|
||||
outputLn (
|
||||
printf "J'ai choisi un nombre compris entre %d et %d, a vous de deviner lequel" minNumber maxNumber
|
||||
)
|
||||
liftIO $ randomRIO range
|
||||
|
||||
initPlayerVsPlayer :: (Int, Int) -> Game Int
|
||||
initPlayerVsPlayer range@(minNumber, maxNumber) = do
|
||||
mapM_ outputLn $ formatTitle "Mode deux joueurs"
|
||||
mapM_ outputLn [
|
||||
printf "L'un des joueurs doit choisir secretement un nombre compris entre %d et %d que le deuxieme joueur va essayer de deviner." minNumber maxNumber
|
||||
, "Écrivez le nombre choisi sans que l'autre joueur ne le voit : "
|
||||
]
|
||||
numberToGuess <- getSecretNumber range
|
||||
liftIO clearScreen
|
||||
outputLn "Le nombre choisi a ete enregistre, c'est au deuxieme joueur de le trouver."
|
||||
return numberToGuess
|
||||
|
||||
getSecretNumber :: MonadIO m => (Int, Int) -> m Int
|
||||
getSecretNumber range = prompt (return ()) (outputLn $ warning range) range
|
||||
where
|
||||
warning = uncurry (printf "Merci de choisir un nombre compris ENTRE %d et %d : ")
|
||||
|
||||
guess :: Int -> (Int, Int) -> StateT Int IO Int
|
||||
guess expected range = do
|
||||
n <- prompt (modify (+1) >> outputLn "Dites un nombre : ") onError range
|
||||
case n of
|
||||
_ | n < expected -> outputLn "C'est plus" >> guess expected range
|
||||
| n > expected -> outputLn "C'est moins" >> guess expected range
|
||||
| otherwise -> outputLn (found n) >> gets id
|
||||
where
|
||||
warning = printf "Pour rappel, le nombre choisi est compris entre %d et %d"
|
||||
onError = outputLn (uncurry warning range)
|
||||
found = printf "Excellent! Il fallait effectivement trouver le nombre %d"
|
||||
|
||||
play :: Game ()
|
||||
play = do
|
||||
numberToGuess <- initPlay =<< gets id
|
||||
range <- gets (bounds . level)
|
||||
steps <- liftIO $ evalStateT (guess numberToGuess range) 0
|
||||
outputLn (printf "Bravo!!! Vous avez trouvé en %s." $ numberOfSteps steps)
|
||||
select >>= dispatch
|
||||
where
|
||||
numberOfSteps 1 = "1 coup"
|
||||
numberOfSteps n = printf "%d coups" n
|
||||
|
||||
settings :: Game ()
|
||||
settings = do
|
||||
level <- select
|
||||
outputLn (printf "Vous jouez en mode %s." $ showLevel level)
|
||||
mode <- select
|
||||
put $ Settings {level, mode}
|
||||
select >>= dispatch
|
||||
where
|
||||
showLevel Easy = "facile"
|
||||
showLevel Normal = "moyen"
|
||||
showLevel Hard = "difficile"
|
||||
|
||||
dispatch :: MainMenu -> Game ()
|
||||
dispatch Play = play
|
||||
dispatch ChangeSettings = settings
|
||||
dispatch Quit = return ()
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
clearScreen
|
||||
putStrLn "=====> JEU DU PLUS OU MOINS <====="
|
||||
evalStateT (select >>= dispatch) defaultSettings
|
15
src/MainMenu.hs
Normal file
15
src/MainMenu.hs
Normal file
|
@ -0,0 +1,15 @@
|
|||
module MainMenu (MainMenu(..)) where
|
||||
|
||||
import Menu (Menu(..), Promptable(..))
|
||||
|
||||
data MainMenu = Play | ChangeSettings | Quit deriving (Enum, Bounded)
|
||||
|
||||
instance Promptable MainMenu where
|
||||
menu = Menu {
|
||||
title = "Menu principal"
|
||||
, question = "Choisissez une action"
|
||||
, invalidText = "Ce choix n'est pas valide"
|
||||
}
|
||||
showOption Play = "Jouer"
|
||||
showOption ChangeSettings = "Changer les réglages"
|
||||
showOption Quit = "Quitter"
|
40
src/Menu.hs
Normal file
40
src/Menu.hs
Normal file
|
@ -0,0 +1,40 @@
|
|||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
module Menu (
|
||||
Menu(..)
|
||||
, Promptable(..)
|
||||
, formatTitle
|
||||
, select
|
||||
) where
|
||||
|
||||
import Control.Monad.IO.Class (MonadIO(..))
|
||||
import Text.Printf (printf)
|
||||
import Utils (outputLn, prompt)
|
||||
|
||||
data Menu a = Menu {
|
||||
title :: String
|
||||
, question :: String
|
||||
, invalidText :: String
|
||||
}
|
||||
|
||||
formatTitle :: String -> [String]
|
||||
formatTitle aTitle = [line, formattedTitle, line]
|
||||
where
|
||||
formattedTitle = "==> " ++ aTitle
|
||||
line = const '-' <$> formattedTitle
|
||||
|
||||
formatOption :: Promptable a => a -> String
|
||||
formatOption o = printf "%d. %s" (fromEnum o) (showOption o)
|
||||
|
||||
class (Bounded a, Enum a) => Promptable a where
|
||||
menu :: Menu a
|
||||
showOption :: a -> String
|
||||
|
||||
select :: forall a m. (MonadIO m, Promptable a) => m a
|
||||
select = toEnum <$> prompt
|
||||
(mapM_ outputLn (formatTitle title ++ options ++ [question]))
|
||||
(outputLn invalidText)
|
||||
(fromEnum (minBound :: a), fromEnum (maxBound :: a))
|
||||
where
|
||||
Menu {title, question, invalidText} = (menu :: Menu a)
|
||||
options = formatOption <$> ([minBound .. maxBound] :: [a])
|
15
src/Mode.hs
Normal file
15
src/Mode.hs
Normal file
|
@ -0,0 +1,15 @@
|
|||
module Mode (Mode(..)) where
|
||||
|
||||
import Menu (Menu(..), Promptable(..))
|
||||
|
||||
data Mode = OnePlayer | PlayerVsPlayer deriving (Enum, Bounded)
|
||||
|
||||
instance Promptable Mode where
|
||||
menu = Menu {
|
||||
title = "Choix du mode de jeu"
|
||||
, question = "Entrez le numero du mode qui vous interesse : "
|
||||
, invalidText = "Votre choix n'est pas valide, merci de taper 1 ou 2 selon le mode de jeu que vous souhaitez activer."
|
||||
}
|
||||
|
||||
showOption OnePlayer = "Mode un joueur"
|
||||
showOption PlayerVsPlayer = "Mode deux joueurs"
|
19
src/Utils.hs
Normal file
19
src/Utils.hs
Normal file
|
@ -0,0 +1,19 @@
|
|||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
module Utils (
|
||||
outputLn
|
||||
, prompt
|
||||
) where
|
||||
|
||||
import Control.Monad.IO.Class (MonadIO(..))
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
outputLn :: MonadIO m => String -> m ()
|
||||
outputLn = liftIO . putStrLn
|
||||
|
||||
prompt :: MonadIO m => m () -> m () -> (Int, Int) -> m Int
|
||||
prompt message onError range@(minVal, maxVal) = do
|
||||
message
|
||||
mn <- readMaybe <$> liftIO getLine
|
||||
case mn of
|
||||
Just n | n >= minVal && n <= maxVal -> return n
|
||||
_ -> onError >> prompt message onError range
|
Loading…
Reference in a new issue