98 Commits
0.1.0 ... 0.4.0

Author SHA1 Message Date
Jonathan Lamothe
c7849d3558 version 0.4.0 2019-10-17 09:59:20 -04:00
Jonathan Lamothe
756d0997a8 updated change log 2019-10-16 02:40:09 -04:00
Jonathan Lamothe
569f009dcd Merge pull request #26 from mtlstats/ytd-stats
year-to-date statistics
2019-10-16 02:37:50 -04:00
Jonathan Lamothe
cfe2969106 generate empty game stats report on failure 2019-10-16 02:32:57 -04:00
Jonathan Lamothe
19e0242135 fixed name column spacing 2019-10-16 02:26:42 -04:00
Jonathan Lamothe
32f61ccc89 implemented year-to-date report 2019-10-16 02:23:52 -04:00
Jonathan Lamothe
bfe568492d implemented playerReport
a private function in the Mtlstats.Report module
2019-10-16 02:23:52 -04:00
Jonathan Lamothe
277ba9a9dd implemented playerNameColWidth 2019-10-15 01:03:32 -04:00
Jonathan Lamothe
d338930800 implemented playerIsActive 2019-10-15 00:51:42 -04:00
Jonathan Lamothe
363d0cb2d3 don't scroll past top of page 2019-10-15 00:16:44 -04:00
Jonathan Lamothe
a91ed5afb3 enable scrolling of report 2019-10-11 23:13:00 -04:00
Jonathan Lamothe
db8bbd9786 added scrollOffset field to ProgState 2019-10-11 22:24:27 -04:00
Jonathan Lamothe
c4f68bb29c Merge pull request #25 from mtlstats/pmin-prompt
Prompt user for penalty minutes and assign
2019-10-11 01:17:43 -04:00
Jonathan Lamothe
e2c3b57749 implemented assignPMins 2019-10-11 01:10:50 -04:00
Jonathan Lamothe
3d1f6170f6 implemented assignPMinsPrompt 2019-10-09 22:33:48 -04:00
Jonathan Lamothe
1a481ab49d implemented getPMinsC 2019-10-09 22:24:30 -04:00
Jonathan Lamothe
afd2bac7b5 implemented pMinPlayerPrompt 2019-10-09 21:54:55 -04:00
Jonathan Lamothe
ffe9b7f87f implemented pMinPlayerC 2019-10-09 01:24:55 -04:00
Jonathan Lamothe
e1a48afc5c penalty minutes control framework 2019-10-09 00:58:49 -04:00
Jonathan Lamothe
1810434716 added selectedPlayer and pMinsRecorded fields to GameState 2019-10-09 00:50:10 -04:00
Jonathan Lamothe
146e2e42a1 Merge pull request #24 from mtlstats/gs-totals
Calculate and display total player stats for game
2019-10-09 00:39:58 -04:00
Jonathan Lamothe
a9c036f876 renamed pPoints to psPoints 2019-10-09 00:35:35 -04:00
Jonathan Lamothe
0b249bcdae calculate and display total game stats 2019-10-09 00:30:03 -04:00
Jonathan Lamothe
74fd4fe2fb implemented addPlayerStats 2019-10-09 00:24:34 -04:00
Jonathan Lamothe
5f53413ef7 split report into standings and game stats 2019-10-09 00:01:12 -04:00
Jonathan Lamothe
83f951f7e4 version 0.3.0 2019-10-03 09:29:34 -04:00
Jonathan Lamothe
54a631557e Merge pull request #23 from mtlstats/game-stats
Show game statistics in report
2019-10-03 03:15:22 -04:00
Jonathan Lamothe
8424d5f40c add game stats to report 2019-10-03 03:08:17 -04:00
Jonathan Lamothe
de7f3f7a3e update recordGoalAssists test 2019-10-03 02:29:12 -04:00
Jonathan Lamothe
9d04abecff update awardAssist 2019-10-03 02:28:16 -04:00
Jonathan Lamothe
ad840cca65 awardGoal updates game stats 2019-10-03 02:10:22 -04:00
Jonathan Lamothe
b17e63246f added gamePlayerStats field to GameState 2019-10-03 01:10:44 -04:00
Jonathan Lamothe
9977a73da4 Merge pull request #22 from mtlstats/confirm-ga
Confirm goal/assist data
2019-10-02 02:09:07 -04:00
Jonathan Lamothe
0aa2b49ba2 fixed formatting of goal data confirmation prompt 2019-10-02 01:57:58 -04:00
Jonathan Lamothe
7da4c54e65 implemented resetGoalData 2019-10-02 01:55:07 -04:00
Jonathan Lamothe
4f70c84c6b implemented playerSummary 2019-10-02 01:31:07 -04:00
Jonathan Lamothe
2be7d2bf1d implemented confirmGoalDataC 2019-10-01 01:02:12 -04:00
Jonathan Lamothe
66148a25d8 don't automatically update goal/assist stats 2019-10-01 01:02:12 -04:00
Jonathan Lamothe
08c3382fe8 recordGoalAssists should clear confirmGoalDataFlag 2019-10-01 00:06:54 -04:00
Jonathan Lamothe
739db189ae added confirmGoalDataFlag field to GameState 2019-10-01 00:02:49 -04:00
Jonathan Lamothe
fc20259a48 Merge pull request #21 from mtlstats/refactor
Refactor
2019-09-28 02:17:13 -04:00
Jonathan Lamothe
b8a3af11a1 reference goals and assists by index number, not names 2019-09-28 02:09:11 -04:00
Jonathan Lamothe
ac92182b20 insert players at end of list (preserve index numbers) 2019-09-28 01:46:28 -04:00
Jonathan Lamothe
767c9b9221 Merge pull request #20 from mtlstats/assists
Assists
2019-09-27 01:46:06 -04:00
Jonathan Lamothe
669c854f4f implemented awardGoalAssists 2019-09-27 01:39:50 -04:00
Jonathan Lamothe
11fcbfcbdd implemented awardAssist 2019-09-26 02:07:55 -04:00
Jonathan Lamothe
c7c267b2a1 pressing enter without input results in player search failure 2019-09-26 01:36:10 -04:00
Jonathan Lamothe
75803edfe7 implemented assist prompt 2019-09-26 01:23:34 -04:00
Jonathan Lamothe
ffdb8e1e8c framework for recording assists 2019-09-25 02:44:42 -04:00
Jonathan Lamothe
8c8a2d52a6 implemented modifyPlayer 2019-09-25 02:42:37 -04:00
Jonathan Lamothe
625d9c616a added goalBy and assistsBy to GameState 2019-09-25 02:42:37 -04:00
Jonathan Lamothe
fc58b0a72b added maxAssists config value 2019-09-25 01:30:28 -04:00
Jonathan Lamothe
c9b822df3c Merge pull request #19 from mtlstats/gfga
Calculate goals for/goals against
2019-09-21 00:43:35 -04:00
Jonathan Lamothe
a9918c559b update report to contain goals for and goals against 2019-09-21 00:35:46 -04:00
Jonathan Lamothe
06c94260ad update goals for and goals against when updating game stats 2019-09-21 00:31:40 -04:00
Jonathan Lamothe
9f68d0da1d added gmsGoalsFor and gmsGoalsAgainst fields to GameStats 2019-09-21 00:03:33 -04:00
Jonathan Lamothe
5332dc0d7f version 0.2.0 2019-09-19 23:46:38 -04:00
Jonathan Lamothe
2ac9aad199 updated change log 2019-09-19 13:02:46 -04:00
Jonathan Lamothe
49b909e4b1 Merge pull request #18 from mtlstats/db
load/save database
2019-09-19 12:48:36 -04:00
Jonathan Lamothe
23a33fc27a save database on exit 2019-09-19 07:34:19 -04:00
Jonathan Lamothe
d58293bef5 load database on start 2019-09-19 07:34:19 -04:00
Jonathan Lamothe
4985d2694a Merge pull request #17 from mtlstats/goal-points
Goal points
2019-09-19 06:42:06 -04:00
Jonathan Lamothe
e3388c45c7 limit number of player shortcuts displayed 2019-09-19 06:34:03 -04:00
Jonathan Lamothe
f7e6ac9437 clear input buffer after player selection with function key 2019-09-19 06:25:38 -04:00
Jonathan Lamothe
a66be1a45e prompt user for players who've scored goals 2019-09-19 06:21:04 -04:00
Jonathan Lamothe
e80b7ec48c implemented recordGoalPrompt 2019-09-19 06:21:04 -04:00
Jonathan Lamothe
b125e72034 implemented awardGoal 2019-09-19 06:21:04 -04:00
Jonathan Lamothe
156c2baaba implemented selectPlayerPrompt 2019-09-19 06:21:04 -04:00
Jonathan Lamothe
11a66cfd33 allow player creation callbacks to be impure 2019-09-19 04:01:28 -04:00
Jonathan Lamothe
8277f8bac7 implemented playerSearchExact 2019-09-19 03:11:48 -04:00
Jonathan Lamothe
d4cfbcb968 changed promptFunctionKey to promptSpecialKey 2019-09-19 02:45:50 -04:00
Jonathan Lamothe
3d705c4e6d implemented nth 2019-09-18 02:45:20 -04:00
Jonathan Lamothe
ed9e437a1a implemented playerSearch 2019-09-18 02:45:20 -04:00
Jonathan Lamothe
2ff8cff1c8 implemented unaccountedPoints helper function 2019-09-14 00:42:04 -04:00
Jonathan Lamothe
926a125692 added pointsAccounted field to GameState 2019-09-14 00:41:38 -04:00
Jonathan Lamothe
1a25c0dc92 made callbacks pure 2019-09-14 00:03:26 -04:00
Jonathan Lamothe
6ceb5415c5 use player creation callbacks 2019-09-13 23:54:36 -04:00
Jonathan Lamothe
6dd9350189 added callbacks to CreatePlayerState 2019-09-13 02:26:03 -04:00
Jonathan Lamothe
db0084f991 Merge pull request #16 from mtlstats/refactor
removed cpsConfirmed from CreatePlayerStatus
2019-09-10 16:22:40 -04:00
Jonathan Lamothe
06a762cfdc removed cpsConfirmed from CreatePlayerStatus 2019-09-10 16:07:46 -04:00
Jonathan Lamothe
fde8965b06 Merge pull request #15 from mtlstats/create-player
Create player
2019-09-09 23:42:09 -04:00
Jonathan Lamothe
375e87a49e implemented player confirmation/addition 2019-09-09 23:35:28 -04:00
Jonathan Lamothe
0ee0451496 prompt for player's position 2019-09-09 22:58:04 -04:00
Jonathan Lamothe
57ac90038a prompt for player name 2019-09-09 22:58:04 -04:00
Jonathan Lamothe
754b3dd25c prompt for player number 2019-09-09 22:58:04 -04:00
Jonathan Lamothe
154c3979a5 implemented createPlayerStateL 2019-09-09 11:43:37 -04:00
Jonathan Lamothe
d5d08aa0f7 added create player option to main menu 2019-09-09 10:51:32 -04:00
Jonathan Lamothe
6b73e367e4 implemented CreatePlayerState 2019-09-08 12:06:38 -04:00
Jonathan Lamothe
be5d10b6fd moved ProgMode 2019-09-07 11:33:03 -04:00
Jonathan Lamothe
4891605089 updated change log 2019-09-07 09:38:05 -04:00
Jonathan Lamothe
6cb348a4a8 Merge pull request #14 from mtlstats/verify-input
Verify input
2019-09-07 09:15:29 -04:00
Jonathan Lamothe
9c0ebb42d1 renamed date to gameDate 2019-09-07 09:06:16 -04:00
Jonathan Lamothe
dc2f632563 prompt for confirmation of game input 2019-09-07 00:27:18 -04:00
Jonathan Lamothe
27867ba69d implemented Mtlstats.Report.date 2019-09-07 00:26:15 -04:00
Jonathan Lamothe
e0dd80079d implemented ynHandler 2019-09-06 23:25:13 -04:00
Jonathan Lamothe
1e7c4d6c19 added dataVerified field to GameState 2019-09-06 11:21:46 -04:00
Jonathan Lamothe
00c96e763d Merge pull request #13 from mtlstats/ot-fix
overtime losses don't count towards the loss column
2019-09-06 10:21:55 -04:00
Jonathan Lamothe
a9ce9a54d7 overtime losses don't count towards the loss column 2019-09-06 10:13:48 -04:00
18 changed files with 1935 additions and 286 deletions

View File

@@ -1,3 +1,20 @@
# Changelog for mtlstats # Changelog for mtlstats
## Unreleased changes ## v0.4.0
- Record penalty minutes
- Calculate total game statistics
- Generate year-to-date statistics report
## v0.3.0
- Record goals and assists
- Track goals for and goals against
## v0.2.0
- Overtime losses don't count in the loss column
- Confirm game data with user before updating stats
- Implemented player creation
- Goal points are now assigned to players
- Loading/saving of database

View File

@@ -1,5 +1,5 @@
name: mtlstats name: mtlstats
version: 0.1.0 version: 0.4.0
github: "mtlstats/mtlstats" github: "mtlstats/mtlstats"
license: GPL-3 license: GPL-3
author: "Jonathan Lamothe" author: "Jonathan Lamothe"
@@ -22,6 +22,8 @@ description: Please see the README on GitHub at <https://github.com/jlam
dependencies: dependencies:
- base >= 4.7 && < 5 - base >= 4.7 && < 5
- aeson >= 1.4.4.0 && < 1.5 - aeson >= 1.4.4.0 && < 1.5
- containers >= 0.6.0.1 && < 0.7
- easy-file >= 0.2.2 && < 0.3
- extra >= 1.6.17 && < 1.7 - extra >= 1.6.17 && < 1.7
- microlens-th >= 0.4.2.3 && < 0.5 - microlens-th >= 0.4.2.3 && < 0.5
- ncurses >= 0.2.16 && < 0.3 - ncurses >= 0.2.16 && < 0.3

View File

