Jan 1, 0001 00:00 · 4387 words · 21 minutes read

Blog Setup

In the server, in case you get a 402 error, ensure that the permissions are properly set:

chmod -R 700 ~/bonfacemunyoki.com
chown -R bonface.www-data ~/bonfacemunyoki.com

The deploy script:


#!/bin/bash

cd ~/Public/BonfaceKilz/
echo -e "\033[0;32mDeploying updates to Remote Site...\033[0m"
hugo

rsync -avz --delete public/ [email protected]:~/bonfacemunyoki.com/

DONE Switching to Programmer’s Dvorak

I’m tired of trying to do something worthwhile for the human race, they simply don’t want to change!

– Dr. August Dvorak

Three weeks ago, I decided to switch from the QWERTY keyboard layout to the Programmer Dvorak keyboard layout. This layout is a modification of the Dvorak keyboard layout which moves the characters that programmers usually need to the fingers1. My boss’ insistence on this layout’s efficiency inspired me to try out the layout.

So far(this is my third week), I’m okay with the switch. I have had to practice how to touch type using a typing tutor from some online resource. At first, the switch was frustrating and confusing, but after a while, my fingers got used to the new layout. Another side effect of adapting this keyboard layout is that it has made my machine really hard to use for people who use the QWERTY layout. Anytime a random person tries to type something on my machine, they inadvertently type gibberish.

For anyone interested in learning Dvorak’s history, look at this zine. This zine does a great job in explaining the history of both QWERTY and the Dvorak keyboard layout.

DONE Monoids and Semigroups

Before talking about Monoids and Semigroups, you first have to understand what “an algebra”(in the context of Functional Programming) is. From Mathematics, Algebra is a tool used to manipulate symbols. We really don’t care what those symbols contain; instead, what we care about is some of the laws you have to follow. In the Functional Programming universe, an algebra is an operation and the set it can operate on. Both a Monoid and Semigroup are algebras.

A Monoid is an operation which meets the following properties:

  • It has an identity. An identity function always returns the same value used as its argument. The id of the sum operation is 0 and the id of the product is 1. A Monoid has both the left and right identity.
  • It is associative. Associativity means that an operation can be grouped in any order e.g. : (a + b) + c == a + (b + c)
  • It is a binary operator. A binary operator only takes 2 arguments.


A Semigroup is similar to the Monoid; the only difference being that it does not have to meet the identity requirement. A Monoid can be considered a subset of the Semigroup set.

In Haskell, the Monoid is defined in its own typeclass as:

class Monoid m where
  mempty  :: m
  mappend :: m -> m -> m

  mconcat :: [m] -> m
  mconcat = foldr mappend mempty

When a specific type implements the Monoid typeclass, it will automatically be reduce-able. You can also be able to use the infix operator `<>` which behaves in a similar way to `mappend`. In Haskell, the most common case of a type that has an instance of the Monoid typeclass is the List.

mappend [1, 2, 3] [4, 5, 6]
[1, 2, 3] <> [4, 5, 6]
[1, 2, 3] ++ [4, 5, 6]

-- They all print the same thing:
-- [1, 2, 3, 4, 5, 6]

DONE Using Computers in the digital age

I suppose many people will continue moving towards careless computing, because there’s a sucker born every minute.

– Richard Stallman

Computing has become an important driver in our daily lives. This is in the form of our mobile devices (which power most of our daily interactions with other people) and workstations(which is a big driving force in our work and private lives). The internet has become more accessible and cheaper than ever before. This has led to the rise of social media platforms(engineered to retain their use) for different niches in our population.

Earlier today, I left my Android phone in an Uber. Initially, I panicked and thought that I had permanently lost my phone. Later after calming down, I used the Uber platform to report the missing phone. This did not work out well. I dialled my line severally in the hopes that the Uber driver would pick it up. Unfortunately, the phone was in silent-mode. Since the phone was ringing, I figured that the phone was still on. I proceeded to use Google’s Android Device Manager to track my phone. I used this platform to remotely ring my device while simultaneously placing a call. This finally caught the attention of the driver. My phone was safely returned in a few hours.

In the period of time that I was phoneless, I felt “incomplete”. All my instant messaging platforms were moot, I couldn’t read my mail, and the unnerving paranoia of something fishy happening on my device hovered like some dark gloomy cloud in my head. Needless to say, I was unproductive for a while. After getting my phone back, it hit me hard just how dependent I had become on my devices. This sparked some interesting conversation with the guys at the office.

