feat: documentation and box layout

This commit is contained in:
vegowotenks 2025-07-18 18:05:38 +02:00
parent cd5de79e24
commit e1f2d5b5ef
6 changed files with 68 additions and 68 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
.stack-work/ .stack-work/
*~ *~
data/

View file

@ -6,7 +6,7 @@ import qualified Sudoku
import qualified Sudoku.Solve as Solve import qualified Sudoku.Solve as Solve
import qualified Sudoku.Render as Render import qualified Sudoku.Render as Render
import Sudoku.State (State) import Sudoku.State (State)
import Options.Applicative (subparser, command, info, Parser, progDesc, execParser) import Options.Applicative (subparser, command, info, Parser, progDesc, execParser, idm, customExecParser, ParserPrefs, showHelpOnEmpty, prefs)
import System.Exit (exitFailure) import System.Exit (exitFailure)
data Mode data Mode
@ -21,7 +21,7 @@ parseMode = subparser
main :: IO () main :: IO ()
main = do main = do
mode <- execParser (info parseMode mempty) mode <- customExecParser optparsePreferences (info parseMode idm)
input <- Text.getContents input <- Text.getContents
sudoku <- case runParser Sudoku.parse "<stdin>" input of sudoku <- case runParser Sudoku.parse "<stdin>" input of
Left errorBundle -> (Text.putStr . Text.pack . errorBundlePretty $ errorBundle) >> exitFailure Left errorBundle -> (Text.putStr . Text.pack . errorBundlePretty $ errorBundle) >> exitFailure
@ -34,3 +34,6 @@ main = do
in Render.hints hints in Render.hints hints
optparsePreferences :: ParserPrefs
optparsePreferences = prefs showHelpOnEmpty

View file

@ -1,4 +1,4 @@
cabal-version: 2.2 cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.38.1. -- This file has been generated from package.yaml by hpack version 0.38.1.
-- --
@ -7,22 +7,15 @@ cabal-version: 2.2
name: solvedoku name: solvedoku
version: 0.1.0.0 version: 0.1.0.0
description: Please see the README on GitHub at <https://github.com/githubuser/solvedoku#readme> description: Please see the README on GitHub at <https://github.com/githubuser/solvedoku#readme>
homepage: https://github.com/githubuser/solvedoku#readme author: VegOwOtenks
bug-reports: https://github.com/githubuser/solvedoku/issues maintainer: vegowotenks@jossco.de
author: Author name here copyright: 2025 VegOwOtenks
maintainer: example@example.com license: AGPL-3.0
copyright: 2025 Author name here
license: BSD-3-Clause
license-file: LICENSE
build-type: Simple build-type: Simple
extra-source-files: extra-source-files:
README.md README.md
CHANGELOG.md CHANGELOG.md
source-repository head
type: git
location: https://github.com/githubuser/solvedoku
library library
exposed-modules: exposed-modules:
Data.Enum.Util Data.Enum.Util
@ -38,8 +31,6 @@ library
Sudoku.Triple Sudoku.Triple
other-modules: other-modules:
Paths_solvedoku Paths_solvedoku
autogen-modules:
Paths_solvedoku
hs-source-dirs: hs-source-dirs:
src src
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
@ -59,8 +50,6 @@ executable solvedoku-exe
main-is: Main.hs main-is: Main.hs
other-modules: other-modules:
Paths_solvedoku Paths_solvedoku
autogen-modules:
Paths_solvedoku
hs-source-dirs: hs-source-dirs:
app app
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
@ -82,8 +71,6 @@ test-suite solvedoku-test
main-is: Spec.hs main-is: Spec.hs
other-modules: other-modules:
Paths_solvedoku Paths_solvedoku
autogen-modules:
Paths_solvedoku
hs-source-dirs: hs-source-dirs:
test test
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N

View file