@@ -19,15 +19,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE ScopedTypeVariables #-}
module Mtlstats (initState, mainLoop) where module Mtlstats (initState, mainLoop) where
import Control.Exception (IOException, catch)
import Control.Monad (void) import Control.Monad (void)
import Control.Monad.Extra (whenM) import Control.Monad.Extra (whenM)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (get, gets) import Control.Monad.Trans.State (get, gets)
import Data.Maybe (fromJust) import Data.Aeson (decodeFileStrict)
import Data.Maybe (fromJust, fromMaybe)
import Lens.Micro ((&), (.~))
import System.EasyFile (getAppUserDataDirectory, (</>))
import qualified UI.NCurses as C import qualified UI.NCurses as C
import Mtlstats.Config
import Mtlstats.Control import Mtlstats.Control
import Mtlstats.Types import Mtlstats.Types
@@ -36,7 +44,15 @@ initState :: C.Curses ProgState
initState = do initState = do
C.setEcho False C.setEcho False
void $ C.setCursorMode C.CursorInvisible void $ C.setCursorMode C.CursorInvisible
return newProgState db <- liftIO $ do
dir <- getAppUserDataDirectory appName
let dbFile = dir </> dbFname
fromMaybe newDatabase <$> catch
(decodeFileStrict dbFile)
(\(_ :: IOException) -> return Nothing)
return
$ newProgState
& database .~ db
-- | Main program loop -- | Main program loop
mainLoop :: Action () mainLoop :: Action ()

View File

@@ -30,13 +30,26 @@ module Mtlstats.Actions
, overtimeCheck , overtimeCheck
, updateGameStats , updateGameStats
, validateGameDate , validateGameDate
, createPlayer
, addPlayer
, recordGoalAssists
, awardGoal
, awardAssist
, resetGoalData
, assignPMins
, backHome
, scrollUp
, scrollDown
) where ) where
import Control.Monad.Trans.State (modify)
import qualified Data.Map as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Time.Calendar (fromGregorianValid) import Data.Time.Calendar (fromGregorianValid)
import Lens.Micro (over, (^.), (&), (.~), (?~), (%~), (+~)) import Lens.Micro (over, (^.), (&), (.~), (?~), (%~), (+~))
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Util
-- | Starts a new season -- | Starts a new season
startNewSeason :: ProgState -> ProgState startNewSeason :: ProgState -> ProgState
@@ -78,26 +91,37 @@ overtimeCheck s
-- | Adjusts the game stats based on the results of the current game -- | Adjusts the game stats based on the results of the current game
updateGameStats :: ProgState -> ProgState updateGameStats :: ProgState -> ProgState
updateGameStats s = fromMaybe s $ do updateGameStats s = fromMaybe s $ do
gType <- s^.progMode.gameStateL.gameType let gs = s^.progMode.gameStateL
won <- gameWon $ s^.progMode.gameStateL gType <- gs^.gameType
lost <- gameLost $ s^.progMode.gameStateL won <- gameWon gs
ot <- s^.progMode.gameStateL.overtimeFlag lost <- gameLost gs
ot <- gs^.overtimeFlag
tScore <- teamScore gs
oScore <- otherScore gs
let let
hw = if gType == HomeGame && won then 1 else 0 hw = if gType == HomeGame && won then 1 else 0
hl = if gType == HomeGame && lost then 1 else 0 hl = if gType == HomeGame && lost then 1 else 0
hot = if gType == HomeGame && ot then 1 else 0 hot = if gType == HomeGame && ot then 1 else 0
hgf = if gType == HomeGame then tScore else 0
hga = if gType == HomeGame then oScore else 0
aw = if gType == AwayGame && won then 1 else 0 aw = if gType == AwayGame && won then 1 else 0
al = if gType == AwayGame && lost then 1 else 0 al = if gType == AwayGame && lost then 1 else 0
aot = if gType == AwayGame && ot then 1 else 0 aot = if gType == AwayGame && ot then 1 else 0
agf = if gType == AwayGame then tScore else 0
aga = if gType == AwayGame then oScore else 0
Just $ s Just $ s
& database.dbHomeGameStats & database.dbHomeGameStats
%~ (gmsWins +~ hw) %~ (gmsWins +~ hw)
. (gmsLosses +~ hl) . (gmsLosses +~ hl)
. (gmsOvertime +~ hot) . (gmsOvertime +~ hot)
. (gmsGoalsFor +~ hgf)
. (gmsGoalsAgainst +~ hga)
& database.dbAwayGameStats & database.dbAwayGameStats
%~ (gmsWins +~ aw) %~ (gmsWins +~ aw)
. (gmsLosses +~ al) . (gmsLosses +~ al)
. (gmsOvertime +~ aot) . (gmsOvertime +~ aot)
. (gmsGoalsFor +~ agf)
. (gmsGoalsAgainst +~ aga)
-- | Validates the game date -- | Validates the game date
validateGameDate :: ProgState -> ProgState validateGameDate :: ProgState -> ProgState
@@ -111,3 +135,115 @@ validateGameDate s = fromMaybe s $ do
. (gameMonth .~ Nothing) . (gameMonth .~ Nothing)
. (gameDay .~ Nothing) . (gameDay .~ Nothing)
else s else s
-- | Starts player creation mode
createPlayer :: ProgState -> ProgState
createPlayer = let
cb = modify $ progMode .~ MainMenu
cps
= newCreatePlayerState
& cpsSuccessCallback .~ cb
& cpsFailureCallback .~ cb
in progMode .~ CreatePlayer cps
-- | Adds the entered player to the roster
addPlayer :: ProgState -> ProgState
addPlayer s = fromMaybe s $ do
let cps = s^.progMode.createPlayerStateL
num <- cps^.cpsNumber
let
name = cps^.cpsName
pos = cps^.cpsPosition
player = newPlayer num name pos
Just $ s & database.dbPlayers
%~ (++[player])
-- | Awards the goal and assists to the players
recordGoalAssists :: ProgState -> ProgState
recordGoalAssists ps = fromMaybe ps $ do
let gs = ps^.progMode.gameStateL
goalId <- gs^.goalBy
let assistIds = gs^.assistsBy
Just $ ps
& awardGoal goalId
& (\s -> foldr awardAssist s assistIds)
& progMode.gameStateL
%~ (goalBy .~ Nothing)
. (assistsBy .~ [])
. (pointsAccounted %~ succ)
. (confirmGoalDataFlag .~ False)
-- | Awards a goal to a player
awardGoal
:: Int
-- ^ The player's index number
-> ProgState
-> ProgState
awardGoal n ps = ps
& progMode.gameStateL.gamePlayerStats %~
(\m -> let
stats = M.findWithDefault newPlayerStats n m
in M.insert n (stats & psGoals %~ succ) m)
& database.dbPlayers %~ map
(\(i, p) -> if i == n
then p
& pYtd.psGoals %~ succ
& pLifetime.psGoals %~ succ
else p) . zip [0..]
-- | Awards an assist to a player
awardAssist
:: Int
-- ^ The player's index number
-> ProgState
-> ProgState
awardAssist n ps = ps
& progMode.gameStateL.gamePlayerStats %~
(\m -> let
stats = M.findWithDefault newPlayerStats n m
in M.insert n (stats & psAssists %~ succ) m)
& database.dbPlayers %~ map
(\(i, p) -> if i == n
then p
& pYtd.psAssists %~ succ
& pLifetime.psAssists %~ succ
else p) . zip [0..]
-- | Resets the entered data for the current goal
resetGoalData :: ProgState -> ProgState
resetGoalData ps = ps & progMode.gameStateL
%~ (goalBy .~ Nothing)
. (assistsBy .~ [])
. (confirmGoalDataFlag .~ False)
-- | Adds penalty minutes to a player
assignPMins
:: Int
-- ^ The number of minutes to add
-> ProgState
-> ProgState
assignPMins mins s = fromMaybe s $ do
n <- s^.progMode.gameStateL.selectedPlayer
Just $ s
& database.dbPlayers %~ modifyNth n
(((pYtd.psPMin) +~ mins) . ((pLifetime.psPMin) +~ mins))
& progMode.gameStateL
%~ ( gamePlayerStats %~ updateMap n newPlayerStats
(psPMin +~ mins)
)
. (selectedPlayer .~ Nothing)
-- | Resets the program state back to the main menu
backHome :: ProgState -> ProgState
backHome
= (progMode .~ MainMenu)
. (inputBuffer .~ "")
. (scrollOffset .~ 0)
-- | Scrolls the display up
scrollUp :: ProgState -> ProgState
scrollUp = scrollOffset %~ max 0 . pred
-- | Scrolls the display down
scrollDown :: ProgState -> ProgState
scrollDown = scrollOffset %~ succ

View File

@@ -24,3 +24,19 @@ module Mtlstats.Config where
-- | The name of the team whose stats we're tracking -- | The name of the team whose stats we're tracking
myTeam :: String myTeam :: String
myTeam = "MONTREAL" myTeam = "MONTREAL"
-- | The maximum number of function keys
maxFunKeys :: Int
maxFunKeys = 9
-- | The application name
appName :: String
appName = "mtlstats"
-- | The database filename
dbFname :: String
dbFname = "database.json"
-- | The maximum number of assists
maxAssists :: Int
maxAssists = 2

View File