I have become dependent on my computing devices. Computing devices are meant to be used, not the other way round. However, it’s becoming increasingly hard to “just use” your devices to get things done. I often find myself doing time-wasting things on my devices to just pass time. I think that using “closed-source” applications (which are sometimes marketed as “open source applications”) are partly to blame for this. These applications “just work” and are convenient. They encourage consumerism, albeit in a techie landscape. Companies and corporations that are behind these apps(and devices) collect your data and do “things” with it, for their own benefit.

Some simple piece of advice from my boss may offer a simple solution to escaping the computing dungeon a lot of us are in. It’s simple: use a computer to get work done and have a life outside them. Work in this context is used in a loose manner to also encapsulate entertainment, exploratory stuff, and other trivial things in addition to actual wqrk. The hugest take-away from this is to be objective about what you want to do with the computer(or device). In my case, I list down everything I want to do with my laptop before I open it. This includes trivial things such as watching videos and browsing sub-reddits. The next step I do is to put this list in some org file. I’ll use this list to guide me when doing things on my laptop. Once I’m done, I’ll turn off my laptop and do something else.

I don’t know how extending the aforementioned ideas work out in a mobile device. For starters, I want to install Lineage OS on my android device in a bid to shake myself loose from Google(it’s playstore and other default apps on Android). From there, I will pick things up and continue my pursuit “freedom”.

Weekend summary

[2018-12-30 Sun 21:16]

Once a month, my brother and I normally spend the whole weekend at a friend’s place

  • [ ] Goals and work ethic

    • do your best daily in a measurable way
    • aim for smaller achievable goals/ tasks as opposed to larger more unreachable tasks
    • Measurability and why it is important
    • Aim to improve yourself
    • Too much information can be a bad thing. Overfitting your life- ideas from: Algorithms to live by
  • [ ] Devin

    • You’ll never know everything
    • (if 1 = x) vs (if x = 1) <- hacks that make the compiler catch errors
    • Be objective vs emotional in work
    • Don’t take things personally
    • Staying a dev vs moving to management
    • More experienced people are usually paid more
    • static vs weakly vs strongly vs dynamically typed languages
    • serializing and deserializing data
  • [ ] Tooling

    • VS code is just fine; how it is maybe slowly taking over
    • Emacs/ Vim is not really scalable
    • Microsoft may open source it’s things
    • The challenge of open sourcing things. It’s hard open sourcing crap code, though you can do it and not make no noise about it
    • Microsoft is moving to Service Oriented Architecture and IAAS; and shying away from os
    • What it means to be open-source nowadays
  • [ ] Reading suggestions

    • Algorithms we live by
    • Noam Talib books
    • Code Simplicity
  • [ ] Agile/ Scrum/ Project Management

    • Time estimates don’t really work
    • Check out the fibonacci scale.
    • Learn from failure to improve your processes

Motherhood - An Essay

Teach girls it’s okay to not want kids. Or even like that. That they can be functional people without being a mother. That deciding into their 30s they want kids isn’t bad and they’re not “too old”. Because motherhood isn’t for everyone. And we need to stop acting like it is.

– Anonymous

I came across the above quote from some random person online. My immediate natural response was: “That’s hard to do”. This post explains why, at least from my point of view.

Let’s start with a little story here first. Imagine two kinds of people here: People A and People B. These 2 groups of people are totally equal, well at least in ability. Now imagine, that at one point in time, People A got a head start in life- they get good jobs where they cultivate their skills. People A get to run the society. They rise in leadership positions and come up with laws that oppress People B. A huge rift grows between the abilities of People A and B. Eventually, some people who are B’s start fighting for their own equal rights. Some sensible people who are A’s see some sense and see the sense in what B’s are fighting for. They push the agenda of equality. Things seem just fine, right?

Well, not really. Oppression in that society will have become systemic. People B will be associated with some kinds of things and jobs in society. Heck, some B’s will form some negative biases against other B’s who are trying to go against the status quo. They may be labelled “queer”.

Also, what it means to be equal has also changed. Who defines equality in this society? What does it mean to be an ‘A’ or a ‘B’ in this society? There’s a myriad of problems not explored in this short story, but I hope you get the point: Things get complicated. There really is no easy “fix”.

The above problem applies to how us humans have formed segregations along some lines: male vs female, white vs black, rich vs poor etc etc.

Now enter motherhood. We live in weird times(I bet every human during different times said this). We live in an age where we treat women the same way we treat men(at least in urban areas). This has had weird side effects, like the emergence of incels. Motherhood is a choice. I support this opinion(I’m a Stallman fanboy Lol- free as in freedom). However, are women who are against motherhood really prepared for the kickbacks from our society?