@ -16,6 +16,8 @@ import qualified Data.Foldable as Foldable
import Control.Arrow (Arrow ((***))) import Control.Arrow (Arrow ((***)))
import qualified Data.Text as Text import qualified Data.Text as Text
-- | A box of text. Has rectangular size, queried with 'size'.
data Box data Box
= Raw (Array (Int, Int) Char) = Raw (Array (Int, Int) Char)
| Grid (Array (Int, Int) Box) (Maybe Border) | Grid (Array (Int, Int) Box) (Maybe Border)
@ -23,6 +25,16 @@ data Box
data Border = Border data Border = Border
{ top, right, left, bottom, topRight, bottomRight, bottomLeft, topLeft, crosssection, verticalBetween, horizontalBetween :: Char } { top, right, left, bottom, topRight, bottomRight, bottomLeft, topLeft, crosssection, verticalBetween, horizontalBetween :: Char }
-- | Draw grids like this:
--
-- @
-- \/---\
-- | | |
-- |-+-|
-- | | |
-- \\---/
-- @
asciiBorder :: Border asciiBorder :: Border
asciiBorder = Border {top='-', right='|', left='|', bottom='-', topRight='\\', bottomRight='/', bottomLeft='\\', topLeft='/', crosssection='+', verticalBetween='|', horizontalBetween='-'} asciiBorder = Border {top='-', right='|', left='|', bottom='-', topRight='\\', bottomRight='/', bottomLeft='\\', topLeft='/', crosssection='+', verticalBetween='|', horizontalBetween='-'}
@ -32,6 +44,8 @@ type Column = Int
square :: Int -> ((Row, Column) -> Char) -> Box square :: Int -> ((Row, Column) -> Char) -> Box
square dim f = Raw $ genArray ((0, 0), (dim - 1, dim - 1)) f square dim f = Raw $ genArray ((0, 0), (dim - 1, dim - 1)) f
-- | All fields will be laid out like in a static grid. There is no compensation for differently sized Boxes in different rows.
grid :: Maybe Border -> (Int, Int) -> ((Int, Int) -> Box) -> Box grid :: Maybe Border -> (Int, Int) -> ((Int, Int) -> Box) -> Box
grid border (rows, cols) f = let grid border (rows, cols) f = let
@ -39,6 +53,8 @@ grid border (rows, cols) f = let
in Grid boxes border in Grid boxes border
-- | Use the 'asciiBorder'.
asciiGrid :: (Int, Int) -> ((Int, Int) -> Box) -> Box asciiGrid :: (Int, Int) -> ((Int, Int) -> Box) -> Box
asciiGrid = grid $ Just asciiBorder asciiGrid = grid $ Just asciiBorder
@ -47,6 +63,8 @@ gridCellSize = let
updateMaxSize box (height, width) = (max height *** max width) $ size box updateMaxSize box (height, width) = (max height *** max width) $ size box
in Foldable.foldr updateMaxSize (0, 0) in Foldable.foldr updateMaxSize (0, 0)
-- | The rectangular size of the box.
size :: Box -> (Int, Int) size :: Box -> (Int, Int)
size = \case size = \case
Raw chars -> arraySize chars -- snd takes the higher bound, +1 because the array has inclusive ranges Raw chars -> arraySize chars -- snd takes the higher bound, +1 because the array has inclusive ranges
@ -66,13 +84,15 @@ arraySize = join bimap (+1) . snd . Array.bounds
orElse :: Maybe a -> a -> a orElse :: Maybe a -> a -> a
orElse option x = Maybe.fromMaybe x option orElse option x = Maybe.fromMaybe x option
-- | Look at a single character in the box.
lookup :: Box -> (Int, Int) -> Maybe Char lookup :: Box -> (Int, Int) -> Maybe Char
lookup b index@(row, col) = case b of lookup b index@(row, col) = case b of
Raw chars -> chars Array.!? index Raw chars -> chars Array.!? index
Grid boxes borderM -> let Grid boxes borderM -> let
(boxRows, boxColumns) = arraySize boxes
(boxHeight, boxWidth) = gridCellSize boxes (boxHeight, boxWidth) = gridCellSize boxes
(totalHeight, totalWidth) = size b
in case borderM of in case borderM of
@ -83,33 +103,27 @@ lookup b index@(row, col) = case b of
innerBox <- boxes Array.!? (boxRow, boxCol) innerBox <- boxes Array.!? (boxRow, boxCol)
lookup innerBox (boxRowIndex, boxColIndex) lookup innerBox (boxRowIndex, boxColIndex)
Just border -> if Just border -> let
| row == 0 && col == 0 -> Just border.topLeft (boxRow, boxRowIndex) = pred row `divMod` (boxHeight + 1)
| row == 0 -> Just border.top (boxCol, boxColIndex) = pred col `divMod` (boxWidth + 1)
| col == 0 -> Just border.left
-- ^ outer border cases are easy
| otherwise -> let
row' = row - 1
col' = col - 1
(boxRow, boxRowIndex) = row' `divMod` (boxHeight + 1)
(boxCol, boxColIndex) = col' `divMod` (boxWidth + 1)
in if in if
| boxRowIndex == boxHeight && boxColIndex == boxWidth -> Just $ | row == 0 && col == 0 -> Just border.topLeft
if boxRow == boxRows - 1 && boxCol == boxColumns - 1 -- is it the last row and col? | col == 0 && row == totalHeight - 1 -> Just border.bottomLeft
then border.bottomRight | row == 0 && col == totalWidth - 1 -> Just border.topRight
else border.crosssection | row == totalHeight - 1 && col == totalWidth - 1 -> Just border.bottomRight
| boxRowIndex == boxHeight -> Just $ | row == 0 -> Just border.top
if boxRow == boxRows - 1 -- is it the last row? | row == totalHeight - 1 -> Just border.bottom
then border.bottom | col == 0 -> Just border.left
else border.horizontalBetween | col == totalWidth - 1 -> Just border.right
| boxColIndex == boxWidth -> Just $ | boxRowIndex == boxHeight && boxColIndex == boxWidth -> Just border.crosssection
if boxCol == boxColumns - 1 -- is it the last column? | boxRowIndex == boxHeight -> Just border.horizontalBetween
then border.right | boxColIndex == boxWidth -> Just border.verticalBetween
else border.verticalBetween | otherwise -> do
| otherwise -> do -- it's not at the border between boxes at all innerBox <- boxes Array.!? (boxRow, boxCol)
innerBox <- boxes Array.!? (boxRow, boxCol) lookup innerBox (boxRowIndex, boxColIndex)
lookup innerBox (boxRowIndex, boxColIndex)
-- | Render everything inside, recursively. Produces a rectangular string with line-breaks.
render :: Box -> Text render :: Box -> Text
render box = let render box = let