@@ -21,138 +21,328 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
module Mtlstats.Control (dispatch) where module Mtlstats.Control (dispatch) where
import Control.Monad (when) import Control.Monad (join, when)
import Control.Monad.Trans.State (modify) import Control.Monad.Trans.State (gets, modify)
import Data.Char (toUpper) import Data.Char (toUpper)
import Lens.Micro ((^.), (.~)) import Data.Maybe (fromJust, fromMaybe, isJust)
import Lens.Micro ((^.), (.~), (%~))
import Lens.Micro.Extras (view)
import qualified UI.NCurses as C import qualified UI.NCurses as C
import Mtlstats.Actions import Mtlstats.Actions
import Mtlstats.Format import Mtlstats.Format
import Mtlstats.Handlers
import Mtlstats.Menu import Mtlstats.Menu
import Mtlstats.Prompt import Mtlstats.Prompt
import Mtlstats.Report import Mtlstats.Report
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Util
-- | Reads the program state and returs the apropriate controller to -- | Reads the program state and returs the apropriate controller to
-- run -- run
dispatch :: ProgState -> Controller dispatch :: ProgState -> Controller
dispatch s = case s^.progMode of dispatch s = case s^.progMode of
MainMenu -> mainMenuC
MainMenu -> Controller NewSeason -> newSeasonC
{ drawController = const $ drawMenu mainMenu
, handleController = menuHandler mainMenu
}
NewSeason -> Controller
{ drawController = const $ drawMenu newSeasonMenu
, handleController = \e -> do
menuHandler newSeasonMenu e
return True
}
NewGame gs NewGame gs
| null $ gs^.gameYear -> gameYearC
| null $ gs^.gameMonth -> gameMonthC
| null $ gs^.gameDay -> gameDayC
| null $ gs^.gameType -> gameTypeC
| null $ gs^.otherTeam -> otherTeamC
| null $ gs^.homeScore -> homeScoreC
| null $ gs^.awayScore -> awayScoreC
| null $ gs^.overtimeFlag -> overtimeFlagC
| not $ gs^.dataVerified -> verifyDataC
| fromJust (unaccountedPoints gs) -> goalInput gs
| isJust $ gs^.selectedPlayer -> getPMinsC
| not $ gs^.pMinsRecorded -> pMinPlayerC
| otherwise -> reportC
CreatePlayer cps
| null $ cps^.cpsNumber -> getPlayerNumC
| null $ cps^.cpsName -> getPlayerNameC
| null $ cps^.cpsPosition -> getPlayerPosC
| otherwise -> confirmCreatePlayerC
| null $ gs^.gameYear -> Controller mainMenuC :: Controller
{ drawController = \s -> do mainMenuC = Controller
header s { drawController = const $ drawMenu mainMenu
drawPrompt gameYearPrompt s , handleController = menuHandler mainMenu
, handleController = \e -> do }
promptHandler gameYearPrompt e
return True
}
| null $ gs^.gameMonth -> Controller newSeasonC :: Controller
{ drawController = \s -> do newSeasonC = Controller
header s { drawController = const $ drawMenu newSeasonMenu
drawMenu gameMonthMenu , handleController = \e -> do
, handleController = \e -> do menuHandler newSeasonMenu e
menuHandler gameMonthMenu e return True
return True }
}
| null $ gs^.gameDay -> Controller gameYearC :: Controller
{ drawController = \s -> do gameYearC = Controller
header s { drawController = \s -> do
drawPrompt gameDayPrompt s header s
, handleController = \e -> do drawPrompt gameYearPrompt s
promptHandler gameDayPrompt e , handleController = \e -> do
modify validateGameDate promptHandler gameYearPrompt e
return True return True
} }
| null $ gs^.gameType -> Controller gameMonthC :: Controller
{ drawController = \s -> do gameMonthC = Controller
header s { drawController = \s -> do
drawMenu gameTypeMenu header s
, handleController = \e -> do drawMenu gameMonthMenu
menuHandler gameTypeMenu e , handleController = \e -> do
return True menuHandler gameMonthMenu e
} return True
}
| null $ gs^.otherTeam -> Controller gameDayC :: Controller
{ drawController = \s -> do gameDayC = Controller
header s { drawController = \s -> do
drawPrompt otherTeamPrompt s header s
, handleController = \e -> do drawPrompt gameDayPrompt s
promptHandler otherTeamPrompt e , handleController = \e -> do
return True promptHandler gameDayPrompt e
} modify validateGameDate
return True
}
| null $ gs^.homeScore -> Controller gameTypeC :: Controller
{ drawController = \s -> do gameTypeC = Controller
header s { drawController = \s -> do
drawPrompt homeScorePrompt s header s
, handleController = \e -> do drawMenu gameTypeMenu
promptHandler homeScorePrompt e , handleController = \e -> do
return True menuHandler gameTypeMenu e
} return True
}
| null $ gs^.awayScore -> Controller otherTeamC :: Controller
{ drawController = \s -> do otherTeamC = Controller
header s { drawController = \s -> do
drawPrompt awayScorePrompt s header s
, handleController = \e -> do drawPrompt otherTeamPrompt s
promptHandler awayScorePrompt e , handleController = \e -> do
modify overtimeCheck promptHandler otherTeamPrompt e
return True
}
homeScoreC :: Controller
homeScoreC = Controller
{ drawController = \s -> do
header s
drawPrompt homeScorePrompt s
, handleController = \e -> do
promptHandler homeScorePrompt e
return True
}
awayScoreC :: Controller
awayScoreC = Controller
{ drawController = \s -> do
header s
drawPrompt awayScorePrompt s
, handleController = \e -> do
promptHandler awayScorePrompt e
modify overtimeCheck
return True
}
overtimeFlagC :: Controller
overtimeFlagC = Controller
{ drawController = \s -> do
header s
C.drawString "Did the game go into overtime? (Y/N)"
return C.CursorInvisible
, handleController = \e -> do
modify $ progMode.gameStateL.overtimeFlag .~ ynHandler e
return True
}
verifyDataC :: Controller
verifyDataC = Controller
{ drawController = \s -> do
let gs = s^.progMode.gameStateL
header s
C.drawString "\n"
C.drawString $ " Date: " ++ gameDate gs ++ "\n"
C.drawString $ " Game type: " ++ show (fromJust $ gs^.gameType) ++ "\n"
C.drawString $ "Other team: " ++ gs^.otherTeam ++ "\n"
C.drawString $ "Home score: " ++ show (fromJust $ gs^.homeScore) ++ "\n"
C.drawString $ "Away score: " ++ show (fromJust $ gs^.awayScore) ++ "\n"
C.drawString $ " Overtime: " ++ show (fromJust $ gs^.overtimeFlag) ++ "\n\n"
C.drawString "Is the above information correct? (Y/N)"
return C.CursorInvisible
, handleController = \e -> do
case ynHandler e of
Just True -> do
modify $ progMode.gameStateL.dataVerified .~ True
modify updateGameStats modify updateGameStats
return True Just False -> modify $ progMode.gameStateL .~ newGameState
} Nothing -> return ()
return True
}
| null $ gs^.overtimeFlag -> Controller goalInput :: GameState -> Controller
{ drawController = \s -> do goalInput gs
header s | null (gs^.goalBy ) = recordGoalC
C.drawString "Did the game go into overtime? (Y/N)" | not (gs^.confirmGoalDataFlag) = recordAssistC
return C.CursorInvisible | otherwise = confirmGoalDataC
, handleController = \e -> do
overtimePrompt e
modify updateGameStats
return True
}
| otherwise -> Controller recordGoalC :: Controller
{ drawController = \s -> do recordGoalC = Controller
(_, cols) <- C.windowSize { drawController = \s -> let
C.drawString $ report (fromInteger $ pred cols) s (game, goal) = gameGoal s
return C.CursorInvisible in drawPrompt (recordGoalPrompt game goal) s
, handleController = \e -> do , handleController = \e -> do
when (game, goal) <- gets gameGoal
(case e of promptHandler (recordGoalPrompt game goal) e
C.EventCharacter _ -> True return True
C.EventSpecialKey _ -> True }
_ -> False) $
modify $ progMode .~ MainMenu recordAssistC :: Controller
return True recordAssistC = Controller
} { drawController = \s -> let
(game, goal, assist) = gameGoalAssist s
in drawPrompt (recordAssistPrompt game goal assist) s
, handleController = \e -> do
(game, goal, assist) <- gets gameGoalAssist
promptHandler (recordAssistPrompt game goal assist) e
return True
}
confirmGoalDataC :: Controller
confirmGoalDataC = Controller
{ drawController = \s -> do
let
(game, goal) = gameGoal s
gs = s^.progMode.gameStateL
players = s^.database.dbPlayers
msg = unlines $
[ " Game: " ++ padNum 2 game
, " Goal: " ++ show goal
, "Goal scored by: " ++
playerSummary (fromJust $ gs^.goalBy >>= flip nth players)
] ++
map
(\pid -> " Assisted by: " ++
playerSummary (fromJust $ nth pid players))
(gs^.assistsBy) ++
[ ""
, "Is the above information correct? (Y/N)"
]
C.drawString msg
return C.CursorInvisible
, handleController = \e -> do
case ynHandler e of
Just True -> modify recordGoalAssists
Just False -> modify resetGoalData
Nothing -> return ()
return True
}
pMinPlayerC :: Controller
pMinPlayerC = Controller
{ drawController = \s -> do
header s
drawPrompt pMinPlayerPrompt s
, handleController = \e -> do
promptHandler pMinPlayerPrompt e
return True
}
getPMinsC :: Controller
getPMinsC = Controller
{ drawController = \s -> do
header s
C.drawString $ fromMaybe "" $ do
pid <- s^.progMode.gameStateL.selectedPlayer
player <- nth pid $ s^.database.dbPlayers
Just $ playerSummary player ++ "\n"
drawPrompt assignPMinsPrompt s
, handleController = \e -> do
promptHandler assignPMinsPrompt e
return True
}
reportC :: Controller
reportC = Controller
{ drawController = \s -> do
(rows, cols) <- C.windowSize
C.drawString $ unlines $ slice
(s^.scrollOffset)
(fromInteger $ pred rows)
(report (fromInteger $ pred cols) s)
return C.CursorInvisible
, handleController = \e -> do
case e of
C.EventSpecialKey C.KeyUpArrow -> modify scrollUp
C.EventSpecialKey C.KeyDownArrow -> modify scrollDown
C.EventSpecialKey C.KeyHome -> modify $ scrollOffset .~ 0
C.EventSpecialKey _ -> modify backHome
C.EventCharacter _ -> modify backHome
_ -> return ()
return True
}
header :: ProgState -> C.Update () header :: ProgState -> C.Update ()
header s = C.drawString $ header s = C.drawString $
"*** GAME " ++ padNum 2 (s^.database.dbGames) ++ " ***\n" "*** GAME " ++ padNum 2 (s^.database.dbGames) ++ " ***\n"
overtimePrompt :: C.Event -> Action () getPlayerNumC :: Controller
overtimePrompt (C.EventCharacter c) = modify $ getPlayerNumC = Controller
progMode.gameStateL.overtimeFlag .~ case toUpper c of { drawController = drawPrompt playerNumPrompt
'Y' -> Just True , handleController = \e -> do
'N' -> Just False promptHandler playerNumPrompt e
_ -> Nothing return True
overtimePrompt _ = return () }
getPlayerNameC :: Controller
getPlayerNameC = Controller
{ drawController = drawPrompt playerNamePrompt
, handleController = \e -> do
promptHandler playerNamePrompt e
return True
}
getPlayerPosC :: Controller
getPlayerPosC = Controller
{ drawController = drawPrompt playerPosPrompt
, handleController = \e -> do
promptHandler playerPosPrompt e
return True
}
confirmCreatePlayerC :: Controller
confirmCreatePlayerC = Controller
{ drawController = \s -> do
let cps = s^.progMode.createPlayerStateL
C.drawString $ " Player number: " ++ show (fromJust $ cps^.cpsNumber) ++ "\n"
C.drawString $ " Player name: " ++ cps^.cpsName ++ "\n"
C.drawString $ "Player position: " ++ cps^.cpsPosition ++ "\n\n"
C.drawString "Create player: are you sure? (Y/N)"
return C.CursorInvisible
, handleController = \e -> do
case ynHandler e of
Just True -> do
modify addPlayer
join $ gets $ view $ progMode.createPlayerStateL.cpsSuccessCallback
Just False ->
join $ gets $ view $ progMode.createPlayerStateL.cpsFailureCallback
Nothing -> return ()
return True
}
gameGoal :: ProgState -> (Int, Int)
gameGoal s =
( s^.database.dbGames
, succ $ s^.progMode.gameStateL.pointsAccounted
)
gameGoalAssist :: ProgState -> (Int, Int, Int)
gameGoalAssist s = let
(game, goal) = gameGoal s
assist = succ $ length $ s^.progMode.gameStateL.assistsBy
in (game, goal, assist)

33
src/Mtlstats/Handlers.hs Normal file
View File

@@ -0,0 +1,33 @@
{- |
mtlstats
Copyright (C) 2019 Rhéal Lamothe
<rheal.lamothe@gmail.com>
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 <https://www.gnu.org/licenses/>.
-}
module Mtlstats.Handlers (ynHandler) where
import Data.Char (toUpper)
import qualified UI.NCurses as C
-- | Handler for a yes/no prompt
ynHandler :: C.Event -> Maybe Bool
ynHandler (C.EventCharacter c) = case toUpper c of
'Y' -> Just True
'N' -> Just False
_ -> Nothing
ynHandler _ = Nothing

View File