“Peer pressure” has become compounded by social media. Pictures of new young families paint the facebook and twitter feeds of some people. How does this play out in shaping someone’s opinion?

For companionship seekers living in a conservative environment, how hard or easy is it to find someone you dig that shares your opinions?

– social media – peer pressure – unique situations women face – how dudes view women

A tic-tac-toe game in Haskell.

The game is implemented in Haskell.

Motivation: somebody requested me to do this in haskell

Base Configuration

  • The configuration is usually autogenerated by running ```stack init``` in the root dir
  • Do not touch these files. They are normally auto-generated
  • I used the simple template here.
  • You really don’t need this stuff since the stack will do everything for you

stack.yml:

# This file was automatically generated by 'stack init'
#
# Some commonly used options have been documented as comments in this file.
# For advanced use and comprehensive documentation of the format, please see:
# http://docs.haskellstack.org/en/stable/yaml_configuration/

# Resolver to choose a 'specific' stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# resolver: lts-3.5
# resolver: nightly-2015-09-21
# resolver: ghc-7.10.2
# resolver: ghcjs-0.1.0_ghc-7.10.2
# resolver:
#  name: custom-snapshot
#  location: "./custom-snapshot.yaml"
resolver: lts-12.14

# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# - location:
#    git: https://github.com/commercialhaskell/stack.git
#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#   extra-dep: true
#  subdirs:
#  - auto-update
#  - wai
#
# A package marked 'extra-dep: true' will only be built if demanded by a
# non-dependency (i.e. a user package), and its test suites and benchmarks
# will not be run. This is useful for tweaking upstream packages.
packages:
- '.'
# Dependency packages to be pulled from upstream that are not in the resolver
# (e.g., acme-missiles-0.3)
extra-deps: []

# Override default flag values for local packages and extra-deps
flags: {}

# Extra package databases containing global packages
extra-package-dbs: []

# Control whether we use the GHC we find on the path
# system-ghc: true
#
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: ">=1.4"
#
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
#
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
#
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor
#nix:
#  enable: true
#  packages: [glpk, pcre]

Setup.hs:

import Distribution.Simple
main = defaultMain

tictactoe-hs.cabal:

You can change the executable to anything you want. Also ensure that you have random in the build-depends otherwise things won’t work

Things break if you don’t add the LICENSE and README file

name:                tictactoe-hs
version:             0.1.0.0
-- synopsis:
-- description:
homepage:            bonfacemunyoki.com
license:             BSD3
license-file:        LICENSE
author:              Bonface K. M.
maintainer:          [email protected]
copyright:           2018, BMK
category:            Game
build-type:          Simple
cabal-version:       >=1.10
extra-source-files:  README.md

executable ttt
  hs-source-dirs:      src
  main-is:             Main.hs
  default-language:    Haskell2010
  build-depends:       base >= 4.7 && < 5
                     , random
  other-modules:       TicTacToeLib

library
  hs-source-dirs:      src
  exposed-modules:     TicTacToeLib
  default-language:    Haskell2010
  build-depends:       base >= 4.7 && < 5
                     , random

LICENSE

Copyright Ben Lovy (c) 2018

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of Ben Lovy nor the names of other
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README

# tictactoe-hs
TicTacToe in Haskell.

`stack install` to build and install `ttt` executable.  Remember to set an alarm.  You can't afford another TicTacToe all-nighter and you know it.

The game

Project dependencies

  • Note the aligning- it makes things easy to read
  • Stick to code guidelines

import Control.Monad (forever, when)
import Data.Bool     (bool)
import Data.Char     (digitToInt)
import Data.List     (isSubsequenceOf)
import Data.Maybe    (isJust, isNothing)
import System.Exit   (exitSuccess)
import System.IO     (hFlush, stdout)
import System.Random (randomRIO)

Create our data types

  • We need a board
  • We also need to describe how our board will be displayed
  • Something like this: 1 2 3 4 5 6 7 8 9
  • Each element in the board is a cell.

#+NAME create the board data type

newtype Board = Board [Maybe Player]
data Player = Human | Computer deriving (Eq, Show)

instance Show Board where
  show (Board cs) = foldr spaceEachThird [] . withIndicesFrom 1 . fmap showCell $ withIndicesFrom 1 cs
    where spaceEachThird a = (++) (bool (snd a) (snd a ++ "\n") (fst a `rem` 3 == 0))

Win states

  • Earlier considerations:
    • Create function that checks diagonal, horizontal, and vertical for wins
    • Too much work
  • Instead, have a list with the win states
  • Lazy but efficient

    -- All the states that indicate a win in the game
    winStates :: [[Int]]
    winStates = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
    