View file

@ -3,7 +3,7 @@
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE DeriveTraversable #-}
module Sudoku.Area (Area(..), indexFromDigit) where module Sudoku.Area (Area(..), indexFromDigit, digitFromIndex) where
import Sudoku.Triple (Triple, Index) import Sudoku.Triple (Triple, Index)
import Data.Functor.Compose (Compose(..)) import Data.Functor.Compose (Compose(..))
@ -20,18 +20,10 @@ instance Distributive Area where
distribute :: Functor f => f (Area a) -> Area (f a) distribute :: Functor f => f (Area a) -> Area (f a)
distribute = Area . distribute . fmap get distribute = Area . distribute . fmap get
indexFromDigit :: Int -> (Index, Index) -- | Accepts arguments from `0` to `8`. Everything else fails with 'error'
indexFromDigit i = join bimap toEnum $ i `divMod` 3 --
digitFromIndex :: (Index, Index) -> Int
digitFromIndex (outer, inner) = fromEnum outer * 3 + fromEnum inner
-- prop> \ i -> digitFromIndex (indexFromDigit (i `mod` 9)) == i `mod` 9 -- prop> \ i -> digitFromIndex (indexFromDigit (i `mod` 9)) == i `mod` 9
-- +++ OK, passed 100 tests. -- +++ OK, passed 100 tests.
-- >>> import Sudoku.Triple (Index(..))
-- >>> digitFromIndex (First, First)
-- 0
-- --
-- >>> indexFromDigit 9 -- >>> indexFromDigit 9
-- *** Exception: Enum.toEnum{Index}: tag (3) is outside of enumeration's range (0,2) -- *** Exception: Enum.toEnum{Index}: tag (3) is outside of enumeration's range (0,2)
@ -39,3 +31,15 @@ digitFromIndex (outer, inner) = fromEnum outer * 3 + fromEnum inner
-- >>> indexFromDigit 6 -- >>> indexFromDigit 6
-- (Third,First) -- (Third,First)
indexFromDigit :: Int -> (Index, Index)
indexFromDigit i = join bimap toEnum $ i `divMod` 3
-- | Inverse of 'indexFromDigit'
--
-- >>> import Sudoku.Triple (Index(..))
-- >>> digitFromIndex (First, First)
-- 0
digitFromIndex :: (Index, Index) -> Int
digitFromIndex (outer, inner) = fromEnum outer * 3 + fromEnum inner

View file

@ -1,9 +0,0 @@
003020600
900305001
001806400
008102900
700000008
006708200
002609500
800203009
005010300