@@ -30,12 +30,21 @@ module Mtlstats.Menu (
gameTypeMenu gameTypeMenu
) where ) where
import Control.Monad.Trans.State (modify) import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (gets, modify)
import Data.Aeson (encodeFile)
import Data.Char (toUpper) import Data.Char (toUpper)
import Lens.Micro ((^.), (.~), (?~)) import Lens.Micro ((^.), (.~), (?~))
import Lens.Micro.Extras (view)
import System.EasyFile
( createDirectoryIfMissing
, getAppUserDataDirectory
, (</>)
)
import qualified UI.NCurses as C import qualified UI.NCurses as C
import Mtlstats.Actions import Mtlstats.Actions
import Mtlstats.Config
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Types.Menu import Mtlstats.Types.Menu
@@ -60,7 +69,15 @@ mainMenu = Menu "*** MAIN MENU ***" True
modify startNewSeason >> return True modify startNewSeason >> return True
, MenuItem '2' "New Game" $ , MenuItem '2' "New Game" $
modify startNewGame >> return True modify startNewGame >> return True
, MenuItem '3' "Exit" $ , MenuItem '3' "Create Player" $
modify createPlayer >> return True
, MenuItem '4' "Exit" $ do
db <- gets $ view database
liftIO $ do
dir <- getAppUserDataDirectory appName
let dbFile = dir </> dbFname
createDirectoryIfMissing True dir
encodeFile dbFile db
return False return False
] ]

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE LambdaCase #-}
module Mtlstats.Prompt ( module Mtlstats.Prompt (
-- * Prompt Functions -- * Prompt Functions
drawPrompt, drawPrompt,
@@ -30,20 +32,31 @@ module Mtlstats.Prompt (
gameDayPrompt, gameDayPrompt,
otherTeamPrompt, otherTeamPrompt,
homeScorePrompt, homeScorePrompt,
awayScorePrompt awayScorePrompt,
playerNumPrompt,
playerNamePrompt,
playerPosPrompt,
selectPlayerPrompt,
recordGoalPrompt,
recordAssistPrompt,
pMinPlayerPrompt,
assignPMinsPrompt
) where ) where
import Control.Monad (when) import Control.Monad (when)
import Control.Monad.Trans.State (gets, modify) import Control.Monad.Trans.State (gets, modify)
import Data.Char (isDigit, toUpper) import Data.Char (isDigit, toUpper)
import Data.Foldable (forM_) import Data.Foldable (forM_)
import Lens.Micro ((^.), (.~), (?~)) import Lens.Micro ((^.), (&), (.~), (?~), (%~))
import Lens.Micro.Extras (view) import Lens.Micro.Extras (view)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import qualified UI.NCurses as C import qualified UI.NCurses as C
import Mtlstats.Actions import Mtlstats.Actions
import Mtlstats.Config
import Mtlstats.Format
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Util
-- | Draws the prompt to the screen -- | Draws the prompt to the screen
drawPrompt :: Prompt -> ProgState -> C.Update C.CursorMode drawPrompt :: Prompt -> ProgState -> C.Update C.CursorMode
@@ -63,8 +76,8 @@ promptHandler p (C.EventCharacter c) = let
modify $ addChar c' modify $ addChar c'
promptHandler _ (C.EventSpecialKey C.KeyBackspace) = promptHandler _ (C.EventSpecialKey C.KeyBackspace) =
modify removeChar modify removeChar
promptHandler p (C.EventSpecialKey (C.KeyFunction k)) = promptHandler p (C.EventSpecialKey k) =
promptFunctionKey p k promptSpecialKey p k
promptHandler _ _ = return () promptHandler _ _ = return ()
-- | Builds a string prompt -- | Builds a string prompt
@@ -75,10 +88,10 @@ strPrompt
-- ^ The callback function for the result -- ^ The callback function for the result
-> Prompt -> Prompt
strPrompt pStr act = Prompt strPrompt pStr act = Prompt
{ promptDrawer = drawSimplePrompt pStr { promptDrawer = drawSimplePrompt pStr
, promptCharCheck = const True , promptCharCheck = const True
, promptAction = act , promptAction = act
, promptFunctionKey = const $ return () , promptSpecialKey = const $ return ()
} }
-- | Builds a numeric prompt -- | Builds a numeric prompt
@@ -89,10 +102,10 @@ numPrompt
-- ^ The callback function for the result -- ^ The callback function for the result
-> Prompt -> Prompt
numPrompt pStr act = Prompt numPrompt pStr act = Prompt
{ promptDrawer = drawSimplePrompt pStr { promptDrawer = drawSimplePrompt pStr
, promptCharCheck = isDigit , promptCharCheck = isDigit
, promptAction = \inStr -> forM_ (readMaybe inStr) act , promptAction = \inStr -> forM_ (readMaybe inStr) act
, promptFunctionKey = const $ return () , promptSpecialKey = const $ return ()
} }
-- | Prompts for the game year -- | Prompts for the game year
@@ -120,5 +133,119 @@ awayScorePrompt :: Prompt
awayScorePrompt = numPrompt "Away score: " $ awayScorePrompt = numPrompt "Away score: " $
modify . (progMode.gameStateL.awayScore ?~) modify . (progMode.gameStateL.awayScore ?~)
-- | Prompts for a new player's number
playerNumPrompt :: Prompt
playerNumPrompt = numPrompt "Player number: " $
modify . (progMode.createPlayerStateL.cpsNumber ?~)
-- | Prompts for a new player's name
playerNamePrompt :: Prompt
playerNamePrompt = strPrompt "Player name: " $
modify . (progMode.createPlayerStateL.cpsName .~)
-- | Prompts for a new player's position
playerPosPrompt :: Prompt
playerPosPrompt = strPrompt "Player position: " $
modify . (progMode.createPlayerStateL.cpsPosition .~)
-- | Selects a player (creating one if necessary)
selectPlayerPrompt
:: String
-- ^ The prompt string
-> (Maybe Int -> Action ())
-- ^ The callback to run (takes the index number of the payer as
-- input)
-> Prompt
selectPlayerPrompt pStr callback = Prompt
{ promptDrawer = \s -> do
let sStr = s^.inputBuffer
C.drawString pStr
C.drawString sStr
(row, col) <- C.cursorPosition
C.drawString "\n\nPlayer select:\n"
let sel = zip [1..maxFunKeys] $ playerSearch sStr $ s^.database.dbPlayers
mapM_
(\(n, (_, p)) -> C.drawString $
"F" ++ show n ++ ") " ++ p^.pName ++ " (" ++ show (p^.pNumber) ++ ")\n")
sel
C.moveCursor row col
, promptCharCheck = const True
, promptAction = \sStr -> if null sStr
then callback Nothing
else do
players <- gets $ view $ database.dbPlayers
case playerSearchExact sStr players of
Just (n, _) -> callback $ Just n
Nothing -> do
mode <- gets $ view progMode
let
cps
= newCreatePlayerState
& cpsName .~ sStr
& cpsSuccessCallback .~ do
modify $ progMode .~ mode
pIndex <- pred . length <$> gets (view $ database.dbPlayers)
callback $ Just pIndex
& cpsFailureCallback .~ do
modify $ progMode .~ mode
callback Nothing
modify $ progMode .~ CreatePlayer cps
, promptSpecialKey = \case
C.KeyFunction n -> do
sStr <- gets $ view inputBuffer
players <- gets $ view $ database.dbPlayers
modify $ inputBuffer .~ ""
let
fKey = pred $ fromIntegral n
options = playerSearch sStr players
sel = fst <$> nth fKey options
callback sel
_ -> return ()
}
-- | Prompts for the player who scored the goal
recordGoalPrompt
:: Int
-- ^ The game number
-> Int
-- ^ The goal number
-> Prompt
recordGoalPrompt game goal = selectPlayerPrompt
( "*** GAME " ++ padNum 2 game ++ " ***\n"
++ "Who scored goal number " ++ show goal ++ "? "
) $ modify . (progMode.gameStateL.goalBy .~)
-- | Prompts for a player who assisted the goal
recordAssistPrompt
:: Int
-- ^ The game number
-> Int
-- ^ The goal nuber
-> Int
-- ^ The assist number
-> Prompt
recordAssistPrompt game goal assist = selectPlayerPrompt
( "*** GAME " ++ padNum 2 game ++ " ***\n"
++ "Goal: " ++ show goal ++ "\n"
++ "Assist #" ++ show assist ++ ": "
) $ \case
Nothing -> modify $ progMode.gameStateL.confirmGoalDataFlag .~ True
Just n -> do
modify $ progMode.gameStateL.assistsBy %~ (++[n])
nAssists <- length <$> gets (view $ progMode.gameStateL.assistsBy)
when (nAssists >= maxAssists) $
modify $ progMode.gameStateL.confirmGoalDataFlag .~ True
pMinPlayerPrompt :: Prompt
pMinPlayerPrompt = selectPlayerPrompt
"Assign penalty minutes to: " $
\case
Nothing -> modify $ progMode.gameStateL.pMinsRecorded .~ True
Just n -> modify $ progMode.gameStateL.selectedPlayer ?~ n
assignPMinsPrompt :: Prompt
assignPMinsPrompt = numPrompt "Penalty minutes: " $
modify . assignPMins
drawSimplePrompt :: String -> ProgState -> C.Update () drawSimplePrompt :: String -> ProgState -> C.Update ()
drawSimplePrompt pStr s = C.drawString $ pStr ++ s^.inputBuffer drawSimplePrompt pStr s = C.drawString $ pStr ++ s^.inputBuffer

View File

@@ -19,14 +19,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
module Mtlstats.Report (report) where module Mtlstats.Report (report, gameDate, playerNameColWidth) where
import qualified Data.Map as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Lens.Micro ((^.)) import Lens.Micro ((^.))
import Mtlstats.Config import Mtlstats.Config
import Mtlstats.Format import Mtlstats.Format
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Util
-- | Generates the report -- | Generates the report
report report
@@ -34,12 +36,21 @@ report
-- ^ The number of columns for the report -- ^ The number of columns for the report
-> ProgState -> ProgState
-- ^ The program state -- ^ The program state
-> String -> [String]
report width s = unlines $ fromMaybe [] $ do report width s
= standingsReport width s
++ [""]
++ gameStatsReport width s
++ [""]
++ yearToDateStatsReport width s
standingsReport :: Int -> ProgState -> [String]
standingsReport width s = fromMaybe [] $ do
let let
db = s^.database db = s^.database
gs = s^.progMode.gameStateL gs = s^.progMode.gameStateL
gNum = db^.dbGames gNum = db^.dbGames
date = gameDate gs
hTeam = homeTeam gs hTeam = homeTeam gs
aTeam = awayTeam gs aTeam = awayTeam gs
hStats = db^.dbHomeGameStats hStats = db^.dbHomeGameStats
@@ -47,10 +58,6 @@ report width s = unlines $ fromMaybe [] $ do
tStats = addGameStats hStats aStats tStats = addGameStats hStats aStats
hScore <- gs^.homeScore hScore <- gs^.homeScore
aScore <- gs^.awayScore aScore <- gs^.awayScore
month <- month <$> gs^.gameMonth
day <- padNum 2 <$> gs^.gameDay
year <- show <$> gs^.gameYear
let date = month ++ " " ++ day ++ " " ++ year
Just Just
[ overlay [ overlay
("GAME NUMBER " ++ padNum 2 gNum) ("GAME NUMBER " ++ padNum 2 gNum)
@@ -66,6 +73,8 @@ report width s = unlines $ fromMaybe [] $ do
++ right 4 "W" ++ right 4 "W"
++ right 4 "L" ++ right 4 "L"
++ right 4 "OT" ++ right 4 "OT"
++ right 4 "GF"
++ right 4 "GA"
++ right 4 "P" ++ right 4 "P"
, centre width , centre width
$ left 11 "HOME" $ left 11 "HOME"
@@ -75,16 +84,81 @@ report width s = unlines $ fromMaybe [] $ do
++ showStats aStats ++ showStats aStats
, centre width , centre width
$ replicate 11 ' ' $ replicate 11 ' '
++ replicate (2 + 4 * 4) '-' ++ replicate (2 + 4 * 6) '-'
, centre width , centre width
$ left 11 "TOTALS" $ left 11 "TOTALS"
++ showStats tStats ++ showStats tStats
] ]
gameStatsReport :: Int -> ProgState -> [String]
gameStatsReport width s = playerReport width "GAME" $
fromMaybe [] $ mapM
(\(pid, stats) -> do
p <- nth pid $ s^.database.dbPlayers
Just (p, stats))
(M.toList $ s^.progMode.gameStateL.gamePlayerStats)
yearToDateStatsReport :: Int -> ProgState -> [String]
yearToDateStatsReport width s = playerReport width "YEAR TO DATE" $
map (\p -> (p, p^.pYtd)) $
filter playerIsActive $ s^.database.dbPlayers
gameDate :: GameState -> String
gameDate gs = fromMaybe "" $ do
year <- show <$> gs^.gameYear
month <- month <$> gs^.gameMonth
day <- padNum 2 <$> gs^.gameDay
Just $ month ++ " " ++ day ++ " " ++ year
playerReport :: Int -> String -> [(Player, PlayerStats)] -> [String]
playerReport width label ps = let
nameWidth = playerNameColWidth $ map fst ps
tStats = foldr (addPlayerStats . snd) newPlayerStats ps
in
[ centre width (label ++ " STATISTICS")
, ""
, centre width
$ "NO. "
++ left nameWidth "PLAYER"
++ right 3 "G"
++ right 6 "A"
++ right 6 "P"
++ right 6 "PM"
] ++ map
(\(p, stats) -> centre width
$ right 2 (show $ p^.pNumber)
++ " "
++ left nameWidth (p^.pName)
++ right 3 (show $ stats^.psGoals)
++ right 6 (show $ stats^.psAssists)
++ right 6 (show $ psPoints stats)
++ right 6 (show $ stats^.psPMin))
ps ++
[ centre width
$ replicate (4 + nameWidth) ' '
++ replicate (3 + 3 * 6) '-'
, overlay
(label ++ " TOTALS")
( centre width
$ replicate (4 + nameWidth) ' '
++ right 3 (show $ tStats^.psGoals)
++ right 6 (show $ tStats^.psAssists)
++ right 6 (show $ psPoints tStats)
++ right 6 (show $ tStats^.psPMin)
)
]
playerNameColWidth :: [Player] -> Int
playerNameColWidth = foldr
(\player current -> max current $ succ $ length $ player^.pName)
10
showStats :: GameStats -> String showStats :: GameStats -> String
showStats gs showStats gs
= right 2 (show $ gmsGames gs) = right 2 (show $ gmsGames gs)
++ right 4 (show $ gs^.gmsWins) ++ right 4 (show $ gs^.gmsWins)
++ right 4 (show $ gs^.gmsLosses) ++ right 4 (show $ gs^.gmsLosses)
++ right 4 (show $ gs^.gmsOvertime) ++ right 4 (show $ gs^.gmsOvertime)
++ right 4 (show $ gs^.gmsGoalsFor)
++ right 4 (show $ gs^.gmsGoalsAgainst)
++ right 4 (show $ gmsPoints gs) ++ right 4 (show $ gmsPoints gs)

View File

@@ -26,9 +26,10 @@ module Mtlstats.Types (
Controller (..), Controller (..),
Action, Action,
ProgState (..), ProgState (..),
GameState (..),
ProgMode (..), ProgMode (..),
GameState (..),
GameType (..), GameType (..),
CreatePlayerState (..),
Database (..), Database (..),
Player (..), Player (..),
PlayerStats (..), PlayerStats (..),
@@ -41,8 +42,10 @@ module Mtlstats.Types (
database, database,
progMode, progMode,
inputBuffer, inputBuffer,
scrollOffset,
-- ** ProgMode Lenses -- ** ProgMode Lenses
gameStateL, gameStateL,
createPlayerStateL,
-- ** GameState Lenses -- ** GameState Lenses
gameYear, gameYear,
gameMonth, gameMonth,
@@ -52,6 +55,20 @@ module Mtlstats.Types (
homeScore, homeScore,
awayScore, awayScore,
overtimeFlag, overtimeFlag,
dataVerified,
pointsAccounted,
goalBy,
assistsBy,
gamePlayerStats,
confirmGoalDataFlag,
selectedPlayer,
pMinsRecorded,
-- ** CreatePlayerState Lenses
cpsNumber,
cpsName,
cpsPosition,
cpsSuccessCallback,
cpsFailureCallback,
-- ** Database Lenses -- ** Database Lenses
dbPlayers, dbPlayers,
dbGoalies, dbGoalies,
@@ -85,9 +102,12 @@ module Mtlstats.Types (
gmsWins, gmsWins,
gmsLosses, gmsLosses,
gmsOvertime, gmsOvertime,
gmsGoalsFor,
gmsGoalsAgainst,
-- * Constructors -- * Constructors
newProgState, newProgState,
newGameState, newGameState,
newCreatePlayerState,
newDatabase, newDatabase,
newPlayer, newPlayer,
newPlayerStats, newPlayerStats,
@@ -103,12 +123,20 @@ module Mtlstats.Types (
gameWon, gameWon,
gameLost, gameLost,
gameTied, gameTied,
unaccountedPoints,
-- ** GameStats Helpers -- ** GameStats Helpers
gmsGames, gmsGames,
gmsPoints, gmsPoints,
addGameStats, addGameStats,
-- ** Player Helpers -- ** Player Helpers
pPoints playerSearch,
playerSearchExact,
modifyPlayer,
playerSummary,
playerIsActive,
-- ** PlayerStats Helpers
psPoints,
addPlayerStats
) where ) where
import Control.Monad.Trans.State (StateT) import Control.Monad.Trans.State (StateT)
@@ -124,6 +152,9 @@ import Data.Aeson
, (.:) , (.:)
, (.=) , (.=)
) )
import Data.List (isInfixOf)
import qualified Data.Map as M
import Data.Maybe (listToMaybe)
import Lens.Micro (Lens', lens, (&), (^.), (.~)) import Lens.Micro (Lens', lens, (&), (^.), (.~))
import Lens.Micro.TH (makeLenses) import Lens.Micro.TH (makeLenses)
import qualified UI.NCurses as C import qualified UI.NCurses as C
@@ -143,40 +174,66 @@ type Action a = StateT ProgState C.Curses a
-- | Represents the program state -- | Represents the program state
data ProgState = ProgState data ProgState = ProgState
{ _database :: Database { _database :: Database
-- ^ The data to be saved -- ^ The data to be saved
, _progMode :: ProgMode , _progMode :: ProgMode
-- ^ The program's mode -- ^ The program's mode
, _inputBuffer :: String , _inputBuffer :: String
-- ^ Buffer for user input -- ^ Buffer for user input
} deriving (Eq, Show) , _scrollOffset :: Int
-- ^ The scrolling offset for the display
-- | The game state }
data GameState = GameState
{ _gameYear :: Maybe Int
-- ^ The year the game took place
, _gameMonth :: Maybe Int
-- ^ The month the game took place
, _gameDay :: Maybe Int
-- ^ The day of the month the game took place
, _gameType :: Maybe GameType
-- ^ The type of game (home/away)
, _otherTeam :: String
-- ^ The name of the other team
, _homeScore :: Maybe Int
-- ^ The home team's score
, _awayScore :: Maybe Int
-- ^ The away team's score
, _overtimeFlag :: Maybe Bool
-- ^ Indicates whether or not the game went into overtime
} deriving (Eq, Show)
-- | The program mode -- | The program mode
data ProgMode data ProgMode
= MainMenu = MainMenu
| NewSeason | NewSeason
| NewGame GameState | NewGame GameState
deriving (Eq, Show) | CreatePlayer CreatePlayerState
instance Show ProgMode where
show MainMenu = "MainMenu"
show NewSeason = "NewSeason"
show (NewGame _) = "NewGame"
show (CreatePlayer _) = "CreatePlayer"
-- | The game state
data GameState = GameState
{ _gameYear :: Maybe Int
-- ^ The year the game took place
, _gameMonth :: Maybe Int
-- ^ The month the game took place
, _gameDay :: Maybe Int
-- ^ The day of the month the game took place
, _gameType :: Maybe GameType
-- ^ The type of game (home/away)
, _otherTeam :: String
-- ^ The name of the other team
, _homeScore :: Maybe Int
-- ^ The home team's score
, _awayScore :: Maybe Int
-- ^ The away team's score
, _overtimeFlag :: Maybe Bool
-- ^ Indicates whether or not the game went into overtime
, _dataVerified :: Bool
-- ^ Set to 'True' when the user confirms the entered data
, _pointsAccounted :: Int
-- ^ The number of game points accounted for
, _goalBy :: Maybe Int
-- ^ The index number of the player who scored the most recently
-- entered goal
, _assistsBy :: [Int]
-- ^ The index numbers of the players who have assisted the most
-- recently entered goal
, _gamePlayerStats :: M.Map Int PlayerStats
-- ^ The player stats accumulated over the game
, _confirmGoalDataFlag :: Bool
-- ^ Set when the user confirms the goal data
, _selectedPlayer :: Maybe Int
-- ^ Index number of the selected 'Player'
, _pMinsRecorded :: Bool
-- ^ Set when the penalty mintes have been recorded
} deriving (Eq, Show)
-- | The type of game -- | The type of game
data GameType data GameType
@@ -184,6 +241,20 @@ data GameType
| AwayGame | AwayGame
deriving (Eq, Show) deriving (Eq, Show)
-- | Player creation status
data CreatePlayerState = CreatePlayerState
{ _cpsNumber :: Maybe Int
-- ^ The player's number
, _cpsName :: String
-- ^ The player's name
, _cpsPosition :: String
-- ^ The player's position
, _cpsSuccessCallback :: Action ()
-- ^ The function to call on success
, _cpsFailureCallback :: Action ()
-- ^ The function to call on failure
}
-- | Represents the database -- | Represents the database
data Database = Database data Database = Database
{ _dbPlayers :: [Player] { _dbPlayers :: [Player]
@@ -366,12 +437,16 @@ instance ToJSON GoalieStats where
-- | Game statistics -- | Game statistics
data GameStats = GameStats data GameStats = GameStats
{ _gmsWins :: Int { _gmsWins :: Int
-- ^ Games won -- ^ Games won
, _gmsLosses :: Int , _gmsLosses :: Int
-- ^ Games lost -- ^ Games lost
, _gmsOvertime :: Int , _gmsOvertime :: Int
-- ^ Games lost in overtime -- ^ Games lost in overtime
, _gmsGoalsFor :: Int
-- ^ Goals for the team
, _gmsGoalsAgainst :: Int
-- ^ Goals against the team
} deriving (Eq, Show) } deriving (Eq, Show)
instance FromJSON GameStats where instance FromJSON GameStats where
@@ -379,32 +454,39 @@ instance FromJSON GameStats where
<$> v .: "wins" <$> v .: "wins"
<*> v .: "losses" <*> v .: "losses"
<*> v .: "overtime" <*> v .: "overtime"
<*> v .: "goals_for"
<*> v .: "goals_against"
instance ToJSON GameStats where instance ToJSON GameStats where
toJSON (GameStats w l ot) = object toJSON (GameStats w l ot gf ga) = object
[ "wins" .= w [ "wins" .= w
, "losses" .= l , "losses" .= l
, "overtime" .= ot , "overtime" .= ot
, "goals_for" .= gf
, "goals_against" .= ga
] ]
toEncoding (GameStats w l ot) = pairs $ toEncoding (GameStats w l ot gf ga) = pairs $
"wins" .= w <> "wins" .= w <>
"losses" .= l <> "losses" .= l <>
"overtime" .= ot "overtime" .= ot <>
"goals_for" .= gf <>
"goals_against" .= ga
-- | Defines a user prompt -- | Defines a user prompt
data Prompt = Prompt data Prompt = Prompt
{ promptDrawer :: ProgState -> C.Update () { promptDrawer :: ProgState -> C.Update ()
-- ^ Draws the prompt to thr screen -- ^ Draws the prompt to thr screen
, promptCharCheck :: Char -> Bool , promptCharCheck :: Char -> Bool
-- ^ Determines whether or not the character is valid -- ^ Determines whether or not the character is valid
, promptAction :: String -> Action () , promptAction :: String -> Action ()
-- ^ Action to perform when the value is entered -- ^ Action to perform when the value is entered
, promptFunctionKey :: Integer -> Action () , promptSpecialKey :: C.Key -> Action ()
-- ^ Action to perform when a function key is pressed -- ^ Action to perform when a special key is pressed
} }
makeLenses ''ProgState makeLenses ''ProgState
makeLenses ''GameState makeLenses ''GameState
makeLenses ''CreatePlayerState
makeLenses ''Database makeLenses ''Database
makeLenses ''Player makeLenses ''Player
makeLenses ''PlayerStats makeLenses ''PlayerStats
@@ -419,25 +501,51 @@ gameStateL = lens
_ -> newGameState) _ -> newGameState)
(\_ gs -> NewGame gs) (\_ gs -> NewGame gs)
createPlayerStateL :: Lens' ProgMode CreatePlayerState
createPlayerStateL = lens
(\case
CreatePlayer cps -> cps
_ -> newCreatePlayerState)
(\_ cps -> CreatePlayer cps)
-- | Constructor for a 'ProgState' -- | Constructor for a 'ProgState'
newProgState :: ProgState newProgState :: ProgState
newProgState = ProgState newProgState = ProgState
{ _database = newDatabase { _database = newDatabase
, _progMode = MainMenu , _progMode = MainMenu
, _inputBuffer = "" , _inputBuffer = ""
, _scrollOffset = 0
} }
-- | Constructor for a 'GameState' -- | Constructor for a 'GameState'
newGameState :: GameState newGameState :: GameState
newGameState = GameState newGameState = GameState
{ _gameYear = Nothing { _gameYear = Nothing
, _gameMonth = Nothing , _gameMonth = Nothing
, _gameDay = Nothing , _gameDay = Nothing
, _gameType = Nothing , _gameType = Nothing
, _otherTeam = "" , _otherTeam = ""
, _homeScore = Nothing , _homeScore = Nothing
, _awayScore = Nothing , _awayScore = Nothing
, _overtimeFlag = Nothing , _overtimeFlag = Nothing
, _dataVerified = False
, _pointsAccounted = 0
, _goalBy = Nothing
, _assistsBy = []
, _gamePlayerStats = M.empty
, _confirmGoalDataFlag = False
, _selectedPlayer = Nothing
, _pMinsRecorded = False
}
-- | Constructor for a 'CreatePlayerState'
newCreatePlayerState :: CreatePlayerState
newCreatePlayerState = CreatePlayerState
{ _cpsNumber = Nothing
, _cpsName = ""
, _cpsPosition = ""
, _cpsSuccessCallback = return ()
, _cpsFailureCallback = return ()
} }
-- | Constructor for a 'Database' -- | Constructor for a 'Database'
@@ -504,9 +612,11 @@ newGoalieStats = GoalieStats
-- | Constructor for a 'GameStats' value -- | Constructor for a 'GameStats' value
newGameStats :: GameStats newGameStats :: GameStats
newGameStats = GameStats newGameStats = GameStats
{ _gmsWins = 0 { _gmsWins = 0
, _gmsLosses = 0 , _gmsLosses = 0
, _gmsOvertime = 0 , _gmsOvertime = 0
, _gmsGoalsFor = 0
, _gmsGoalsAgainst = 0
} }
-- | Determines the team's score -- | Determines the team's score
@@ -545,15 +655,26 @@ gameWon gs = (>) <$> teamScore gs <*> otherScore gs
-- | Checks if the game was lost -- | Checks if the game was lost
gameLost :: GameState -> Maybe Bool gameLost :: GameState -> Maybe Bool
gameLost gs = (<) <$> teamScore gs <*> otherScore gs gameLost gs = do
ot <- gs^.overtimeFlag
team <- teamScore gs
other <- otherScore gs
Just $ not ot && other > team
-- | Checks if the game has tied -- | Checks if the game has tied
gameTied :: GameState -> Maybe Bool gameTied :: GameState -> Maybe Bool
gameTied gs = (==) <$> gs^.homeScore <*> gs^.awayScore gameTied gs = (==) <$> gs^.homeScore <*> gs^.awayScore
-- | Checks for unaccounted points
unaccountedPoints :: GameState -> Maybe Bool
unaccountedPoints gs = do
scored <- teamScore gs
let accounted = gs^.pointsAccounted
Just $ scored > accounted
-- | Calculates the number of games played -- | Calculates the number of games played
gmsGames :: GameStats -> Int gmsGames :: GameStats -> Int
gmsGames gs = gs^.gmsWins + gs^.gmsLosses gmsGames gs = gs^.gmsWins + gs^.gmsLosses + gs^.gmsOvertime
-- | Calculates the number of points -- | Calculates the number of points
gmsPoints :: GameStats -> Int gmsPoints :: GameStats -> Int
@@ -562,11 +683,77 @@ gmsPoints gs = 2 * gs^.gmsWins + gs^. gmsOvertime
-- | Adds two 'GameStats' values together -- | Adds two 'GameStats' values together
addGameStats :: GameStats -> GameStats -> GameStats addGameStats :: GameStats -> GameStats -> GameStats
addGameStats s1 s2 = GameStats addGameStats s1 s2 = GameStats
{ _gmsWins = s1^.gmsWins + s2^.gmsWins { _gmsWins = s1^.gmsWins + s2^.gmsWins
, _gmsLosses = s1^.gmsLosses + s2^.gmsLosses , _gmsLosses = s1^.gmsLosses + s2^.gmsLosses
, _gmsOvertime = s1^.gmsOvertime + s2^.gmsOvertime , _gmsOvertime = s1^.gmsOvertime + s2^.gmsOvertime
, _gmsGoalsFor = s1^.gmsGoalsFor + s2^.gmsGoalsFor
, _gmsGoalsAgainst = s1^.gmsGoalsAgainst + s2^.gmsGoalsAgainst
} }
-- | Searches through a list of players
playerSearch
:: String
-- ^ The search string
-> [Player]
-- ^ The list of players to search
-> [(Int, Player)]
-- ^ The matching players with their index numbers
playerSearch sStr =
filter (match sStr) .
zip [0..]
where match sStr (_, p) = sStr `isInfixOf` (p^.pName)
-- | Searches for a player by exact match on name
playerSearchExact
:: String
-- ^ The player's name
-> [Player]
-- ^ The list of players to search
-> Maybe (Int, Player)
-- ^ The player's index and value
playerSearchExact sStr =
listToMaybe .
filter (match sStr) .
zip [0..]
where match sStr (_, p) = p^.pName == sStr
-- | Modifies a player with a given name
modifyPlayer
:: (Player -> Player)
-- ^ The modification function
-> String
-- ^ The player's name
-> [Player]
-- ^ The list of players to modify
-> [Player]
-- ^ The modified list
modifyPlayer f n = map
(\p -> if p^.pName == n
then f p
else p)
-- | Provides a short summary string for a player
playerSummary :: Player -> String
playerSummary p =
p^.pName ++ " (" ++ show (p^.pNumber) ++ ") " ++ p^.pPosition
-- | Determines whether or not a player has been active in the current
-- season/year
playerIsActive :: Player -> Bool
playerIsActive = do
stats <- (^.pYtd)
return
$ stats^.psGoals /= 0
|| stats^.psAssists /= 0
|| stats^.psPMin /= 0
-- | Calculates a player's points -- | Calculates a player's points
pPoints :: PlayerStats -> Int psPoints :: PlayerStats -> Int
pPoints s = s^.psGoals + s^.psAssists psPoints s = s^.psGoals + s^.psAssists
-- | Adds two 'PlayerStats' together
addPlayerStats :: PlayerStats -> PlayerStats -> PlayerStats
addPlayerStats s1 s2 = newPlayerStats
& psGoals .~ s1^.psGoals + s2^.psGoals
& psAssists .~ s1^.psAssists + s2^.psAssists
& psPMin .~ s1^.psPMin + s2^.psPMin

77
src/Mtlstats/Util.hs Normal file
View File

@@ -0,0 +1,77 @@
{- |
mtlstats
Copyright (C) 2019 Rhéal Lamothe
<rheal.lamothe@gmail.com>
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 <https://www.gnu.org/licenses/>.
-}
module Mtlstats.Util (nth, modifyNth, updateMap, slice) where
import qualified Data.Map as M
-- | Attempt to select the element from a list at a given index
nth
:: Int
-- ^ The index
-> [a]
-- ^ The list
-> Maybe a
nth _ [] = Nothing
nth n (x:xs)
| n == 0 = Just x
| n < 0 = Nothing
| otherwise = nth (pred n) xs
-- | Attempt to modify the index at a given index in a list
modifyNth
:: Int
-- ^ The index
-> (a -> a)
-- ^ The modification function
-> [a]
-- ^ The list
-> [a]
modifyNth n f = map (\(i, x) -> if i == n then f x else x)
. zip [0..]
-- | Modify a value indexed by a given key in a map using a default
-- initial value if not present
updateMap
:: Ord k
=> k
-- ^ The key
-> a
-- ^ The default initial value
-> (a -> a)
-- ^ The modification function
-> M.Map k a
-- ^ The map
-> M.Map k a
updateMap k def f m = let
x = M.findWithDefault def k m
in M.insert k (f x) m
-- | Selects a section of a list
slice
:: Int
-- ^ The index to start at
-> Int
-- ^ The number of elements to take
-> [a]
-- ^ The list to take a subset of
-> [a]
slice offset len = take len . drop offset

View File

@@ -19,15 +19,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE LambdaCase #-}
module ActionsSpec (spec) where module ActionsSpec (spec) where
import Control.Monad (replicateM) import Control.Monad (replicateM)
import qualified Data.Map as M
import Data.Maybe (fromJust)
import Lens.Micro ((^.), (&), (.~), (?~), (%~)) import Lens.Micro ((^.), (&), (.~), (?~), (%~))
import System.Random (randomRIO) import System.Random (randomRIO)
import Test.Hspec (Spec, context, describe, it, shouldBe, shouldNotBe) import Test.Hspec
( Spec
, context
, describe
, it
, runIO
, shouldBe
, shouldNotBe
, shouldSatisfy
)
import Mtlstats.Actions import Mtlstats.Actions
import Mtlstats.Types import Mtlstats.Types
import Mtlstats.Util
spec :: Spec spec :: Spec
spec = describe "Mtlstats.Actions" $ do spec = describe "Mtlstats.Actions" $ do
@@ -39,6 +53,16 @@ spec = describe "Mtlstats.Actions" $ do
overtimeCheckSpec overtimeCheckSpec
updateGameStatsSpec updateGameStatsSpec
validateGameDateSpec validateGameDateSpec
createPlayerSpec
addPlayerSpec
recordGoalAssistsSpec
awardGoalSpec
awardAssistSpec
resetGoalDataSpec
assignPMinsSpec
backHomeSpec
scrollUpSpec
scrollDownSpec
startNewSeasonSpec :: Spec startNewSeasonSpec :: Spec
startNewSeasonSpec = describe "startNewSeason" $ do startNewSeasonSpec = describe "startNewSeason" $ do
@@ -48,7 +72,7 @@ startNewSeasonSpec = describe "startNewSeason" $ do
& startNewSeason & startNewSeason
it "should set the progState to NewSeason" $ it "should set the progState to NewSeason" $
s ^. progMode `shouldBe` NewSeason show (s^.progMode) `shouldBe` "NewSeason"
it "should set the number of games to 0" $ it "should set the number of games to 0" $
s ^. database . dbGames `shouldBe` 0 s ^. database . dbGames `shouldBe` 0
@@ -61,7 +85,7 @@ startNewGameSpec = describe "startNewGame" $ do
s ^. database . dbGames `shouldBe` 1 s ^. database . dbGames `shouldBe` 1
it "should set the mode to NewGame" $ it "should set the mode to NewGame" $
s ^. progMode `shouldBe` NewGame newGameState show (s^.progMode) `shouldBe` "NewGame"
resetYtdSpec :: Spec resetYtdSpec :: Spec
resetYtdSpec = describe "resetYtd" $ resetYtdSpec = describe "resetYtd" $
@@ -190,9 +214,11 @@ updateGameStatsSpec = describe "updateGameStats" $ do
let let
baseStats = newGameStats baseStats = newGameStats
& gmsWins .~ 1 & gmsWins .~ 1
& gmsLosses .~ 1 & gmsLosses .~ 1
& gmsOvertime .~ 1 & gmsOvertime .~ 1
& gmsGoalsFor .~ 1
& gmsGoalsAgainst .~ 1
s t h a o = newProgState s t h a o = newProgState
& progMode.gameStateL & progMode.gameStateL
@@ -204,71 +230,79 @@ updateGameStatsSpec = describe "updateGameStats" $ do
%~ (dbHomeGameStats .~ baseStats) %~ (dbHomeGameStats .~ baseStats)
. (dbAwayGameStats .~ baseStats) . (dbAwayGameStats .~ baseStats)
db hw hl ho aw al ao = newDatabase db hw hl ho hf ha aw al ao af aa = newDatabase
& dbHomeGameStats & dbHomeGameStats
%~ (gmsWins .~ hw) %~ (gmsWins .~ hw)
. (gmsLosses .~ hl) . (gmsLosses .~ hl)
. (gmsOvertime .~ ho) . (gmsOvertime .~ ho)
. (gmsGoalsFor .~ hf)
. (gmsGoalsAgainst .~ ha)
& dbAwayGameStats & dbAwayGameStats
%~ (gmsWins .~ aw) %~ (gmsWins .~ aw)
. (gmsLosses .~ al) . (gmsLosses .~ al)
. (gmsOvertime .~ ao) . (gmsOvertime .~ ao)
. (gmsGoalsFor .~ af)
. (gmsGoalsAgainst .~ aa)
context "home win" $ context "home win" $
it "should record a home win" $ let it "should record a home win" $ let
s' = s (Just HomeGame) (Just 2) (Just 1) (Just False) s' = s (Just HomeGame) (Just 2) (Just 1) (Just False)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 2 1 1 1 1 1 in db' `shouldBe` db 2 1 1 3 2 1 1 1 1 1
context "home loss" $ context "home loss" $
it "should record a home loss" $ let it "should record a home loss" $ let
s' = s (Just HomeGame) (Just 1) (Just 2) (Just False) s' = s (Just HomeGame) (Just 1) (Just 2) (Just False)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 2 1 1 1 1 in db' `shouldBe` db 1 2 1 2 3 1 1 1 1 1
context "home overtime loss" $ context "home overtime loss" $
it "should record a home loss and overtime" $ let it "should record a home overtime" $ let
s' = s (Just HomeGame) (Just 1) (Just 2) (Just True) s' = s (Just HomeGame) (Just 1) (Just 2) (Just True)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 2 2 1 1 1 in db' `shouldBe` db 1 1 2 2 3 1 1 1 1 1
context "away win" $ context "away win" $
it "should record an away win" $ let it "should record an away win" $ let
s' = s (Just AwayGame) (Just 1) (Just 2) (Just False) s' = s (Just AwayGame) (Just 1) (Just 2) (Just False)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 2 1 1 in db' `shouldBe` db 1 1 1 1 1 2 1 1 3 2
context "away loss" $ context "away loss" $
it "should record an away loss" $ let it "should record an away loss" $ let
s' = s (Just AwayGame) (Just 2) (Just 1) (Just False) s' = s (Just AwayGame) (Just 2) (Just 1) (Just False)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 2 1 in db' `shouldBe` db 1 1 1 1 1 1 2 1 2 3
context "away overtime loss" $ context "away overtime loss" $
it "should record an away loss and overtime" $ let it "should record an away overtime" $ let
s' = s (Just AwayGame) (Just 2) (Just 1) (Just True) s' = s (Just AwayGame) (Just 2) (Just 1) (Just True)
db' = updateGameStats s' ^. database db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 2 2 in db' `shouldBe` db 1 1 1 1 1 1 1 2 2 3
context "missing game type" $ context "missing game type" $
it "should not change anything" $ let it "should not change anything" $ let
s' = s Nothing (Just 1) (Just 2) (Just True) s' = s Nothing (Just 1) (Just 2) (Just True)
in updateGameStats s' `shouldBe` s' db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 1 1 1 1 1 1
context "missing home score" $ context "missing home score" $
it "should not change anything" $ let it "should not change anything" $ let
s' = s (Just HomeGame) Nothing (Just 1) (Just True) s' = s (Just HomeGame) Nothing (Just 1) (Just True)
in updateGameStats s' `shouldBe` s' db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 1 1 1 1 1 1
context "missing away score" $ context "missing away score" $
it "should not change anything" $ let it "should not change anything" $ let
s' = s (Just HomeGame) (Just 1) Nothing (Just True) s' = s (Just HomeGame) (Just 1) Nothing (Just True)
in updateGameStats s' `shouldBe` s' db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 1 1 1 1 1 1
context "missing overtime flag" $ context "missing overtime flag" $
it "should not change anything" $ let it "should not change anything" $ let
s' = s (Just HomeGame) (Just 1) (Just 2) Nothing s' = s (Just HomeGame) (Just 1) (Just 2) Nothing
in updateGameStats s' `shouldBe` s' db' = updateGameStats s' ^. database
in db' `shouldBe` db 1 1 1 1 1 1 1 1 1 1
validateGameDateSpec :: Spec validateGameDateSpec :: Spec
validateGameDateSpec = describe "validateGameDate" $ do validateGameDateSpec = describe "validateGameDate" $ do
@@ -315,6 +349,275 @@ validateGameDateSpec = describe "validateGameDate" $ do
s^.progMode.gameStateL.gameMonth `shouldBe` Just 6 s^.progMode.gameStateL.gameMonth `shouldBe` Just 6
s^.progMode.gameStateL.gameDay `shouldBe` Nothing s^.progMode.gameStateL.gameDay `shouldBe` Nothing
createPlayerSpec :: Spec
createPlayerSpec = describe "createPlayer" $
it "should change the mode appropriately" $ let
s = createPlayer newProgState
in show (s^.progMode) `shouldBe` "CreatePlayer"
addPlayerSpec :: Spec
addPlayerSpec = describe "addPlayer" $ do
let
p1 = newPlayer 1 "Joe" "centre"
p2 = newPlayer 2 "Bob" "defense"
db = newDatabase
& dbPlayers .~ [p1]
s pm = newProgState
& progMode .~ pm
& database .~ db
context "data available" $
it "should create the player" $ let
s' = addPlayer $ s $ CreatePlayer $ newCreatePlayerState
& cpsNumber ?~ 2
& cpsName .~ "Bob"
& cpsPosition .~ "defense"
in s'^.database.dbPlayers `shouldBe` [p1, p2]
context "data unavailable" $
it "should not create the player" $ let
s' = addPlayer $ s MainMenu
in s'^.database.dbPlayers `shouldBe` [p1]
recordGoalAssistsSpec :: Spec
recordGoalAssistsSpec = describe "recordGoalAssists" $ do
let
joe = newPlayer 1 "Joe" "centre"
bob = newPlayer 2 "Bob" "defense"
steve = newPlayer 3 "Steve" "forward"
dave = newPlayer 4 "Dave" "somewhere"
frank = newPlayer 5 "Frank" "elsewhere"
ps
= newProgState
& database.dbPlayers .~ [joe, bob, steve, dave, frank]
& progMode.gameStateL
%~ (goalBy ?~ 0)
. (assistsBy .~ [1, 2])
. (confirmGoalDataFlag .~ True)
& recordGoalAssists
mapM_
(\(name, n, goals, assists) -> context name $ do
let
player = (ps^.database.dbPlayers) !! n
stats = M.findWithDefault newPlayerStats n $
ps^.progMode.gameStateL.gamePlayerStats
it ("should set the year-to-date goals to " ++ show goals) $
player^.pYtd.psGoals `shouldBe` goals
it ("should set the lifetime goals to " ++ show goals) $
player^.pLifetime.psGoals `shouldBe` goals
it ("should set the game goals to " ++ show goals) $
stats^.psAssists `shouldBe` assists
it ("should set the year-to-date assists to " ++ show assists) $
player^.pYtd.psAssists `shouldBe` assists
it ("should set the lifetime assists to " ++ show assists) $
player^.pLifetime.psAssists `shouldBe` assists
it ("should set the game assists to " ++ show assists) $
stats^.psAssists `shouldBe` assists)
-- name, index, goals, assists
[ ( "Joe", 0, 1, 0 )
, ( "Bob", 1, 0, 1 )
, ( "Steve", 2, 0, 1 )
, ( "Dave", 3, 0, 0 )
]
it "should clear the goalBy value" $
ps^.progMode.gameStateL.goalBy `shouldBe` Nothing
it "should clear the assistsBy list" $
ps^.progMode.gameStateL.assistsBy `shouldBe` []
it "should increment the pointsAccounted counter" $
ps^.progMode.gameStateL.pointsAccounted `shouldBe` 1
it "should clear the confirmGoalDataFlag" $
ps^.progMode.gameStateL.confirmGoalDataFlag `shouldBe` False
awardGoalSpec :: Spec
awardGoalSpec = describe "awardGoal" $ do
let
joe
= newPlayer 2 "Joe" "centre"
& pYtd.psGoals .~ 1
& pLifetime.psGoals .~ 2
bob
= newPlayer 3 "Bob" "defense"
& pYtd.psGoals .~ 3
& pLifetime.psGoals .~ 4
db
= newDatabase
& dbPlayers .~ [joe, bob]
joeStats
= newPlayerStats
& psGoals .~ 1
ps
= newProgState
& progMode.gameStateL.gamePlayerStats .~ M.singleton 0 joeStats
& database .~ db
mapM_
(\(pName, pid, ytd, lt, game) ->
context pName $ do
let
ps' = awardGoal pid ps
player = (ps'^.database.dbPlayers) !! pid
gStats = (ps'^.progMode.gameStateL.gamePlayerStats) M.! pid
it ("should increment " ++ pName ++ "'s year-to-date goals") $
player^.pYtd.psGoals `shouldBe` ytd
it ("should increment " ++ pName ++ "'s lifetime goals") $
player^.pLifetime.psGoals `shouldBe` lt
it ("should increment " ++ pName ++ "'s game goals") $
gStats^.psGoals `shouldBe` game)
-- player name, player id, ytd goals, lifetime goals, game goals
[ ( "Joe", 0, 2, 3, 2 )
, ( "Bob", 1, 4, 5, 1 )
]
context "invalid index" $ let
ps' = awardGoal 2 ps
in it "should not change the database" $
ps'^.database `shouldBe` db
context "negative index" $ let
ps' = awardGoal (-1) ps
in it "should not change the database" $
ps'^.database `shouldBe` db
awardAssistSpec :: Spec
awardAssistSpec = describe "awardAssist" $ do
let
joe
= newPlayer 1 "Joe" "centre"
& pYtd.psAssists .~ 1
& pLifetime.psAssists .~ 2
bob
= newPlayer 2 "Bob" "defense"
& pYtd.psAssists .~ 3
& pLifetime.psAssists .~ 4
joeStats
= newPlayerStats
& psAssists .~ 1
ps
= newProgState
& progMode.gameStateL.gamePlayerStats .~ M.singleton 0 joeStats
& database.dbPlayers .~ [joe, bob]
mapM_
(\(pName, pid, ytd, lt, game) ->
context pName $ do
let
ps' = awardAssist pid ps
player = (ps'^.database.dbPlayers) !! pid
gStats = (ps'^.progMode.gameStateL.gamePlayerStats) M.! pid
it ("should increment " ++ pName ++ "'s year-to-date assists") $
player^.pYtd.psAssists `shouldBe` ytd
it ("should increment " ++ pName ++ "'s lifetime assists") $
player^.pLifetime.psAssists `shouldBe` lt
it ("should increment " ++ pName ++ "'s game assists") $
gStats^.psAssists `shouldBe` game)
-- player name, player id, ytd assists, lifetime assists, game assists
[ ( "Joe", 0, 2, 3, 2 )
, ( "Bob", 1, 4, 5, 1 )
]
context "invalid index" $ let
ps' = awardAssist (-1) ps
in it "should not change anything" $
ps'^.database.dbPlayers `shouldBe` ps^.database.dbPlayers
resetGoalDataSpec :: Spec
resetGoalDataSpec = describe "resetGoalData" $ do
players <- runIO $ replicateM 5 makePlayer
let
gs
= newGameState
& goalBy ?~ 1
& assistsBy .~ [2, 3]
& confirmGoalDataFlag .~ True
ps
= newProgState
& database.dbPlayers .~ players
& progMode.gameStateL .~ gs
& resetGoalData
it "should clear the goalBy value" $
ps^.progMode.gameStateL.goalBy `shouldBe` Nothing
it "should clear the assists by list" $
ps^.progMode.gameStateL.assistsBy `shouldBe` []
it "should clear confirmGoalDataFlag" $
ps^.progMode.gameStateL.confirmGoalDataFlag `shouldBe` False
assignPMinsSpec :: Spec
assignPMinsSpec = describe "assignPMins" $ let
bob = newPlayer 2 "Bob" "centre"
& pYtd.psPMin .~ 3
& pLifetime.psPMin .~ 4
joe = newPlayer 3 "Joe" "defense"
& pYtd.psPMin .~ 5
& pLifetime.psPMin .~ 6
ps pid = newProgState
& database.dbPlayers .~ [bob, joe]
& progMode.gameStateL
%~ (gamePlayerStats .~ M.fromList [(0, newPlayerStats & psPMin .~ 2)])
. (selectedPlayer .~ pid)
in mapM_
(\(pid, bobLt, bobYtd, bobGame, joeLt, joeYtd, joeGame) ->
context ("selectedPlayer = " ++ show pid) $ do
let ps' = assignPMins 2 $ ps pid
mapM_
(\(name, pid', lt, ytd, game) -> context name $ do
let
player = fromJust $ nth pid' $ ps'^.database.dbPlayers
gStats = ps'^.progMode.gameStateL.gamePlayerStats
pStats = M.findWithDefault newPlayerStats pid' gStats
context "lifetime penalty minutes" $
it ("should be " ++ show lt) $
player^.pLifetime.psPMin `shouldBe` lt
context "year-to-date penalty minutes" $
it ("should be " ++ show ytd) $
player^.pYtd.psPMin `shouldBe` ytd
context "game penalty minutes" $
it ("should be " ++ show game) $
pStats^.psPMin `shouldBe` game)
-- name, index, lifetime, ytd, game
[ ( "Bob", 0, bobLt, bobYtd, bobGame )
, ( "Joe", 1, joeLt, joeYtd, joeGame )
]
it "should set selectedPlayer to Nothing" $
ps'^.progMode.gameStateL.selectedPlayer `shouldBe` Nothing)
-- index, bob lt, bob ytd, bob game, joe lt, joe ytd, joe game
[ ( Just 0, 6, 5, 4, 6, 5, 0 )
, ( Just 1, 4, 3, 2, 8, 7, 2 )
, ( Just 2, 4, 3, 2, 6, 5, 0 )
, ( Nothing, 4, 3, 2, 6, 5, 0 )
]
makePlayer :: IO Player makePlayer :: IO Player
makePlayer = Player makePlayer = Player
<$> makeNum <$> makeNum
@@ -351,3 +654,49 @@ makeNum = randomRIO (1, 10)
makeName :: IO String makeName :: IO String
makeName = replicateM 10 $ randomRIO ('A', 'Z') makeName = replicateM 10 $ randomRIO ('A', 'Z')
backHomeSpec :: Spec
backHomeSpec = describe "backHome" $ do
let
input = newProgState
& progMode.gameStateL .~ newGameState
& inputBuffer .~ "foo"
& scrollOffset .~ 123
result = backHome input
it "should set the program mode back to MainMenu" $
result^.progMode `shouldSatisfy` \case
MainMenu -> True
_ -> False
it "should clear the input buffer" $
result^.inputBuffer `shouldBe` ""
it "should reset the scroll offset" $
result^.scrollOffset `shouldBe` 0
scrollUpSpec :: Spec
scrollUpSpec = describe "scrollUp" $ do
context "scrolled down" $
it "should decrease the scroll offset by one" $ let
ps = newProgState & scrollOffset .~ 10
ps' = scrollUp ps
in ps'^.scrollOffset `shouldBe` 9
context "at top" $
it "should keep the scroll offset at zero" $ let
ps = scrollUp newProgState
in ps^.scrollOffset `shouldBe` 0
context "above top" $
it "should return the scroll offset to zero" $ let
ps = newProgState & scrollOffset .~ (-10)
ps' = scrollUp ps
in ps'^.scrollOffset `shouldBe` 0
scrollDownSpec = describe "scrollDown" $
it "should increase the scroll offset" $ let
ps = newProgState & scrollOffset .~ 10
ps' = scrollDown ps
in ps'^.scrollOffset `shouldBe` 11

46
test/HandlersSpec.hs Normal file
View File

@@ -0,0 +1,46 @@
{-
mtlstats
Copyright (C) 2019 Rhéal Lamothe
<rheal.lamothe@gmail.com>
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 <https://www.gnu.org/licenses/>.
-}
module HandlersSpec (spec) where
import Test.Hspec (Spec, context, describe, it, shouldBe)
import qualified UI.NCurses as C
import Mtlstats.Handlers
spec :: Spec
spec = describe "Mtlstats.Handlers"
ynHandlerSpec
ynHandlerSpec :: Spec
ynHandlerSpec = describe "ynHandler" $ mapM_
(\(desc, event, expected) ->
context desc $
it ("should be " ++ show expected) $
ynHandler event `shouldBe` expected)
-- description, event, expected
[ ( "Y pressed", C.EventCharacter 'Y', Just True )
, ( "y pressed", C.EventCharacter 'y', Just True )
, ( "N pressed", C.EventCharacter 'N', Just False )
, ( "n pressed", C.EventCharacter 'n', Just False )
, ( "x pressed", C.EventCharacter 'x', Nothing )
, ( "other event", C.EventResized, Nothing )
]

65
test/ReportSpec.hs Normal file
View File

@@ -0,0 +1,65 @@
{-
mtlstats
Copyright (C) 2019 Rhéal Lamothe
<rheal.lamothe@gmail.com>
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 <https://www.gnu.org/licenses/>.
-}
module ReportSpec (spec) where
import Lens.Micro ((&), (?~), (%~))
import Test.Hspec (Spec, context, describe, it, shouldBe)
import Mtlstats.Report
import Mtlstats.Types
spec :: Spec
spec = describe "Mtlstats.Report" $ do
gameDateSpec
playerNameColWidthSpec
gameDateSpec :: Spec
gameDateSpec = describe "gameDate" $ do
context "valid gameDate" $
it "should format the date" $ let
gs = newGameState
& gameYear ?~ 1980
& gameMonth ?~ 6
& gameDay ?~ 25
in gameDate gs `shouldBe` "JUN 25 1980"
context "invalid date" $
it "should return an empty string" $
gameDate newGameState `shouldBe` ""
playerNameColWidthSpec :: Spec
playerNameColWidthSpec = describe "playerNameColWidth" $ do
let
short1 = newPlayer 1 "short" "foo"
short2 = newPlayer 2 "shorty" "bar"
long = newPlayer 3 "123456789012345" "baz"
mapM_
(\(label, players, expected) -> context label $
it ("should be " ++ show expected) $
playerNameColWidth players `shouldBe` expected)
-- label, players, expected
[ ( "empty list", [], 10 )
, ( "short names", [short1, short2], 10 )
, ( "long name", [short1, long], 16 )
]

View File

@@ -23,10 +23,16 @@ import Test.Hspec (hspec)
import qualified ActionsSpec as Actions import qualified ActionsSpec as Actions
import qualified FormatSpec as Format import qualified FormatSpec as Format
import qualified HandlersSpec as Handlers
import qualified ReportSpec as Report
import qualified TypesSpec as Types import qualified TypesSpec as Types
import qualified UtilSpec as Util
main :: IO () main :: IO ()
main = hspec $ do main = hspec $ do
Types.spec Types.spec
Actions.spec Actions.spec
Format.spec Format.spec
Handlers.spec
Report.spec
Util.spec

View File

@@ -42,6 +42,7 @@ spec = describe "Mtlstats.Types" $ do
gameStatsSpec gameStatsSpec
databaseSpec databaseSpec
gameStateLSpec gameStateLSpec
createPlayerStateLSpec
teamScoreSpec teamScoreSpec
otherScoreSpec otherScoreSpec
homeTeamSpec homeTeamSpec
@@ -49,10 +50,17 @@ spec = describe "Mtlstats.Types" $ do
gameWonSpec gameWonSpec
gameLostSpec gameLostSpec
gameTiedSpec gameTiedSpec
unaccountedPointsSpec
gmsGamesSpec gmsGamesSpec
gmsPointsSpec gmsPointsSpec
addGameStatsSpec addGameStatsSpec
pPointsSpec playerSearchSpec
playerSearchExactSpec
modifyPlayerSpec
playerSummarySpec
playerIsActiveSpec
psPointsSpec
addPlayerStatsSpec
Menu.spec Menu.spec
playerSpec :: Spec playerSpec :: Spec
@@ -81,6 +89,34 @@ gameStateLSpec = describe "gameStateL" $ lensSpec gameStateL
] ]
where gs t = newGameState & gameType ?~ t where gs t = newGameState & gameType ?~ t
createPlayerStateLSpec :: Spec
createPlayerStateLSpec = describe "createPlayerStateL" $ do
context "getters" $ do
context "state missing" $ let
pm = MainMenu
cps = pm^.createPlayerStateL
in it "should not have a number" $
cps^.cpsNumber `shouldBe` Nothing
context "existing state" $ let
pm = CreatePlayer $ newCreatePlayerState & cpsNumber ?~ 1
cps = pm^.createPlayerStateL
in it "should have a number of 1" $
cps^.cpsNumber `shouldBe` Just 1
context "setters" $ do
context "state missing" $ let
pm = MainMenu
pm' = pm & createPlayerStateL.cpsNumber ?~ 1
in it "should set the player number to 1" $
pm'^.createPlayerStateL.cpsNumber `shouldBe` Just 1
context "existing state" $ let
pm = CreatePlayer $ newCreatePlayerState & cpsNumber ?~ 1
pm' = pm & createPlayerStateL.cpsNumber ?~ 2
in it "should set the player number to 2" $
pm'^.createPlayerStateL.cpsNumber `shouldBe` Just 2
teamScoreSpec :: Spec teamScoreSpec :: Spec
teamScoreSpec = describe "teamScore" $ do teamScoreSpec = describe "teamScore" $ do
let let
@@ -223,16 +259,20 @@ goalieStatsJSON n = Object $ HM.fromList
gameStats :: Int -> GameStats gameStats :: Int -> GameStats
gameStats n = GameStats gameStats n = GameStats
{ _gmsWins = n { _gmsWins = n
, _gmsLosses = n + 1 , _gmsLosses = n + 1
, _gmsOvertime = n + 2 , _gmsOvertime = n + 2
, _gmsGoalsFor = n + 3
, _gmsGoalsAgainst = n + 4
} }
gameStatsJSON :: Int -> Value gameStatsJSON :: Int -> Value
gameStatsJSON n = Object $ HM.fromList gameStatsJSON n = Object $ HM.fromList
[ ( "wins", toJSON n ) [ ( "wins", toJSON n )
, ( "losses", toJSON $ n + 1 ) , ( "losses", toJSON $ n + 1 )
, ( "overtime", toJSON $ n + 2 ) , ( "overtime", toJSON $ n + 2 )
, ( "goals_for", toJSON $ n + 3 )
, ( "goals_against", toJSON $ n + 4 )
] ]
db :: Database db :: Database
@@ -320,30 +360,36 @@ gameWonSpec = describe "gameWon" $ mapM_
gameLostSpec :: Spec gameLostSpec :: Spec
gameLostSpec = describe "gameLost" $ mapM_ gameLostSpec = describe "gameLost" $ mapM_
(\(t, h, a, expected) -> let (\(t, h, a, ot, expected) -> let
desc = "game type: " ++ show t ++ desc = "game type: " ++ show t ++
", home score: " ++ show h ++ ", home score: " ++ show h ++
", away score: " ++ show a ", away score: " ++ show a ++
", overtimr flag: " ++ show ot
gs = newGameState gs = newGameState
& gameType .~ t & gameType .~ t
& homeScore .~ h & homeScore .~ h
& awayScore .~ a & awayScore .~ a
& overtimeFlag .~ ot
in context desc $ in context desc $
it ("should be " ++ show expected) $ it ("should be " ++ show expected) $
gameLost gs `shouldBe` expected) gameLost gs `shouldBe` expected)
-- gameType, homeScore, awayScore, expected -- gameType, homeScore, awayScore, overtimeFlag, expected
[ ( Just HomeGame, Just 1, Just 1, Just False ) [ ( Just HomeGame, Just 1, Just 1, Just False, Just False )
, ( Just HomeGame, Just 1, Just 2, Just True ) , ( Just HomeGame, Just 1, Just 2, Just False, Just True )
, ( Just HomeGame, Just 2, Just 1, Just False ) , ( Just HomeGame, Just 1, Just 2, Just True, Just False )
, ( Just AwayGame, Just 1, Just 1, Just False ) , ( Just HomeGame, Just 2, Just 1, Just False, Just False )
, ( Just AwayGame, Just 1, Just 2, Just False ) , ( Just AwayGame, Just 1, Just 1, Just False, Just False )
, ( Just AwayGame, Just 2, Just 1, Just True ) , ( Just AwayGame, Just 1, Just 2, Just False, Just False )
, ( Nothing, Just 1, Just 2, Nothing ) , ( Just AwayGame, Just 2, Just 1, Just False, Just True )
, ( Just HomeGame, Nothing, Just 1, Nothing ) , ( Just AwayGame, Just 2, Just 1, Just True, Just False )
, ( Just AwayGame, Nothing, Just 1, Nothing ) , ( Nothing, Just 1, Just 2, Just False, Nothing )
, ( Just HomeGame, Just 1, Nothing, Nothing ) , ( Just HomeGame, Nothing, Just 1, Just False, Nothing )
, ( Just AwayGame, Just 1, Nothing, Nothing ) , ( Just AwayGame, Nothing, Just 1, Just False, Nothing )
, ( Nothing, Nothing, Nothing, Nothing ) , ( Just HomeGame, Just 1, Nothing, Just False, Nothing )
, ( Just AwayGame, Just 1, Nothing, Just False, Nothing )
, ( Just HomeGame, Just 1, Just 2, Nothing, Nothing )
, ( Just AwayGame, Just 1, Just 2, Nothing, Nothing )
, ( Nothing, Nothing, Nothing, Just False, Nothing )
] ]
gameTiedSpec :: Spec gameTiedSpec :: Spec
@@ -364,33 +410,65 @@ gameTiedSpec = describe "gameTied" $ mapM_
, ( Just 1, Just 2, Just False ) , ( Just 1, Just 2, Just False )
] ]
unaccountedPointsSpec :: Spec
unaccountedPointsSpec = describe "unaccounted points" $ do
context "no data" $
it "should return Nothing" $
unaccountedPoints newGameState `shouldBe` Nothing
context "unaccounted points" $
it "should return True" $ let
gs = newGameState
& gameType ?~ HomeGame
& homeScore ?~ 1
in unaccountedPoints gs `shouldBe` Just True
context "all points accounted" $
it "should return False" $ let
gs = newGameState
& gameType ?~ HomeGame
& homeScore ?~ 1
& pointsAccounted .~ 1
in unaccountedPoints gs `shouldBe` Just False
context "more points accounted" $
it "should return True" $ let
gs = newGameState
& gameType ?~ HomeGame
& homeScore ?~ 1
& pointsAccounted .~ 2
in unaccountedPoints gs `shouldBe` Just False
gmsGamesSpec :: Spec gmsGamesSpec :: Spec
gmsGamesSpec = describe "gmsGames" $ mapM_ gmsGamesSpec = describe "gmsGames" $ mapM_
(\(w, l, expected) -> let (\(w, l, ot, expected) -> let
desc = "wins: " ++ show w ++ desc = "wins: " ++ show w ++
", losses: " ++ show l ", losses: " ++ show l ++
", overtime: " ++ show ot
gs = newGameStats gs = newGameStats
& gmsWins .~ w & gmsWins .~ w
& gmsLosses .~ l & gmsLosses .~ l
& gmsOvertime .~ ot
in context desc $ in context desc $
it ("should be " ++ show expected) $ it ("should be " ++ show expected) $
gmsGames gs `shouldBe` expected) gmsGames gs `shouldBe` expected)
-- wins, losses, expected -- wins, losses, overtime, expected
[ ( 0, 0, 0 ) [ ( 0, 0, 0, 0 )
, ( 1, 0, 1 ) , ( 1, 0, 0, 1 )
, ( 0, 1, 1 ) , ( 0, 1, 0, 1 )
, ( 1, 1, 2 ) , ( 0, 0, 1, 1 )
, ( 2, 3, 5 ) , ( 1, 1, 1, 3 )
, ( 2, 3, 5, 10 )
] ]
gmsPointsSpec :: Spec gmsPointsSpec :: Spec
gmsPointsSpec = describe "gmsPoints" $ mapM_ gmsPointsSpec = describe "gmsPoints" $ mapM_
(\(w, l, ot, expected) -> let (\(w, l, ot, expected) -> let
gs = GameStats gs
{ _gmsWins = w = newGameStats
, _gmsLosses = l & gmsWins .~ w
, _gmsOvertime = ot & gmsLosses .~ l
} & gmsOvertime .~ ot
in context (show gs) $ in context (show gs) $
it ("should be " ++ show expected) $ it ("should be " ++ show expected) $
gmsPoints gs `shouldBe` expected) gmsPoints gs `shouldBe` expected)
@@ -408,27 +486,110 @@ addGameStatsSpec = describe "addGameStats" $
it "should add the values" $ let it "should add the values" $ let
s1 = GameStats s1 = GameStats
{ _gmsWins = 1 { _gmsWins = 1
, _gmsLosses = 3 , _gmsLosses = 2
, _gmsOvertime = 2 , _gmsOvertime = 3
, _gmsGoalsFor = 4
, _gmsGoalsAgainst = 5
} }
s2 = GameStats s2 = GameStats
{ _gmsWins = 4 { _gmsWins = 6
, _gmsLosses = 6 , _gmsLosses = 7
, _gmsOvertime = 5 , _gmsOvertime = 8
, _gmsGoalsFor = 9
, _gmsGoalsAgainst = 10
} }
expected = GameStats expected = GameStats
{ _gmsWins = 5 { _gmsWins = 7
, _gmsLosses = 9 , _gmsLosses = 9
, _gmsOvertime = 7 , _gmsOvertime = 11
, _gmsGoalsFor = 13
, _gmsGoalsAgainst = 15
} }
in addGameStats s1 s2 `shouldBe` expected in addGameStats s1 s2 `shouldBe` expected
pPointsSpec :: Spec playerSearchSpec :: Spec
pPointsSpec = describe "pPoints" $ mapM_ playerSearchSpec = describe "playerSearch" $ mapM_
(\(sStr, expected) -> context sStr $
it ("should return " ++ show expected) $ let
ps = [joe, bob, steve]
in playerSearch sStr ps `shouldBe` expected)
-- search, result
[ ( "Joe", [(0, joe)] )
, ( "o", [(0, joe), (1, bob)] )
, ( "e", [(0, joe), (2, steve)] )
, ( "x", [] )
]
playerSearchExactSpec :: Spec
playerSearchExactSpec = describe "playerSearchExact" $ mapM_
(\(sStr, expected) -> context sStr $
it ("should be " ++ show expected) $ let
ps = [joe, bob, steve]
in playerSearchExact sStr ps `shouldBe` expected)
-- search, result
[ ( "Joe", Just (0, joe) )
, ( "Bob", Just (1, bob) )
, ( "Steve", Just (2, steve) )
, ( "Sam", Nothing )
, ( "", Nothing )
]
modifyPlayerSpec :: Spec
modifyPlayerSpec = describe "modifyPlayer" $ mapM_
(\(pName, j, b, s) -> let
modifier = pLifetime.psGoals .~ 1
players = modifyPlayer modifier pName [joe, bob, steve]
in context ("modify " ++ pName) $ do
context "Joe's lifetime goals" $
it ("should be " ++ show j) $
head players ^. pLifetime.psGoals `shouldBe` j
context "Bob's lifetime goals" $
it ("should be " ++ show b) $
(players !! 1) ^. pLifetime.psGoals `shouldBe` b
context "Steve's lifetime goals" $
it ("should be " ++ show s) $
last players ^. pLifetime.psGoals `shouldBe` s)
-- player name, Joe's goals, Bob's goals, Steve's goals
[ ( "Joe", 1, 0, 0 )
, ( "Bob", 0, 1, 0 )
, ( "Steve", 0, 0, 1 )
, ( "Sam", 0, 0, 0 )
]
playerSummarySpec :: Spec
playerSummarySpec = describe "playerSummary" $
it "should be \"Joe (2) center\"" $
playerSummary joe `shouldBe` "Joe (2) center"
playerIsActiveSpec :: Spec
playerIsActiveSpec = describe "playerIsActive" $ do
let
pState = newPlayerStats
& psGoals .~ 10
& psAssists .~ 11
& psPMin .~ 12
player = newPlayer 1 "Joe" "centre" & pLifetime .~ pState
mapM_
(\(label, player', expected) -> context label $
it ("should be " ++ show expected) $
playerIsActive player' `shouldBe` expected)
-- label, player, expected
[ ( "not active", player, False )
, ( "has goal", player & pYtd.psGoals .~ 1, True )
, ( "has assist", player & pYtd.psAssists .~ 1, True )
, ( "has penalty minute", player & pYtd.psPMin .~ 1, True )
]
psPointsSpec :: Spec
psPointsSpec = describe "psPoints" $ mapM_
(\(goals, assists, points) -> let (\(goals, assists, points) -> let
desc = "goals: " ++ show goals ++ desc = "goals: " ++ show goals ++
", assists: " ++ show assists ", assists: " ++ show assists
@@ -437,10 +598,46 @@ pPointsSpec = describe "pPoints" $ mapM_
psAssists .~ assists psAssists .~ assists
in context desc $ in context desc $
it ("should be " ++ show points) $ it ("should be " ++ show points) $
pPoints stats `shouldBe` points) psPoints stats `shouldBe` points)
-- goals, assists, points -- goals, assists, points
[ ( 0, 0, 0 ) [ ( 0, 0, 0 )
, ( 1, 0, 1 ) , ( 1, 0, 1 )
, ( 0, 1, 1 ) , ( 0, 1, 1 )
, ( 2, 3, 5 ) , ( 2, 3, 5 )
] ]
addPlayerStatsSpec :: Spec
addPlayerStatsSpec = describe "addPlayerStats" $ do
let
s1
= newPlayerStats
& psGoals .~ 1
& psAssists .~ 2
& psPMin .~ 3
s2
= newPlayerStats
& psGoals .~ 4
& psAssists .~ 5
& psPMin .~ 6
s3 = addPlayerStats s1 s2
describe "psGoals" $
it "should be 5" $
s3^.psGoals `shouldBe` 5
describe "psAssists" $
it "should be 7" $
s3^.psAssists `shouldBe` 7
describe "psPMin" $
it "should be 9" $
s3^.psPMin `shouldBe` 9
joe :: Player
joe = newPlayer 2 "Joe" "center"
bob :: Player
bob = newPlayer 3 "Bob" "defense"
steve :: Player
steve = newPlayer 5 "Steve" "forward"

94
test/UtilSpec.hs Normal file
View File

@@ -0,0 +1,94 @@
{-
mtlstats
Copyright (C) 2019 Rhéal Lamothe
<rheal.lamothe@gmail.com>
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 <https://www.gnu.org/licenses/>.
-}
module UtilSpec (spec) where
import qualified Data.Map as M
import Test.Hspec (Spec, context, describe, it, shouldBe)
import Mtlstats.Util
spec :: Spec
spec = describe "Mtlstats.Util" $ do
nthSpec
modifyNthSpec
updateMapSpec
sliceSpec
nthSpec :: Spec
nthSpec = describe "nth" $ mapM_
(\(n, expected) -> context (show n) $
it ("should be " ++ show expected) $ let
xs = ["foo", "bar", "baz"]
in nth n xs `shouldBe` expected)
-- index, expected
[ ( 0, Just "foo" )
, ( 1, Just "bar" )
, ( 2, Just "baz" )
, ( 3, Nothing )
, ( -1, Nothing )
]
modifyNthSpec :: Spec
modifyNthSpec = describe "modifyNth" $ do
context "in bounds" $
it "should modify the value" $
modifyNth 1 succ [1, 2, 3] `shouldBe` [1, 3, 3]
context "out of bounds" $
it "should not modify the value" $
modifyNth 3 succ [1, 2, 3] `shouldBe` [1, 2, 3]
context "negative index" $
it "should not modify the value" $
modifyNth (-1) succ [1, 2, 3] `shouldBe` [1, 2, 3]
updateMapSpec :: Spec
updateMapSpec = describe "updateMap" $ do
let
input = M.fromList [(1, 2), (3, 5)]
context "key found" $ let
expected = M.fromList [(1, 3), (3, 5)]
in it "should update the value" $
updateMap 1 10 succ input `shouldBe` expected
context "key not found" $ let
expected = M.fromList [(1, 2), (3, 5), (10, 11)]
in it "should create a new value and update the default" $
updateMap 10 10 succ input `shouldBe` expected
sliceSpec :: Spec
sliceSpec = describe "slice" $ do
let list = [2, 4, 6, 8]
context "sublist" $
it "should return the sublist" $
slice 1 2 list `shouldBe` [4, 6]
context "too large" $
it "should return as much of the list as possible" $
slice 1 100 list `shouldBe` [4, 6, 8]
context "negative offset" $
it "should return the correct number of elements from the beginning" $
slice (-10) 2 list `shouldBe` [2, 4]