Cell Functions

  • We need to have functions that operate on a cell

  • Here all the functions that operate in a cell in a nutshell

-- An empty cell shows its number on the grid,
-- A play made by a human is represented with a: " X "
-- and that by a computer with a: " O "
showCell :: (Int, Maybe Player) -> String
showCell (n, Nothing)         = " " ++ show n ++ " "
showCell (_, (Just Human))    = " X "
showCell (_, (Just Computer)) = " O "

isCellOpen :: Board -> Int -> Bool
isCellOpen (Board b) n = isNothing $ b !! (n - 1)

playCell :: Board -> Int -> Player -> Board
playCell (Board b) n player = Board $ prePlayerCells ++ [Just player] ++ postPlayerCells
  where prePlayerCells = take (n - 1) b
        postPlayerCells = drop n b
  • Here’s an example of a board board(it won’t be displayed like that though):

    (1, Nothing) (2, (Just Human)) (3, Nothing) (4, Nothing) (5, Nothing) (6, Nothing) (7, Nothing) (8, Nothing) (9, (Just Computer))

  • So we need a function that can display cells. The above board would look sth like this: 1 X 3 4 5 6 7 8 O

-- An empty cell shows its number on the grid,
-- A play made by a human is represented with a: " X "
-- and that by a computer with a: " O "
showCell :: (Int, Maybe Player) -> String
showCell (n, Nothing)         = " " ++ show n ++ " "
showCell (_, (Just Human))    = " X "
showCell (_, (Just Computer)) = " O "
  • Before a player plays a cell, we need to check that the cell is free

isCellOpen :: Board -> Int -> Bool
isCellOpen (Board b) n = isNothing $ b !! (n - 1)
  • We need a fn that can play a cell. If you choose a num, it returns a board with the cell filled by the player

playCell :: Board -> Int -> Player -> Board
playCell (Board b) n player = Board $ prePlayerCells ++ [Just player] ++ postPlayerCells
  where prePlayerCells = take (n - 1) b
        postPlayerCells = drop n b

Board Functions

We need to be able to:

  • generate an empty board
  • enable a computer to play
  • enable a human to play and check the turns
  • check the wins
  • check for draws

When we put it all together:

freshBoard :: Board
freshBoard = Board $ replicate 9 Nothing

withIndicesFrom :: Int -> [a] -> [(Int, a)]
withIndicesFrom n = zip [n..]

compTurn :: Board -> IO Board
compTurn [email protected](Board b) = do
  let options = filter (isNothing . snd) . withIndicesFrom 1 $ b
  r <- randomRIO (0, length options - 1)
  let play = (fst $ options !! r)
  let b2 = playCell board play Computer
  putStrLn $ "Computer plays " ++ show play
  checkWin b2 Computer
  return b2

humanTurn :: Board -> Int -> IO Board
humanTurn board n = do
  let b = playCell board n Human
  checkWin b Human
  checkDraw b
  return b

checkWin :: Board -> Player -> IO ()
checkWin [email protected](Board b) m =
  let
    bi = withIndicesFrom 0 b
    plays = map fst . filter ((Just m ==) . snd) $ bi
  in
   when (foldr ((||) . flip isSubsequenceOf plays) False winStates) $ do
     print board
     putStrLn $ show m ++ " won!"
     exitSuccess

checkDraw :: Board -> IO ()
checkDraw [email protected](Board b) =
  when ( all isJust b) $ do
    print board
    putStrLn "Draw!"
    exitSuccess

First, we generate an empty board which has nothing. There’s also a generic function that will be used by the other board functions

freshBoard :: Board
freshBoard = Board $ replicate 9 Nothing

withIndicesFrom :: Int -> [a] -> [(Int, a)]
withIndicesFrom n = zip [n..]

The computer’s play is driven by a random number. You get all the free positions then randomly choose one. After the play, we check for a win. We return a board wrapped in a monad(a haskellish way of dealing with side fx)

compTurn :: Board -> IO Board
compTurn [email protected](Board b) = do
  let options = filter (isNothing . snd) . withIndicesFrom 1 $ b
  r <- randomRIO (0, length options - 1)
  let play = (fst $ options !! r)
  let b2 = playCell board play Computer
  putStrLn $ "Computer plays " ++ show play
  checkWin b2 Computer
  return b2

Same concept with a human play.

humanTurn :: Board -> Int -> IO Board
humanTurn board n = do
  let b = playCell board n Human
  checkWin b Human
  checkDraw b
  return b

To check a win, we check if there’s a sub-sequence of the win-states. If it exists, we end the game and declare the winner

checkWin :: Board -> Player -> IO ()
checkWin [email protected](Board b) m =
  let
    bi = withIndicesFrom 0 b
    plays = map fst . filter ((Just m ==) . snd) $ bi
  in
   when (foldr ((||) . flip isSubsequenceOf plays) False winStates) $ do
     print board
     putStrLn $ show m ++ " won!"
     exitSuccess

We really don’t need to check for losses. A win implies a loss to the other; Also checking for wins is really easier.

For draws, when all cells are filled with no wins declared, it’s a draw.

checkDraw :: Board -> IO ()
checkDraw [email protected](Board b) =
  when ( all isJust b) $ do
    print board
    putStrLn "Draw!"
    exitSuccess

Running the game

  • The game runs in an infinite loop
  • Here’s the algorithm for running this game:
    • check the draw
    • print the board
    • get the move from the human
    • if the move is valid, the comp plays
    • otherwise raise some warning

runGame :: Board -> IO ()
runGame board = forever $ do
  checkDraw board
  print board
  putStr "Your move: "
  hFlush stdout
  n <- getLine
  case n of
    [c] ->
      if [c] `elem` map show [(1::Integer)..9]
      then do
          let n' = digitToInt c
          if isCellOpen board n'
          then humanTurn board n' >>= compTurn >>= runGame
          else putStrLn "That's taken!"
      else putStrLn "1-9 only please"
    _   -> putStrLn "Only one digit allowed!"

The Game

  • I put the game logic in one file src/TicTacToe.hs
import Control.Monad (forever, when)
import Data.Bool     (bool)
import Data.Char     (digitToInt)
import Data.List     (isSubsequenceOf)
import Data.Maybe    (isJust, isNothing)
import System.Exit   (exitSuccess)
import System.IO     (hFlush, stdout)
import System.Random (randomRIO)



-- All the states that indicate a win in the game
winStates :: [[Int]]
winStates = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]

-- An empty cell shows its number on the grid,
-- A play made by a human is represented with a: " X "
-- and that by a computer with a: " O "
showCell :: (Int, Maybe Player) -> String
showCell (n, Nothing)         = " " ++ show n ++ " "
showCell (_, (Just Human))    = " X "
showCell (_, (Just Computer)) = " O "

isCellOpen :: Board -> Int -> Bool
isCellOpen (Board b) n = isNothing $ b !! (n - 1)

playCell :: Board -> Int -> Player -> Board
playCell (Board b) n player = Board $ prePlayerCells ++ [Just player] ++ postPlayerCells
  where prePlayerCells = take (n - 1) b
        postPlayerCells = drop n b

freshBoard :: Board
freshBoard = Board $ replicate 9 Nothing

withIndicesFrom :: Int -> [a] -> [(Int, a)]
withIndicesFrom n = zip [n..]

compTurn :: Board -> IO Board
compTurn [email protected](Board b) = do
  let options = filter (isNothing . snd) . withIndicesFrom 1 $ b
  r <- randomRIO (0, length options - 1)
  let play = (fst $ options !! r)
  let b2 = playCell board play Computer
  putStrLn $ "Computer plays " ++ show play
  checkWin b2 Computer
  return b2

humanTurn :: Board -> Int -> IO Board
humanTurn board n = do
  let b = playCell board n Human
  checkWin b Human
  checkDraw b
  return b

checkWin :: Board -> Player -> IO ()
checkWin [email protected](Board b) m =
  let
    bi = withIndicesFrom 0 b
    plays = map fst . filter ((Just m ==) . snd) $ bi
  in
   when (foldr ((||) . flip isSubsequenceOf plays) False winStates) $ do
     print board
     putStrLn $ show m ++ " won!"
     exitSuccess

checkDraw :: Board -> IO ()
checkDraw [email protected](Board b) =
  when ( all isJust b) $ do
    print board
    putStrLn "Draw!"
    exitSuccess

runGame :: Board -> IO ()
runGame board = forever $ do
  checkDraw board
  print board
  putStr "Your move: "
  hFlush stdout
  n <- getLine
  case n of
    [c] ->
      if [c] `elem` map show [(1::Integer)..9]
      then do
          let n' = digitToInt c
          if isCellOpen board n'
          then humanTurn board n' >>= compTurn >>= runGame
          else putStrLn "That's taken!"
      else putStrLn "1-9 only please"
    _   -> putStrLn "Only one digit allowed!"
  • The game will be run here:
module Main where

import TicTacToeLib

main :: IO ()
main = do
  let board = freshBoard
  runGame board