Archive unfinished old changes

archive
Bram van den Heuvel 2026-06-28 15:57:47 +02:00
parent b61f0dfbbb
commit eabba2b7fe
11 changed files with 884 additions and 13 deletions

129
src/Decoder.elm Normal file
View File

@ -0,0 +1,129 @@
module Decoder exposing (..)
import Dict
import FastDict
import Json.Decode as D
import Json.Encode as E
type Information a
= Information
{ decoder : D.Decoder a
, encoder : a -> E.Value
, keys : List String
, default : a
}
bool : List String -> Bool -> Information Bool
bool keys default =
Information
{ decoder = D.bool
, encoder = E.bool
, keys = keys
, default = default
}
dict : List String -> Dict.Dict comparable value -> (comparable -> String) -> (String -> comparable) -> (value -> E.Value) -> D.Decoder value -> Information (Dict.Dict comparable value)
dict keys default fromKey toKey fromValue valueDecoder =
Information
{ decoder =
D.keyValuePairs valueDecoder
|> D.map (List.map (Tuple.mapFirst toKey))
|> D.map Dict.fromList
, encoder = E.dict fromKey fromValue
, keys = keys
, default = default
}
fastdict : List String -> FastDict.Dict comparable value -> (comparable -> String) -> (String -> comparable) -> (value -> E.Value) -> D.Decoder value -> Information (FastDict.Dict comparable value)
fastdict keys default fromKey toKey fromValue valueDecoder =
Information
{ decoder =
D.keyValuePairs valueDecoder
|> D.map (List.map (Tuple.mapFirst toKey))
|> D.map FastDict.fromList
, encoder =
FastDict.toList
>> List.map (Tuple.mapBoth fromKey fromValue)
>> E.object
, keys = keys
, default = default
}
int : List String -> Int -> Information Int
int keys default =
Information
{ decoder = D.int
, encoder = E.int
, keys = keys
, default = default
}
list : List String -> List a -> (a -> E.Value) -> D.Decoder a -> Information (List a)
list keys default fromValue valueDecoder =
Information
{ decoder = D.list valueDecoder
, encoder = E.list fromValue
, keys = keys
, default = default
}
{-| The website allows users to overwrite the pre-generated values.
Hence, if a value already exists, it is returned. If it does not, it will be
generated.
-}
lookUp : E.Value -> Information a -> a
lookUp value (Information { decoder, keys, default }) =
case D.decodeValue (D.at keys decoder) value of
Ok v ->
v
Err _ ->
default
overwrite : a -> Information a -> E.Value -> E.Value
overwrite newValue (Information info) obj =
case info.keys of
[] ->
info.encoder newValue
head :: tail ->
case D.decodeValue (D.keyValuePairs D.value) obj of
Ok o ->
o
|> FastDict.fromList
|> FastDict.update head
(\mv ->
overwrite newValue
(Information
{ encoder = info.encoder
, decoder = info.decoder
, keys = tail
, default = info.default
}
)
(mv |> Maybe.withDefault (E.dict identity identity Dict.empty))
|> Just
)
|> FastDict.toCoreDict
|> E.dict identity identity
Err _ ->
obj
string : List String -> String -> Information String
string keys default =
Information
{ encoder = E.string
, decoder = D.string
, keys = keys
, default = default
}

341
src/Defaults.elm Normal file
View File

@ -0,0 +1,341 @@
module Defaults exposing (..)
{-| The Defaults module exposes config values. These values can be overwritten
but they are usually set by default.
-}
import FastDict as Dict
commonerOccupations : Dict.Dict String Int
commonerOccupations =
Dict.fromList
[ ( "Baker", 25 )
, ( "Shoemaker", 37 )
, ( "Furrier", 62 )
, ( "Household helper", 62 )
, ( "Tailor", 62 )
, ( "Barber", 87 )
, ( "Unlicensed Doctor", 87 )
, ( "Jeweler", 100 )
, ( "Old cloth seller", 100 )
, ( "Pastry baker", 125 )
, ( "Mason", 125 )
, ( "Carpenter", 137 )
, ( "Ostler", 150 )
, ( "Porter", 150 )
, ( "Weaver", 150 )
, ( "Chandler", 175 )
, ( "Mercer", 175 )
, ( "Cooper", 175 )
, ( "Cobbler", 180 )
, ( "Watercarrier", 212 )
, ( "Scabbardmaker", 212 )
, ( "Wine-seller", 225 )
, ( "Hatmaker", 237 )
, ( "Saddler", 250 )
, ( "Chicken butcher", 250 )
, ( "Pursemaker", 275 )
, ( "Soapmaker", 275 )
, ( "Goldsmith", 300 )
, ( "Butcher", 300 )
, ( "Farrier", 300 )
, ( "Fishmonger", 300 )
, ( "Beer seller", 350 )
, ( "Buckle Maker", 350 )
, ( "Plasterer", 350 )
, ( "Spice Merchant", 350 )
, ( "Hedge-Mage", 350 )
, ( "Painter", 375 )
, ( "Hunter", 400 )
, ( "Licensed doctor", 425 )
, ( "Roofer", 450 )
, ( "Locksmith", 475 )
, ( "Bather", 475 )
, ( "Ropemaker", 475 )
, ( "Inn", 500 )
, ( "Tanner", 500 )
, ( "Copyist", 500 )
, ( "Sculptor", 500 )
, ( "Rugmaker", 500 )
, ( "Dyer", 500 )
, ( "Harness-Maker", 500 )
, ( "Bleacher", 525 )
, ( "Hay Merchant", 575 )
, ( "Cutler", 575 )
, ( "Glovemaker", 600 )
, ( "Woodcarver", 600 )
, ( "Woodseller", 600 )
, ( "Herald", 600 )
, ( "Lorimer", 600 )
, ( "Penmaker", 650 )
, ( "Horse Trainer", 700 )
, ( "Bookbinder", 750 )
, ( "Stationer (Ink/Pen Maker)", 700 )
, ( "Spurrer", 800 )
, ( "Dung Carter", 800 )
, ( "Costermonger", 800 )
, ( "Fortune Teller", 900 )
, ( "Chirurgeon", 900 )
, ( "Silversmith", 920 )
, ( "Illuminator", 975 )
, ( "Coppersmith", 1100 )
, ( "Master of Hounds", 1200 )
, ( "Leech Collector", 1300 )
, ( "Tinsmith", 1400 )
, ( "Castellan", 1500 )
, ( "Bookseller", 1575 )
, ( "Composer", 1600 )
, ( "Limeburner", 1600 )
, ( "Ale-conner", 1700 )
, ( "Amanuensis", 1800 )
, ( "Gong Farmer", 1800 )
, ( "Coiner", 1900 )
, ( "Philosopher", 2000 )
, ( "Noble", 3000 )
]
type alias RaceData =
{ density : Int
, maturesAt : Int
, livesUntil : Int
, becomesParentUntil : Int
}
races : Dict.Dict String RaceData
races =
Dict.fromList
[ ( "Human"
, { density = 800000
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Half-elf"
, { density = 100000
, maturesAt = 20
, livesUntil = 200
, becomesParentUntil = 160
}
)
, ( "Dwarf"
, { density = 60000
, maturesAt = 50
, livesUntil = 350
, becomesParentUntil = 150
}
)
, ( "Elf"
, { density = 35000
, maturesAt = 100
, livesUntil = 750
, becomesParentUntil = 500
}
)
, ( "Half-orc"
, { density = 25000
, maturesAt = 14
, livesUntil = 75
, becomesParentUntil = 30
}
)
, ( "Halfling"
, { density = 20000
, maturesAt = 20
, livesUntil = 65
, becomesParentUntil = 40
}
)
, ( "Dragonborn"
, { density = 10000
, maturesAt = 15
, livesUntil = 80
, becomesParentUntil = 40
}
)
, ( "Tiefling"
, { density = 10000
, maturesAt = 20
, livesUntil = 110
, becomesParentUntil = 45
}
)
, ( "Gnome"
, { density = 10000
, maturesAt = 40
, livesUntil = 500
, becomesParentUntil = 200
}
)
, ( "Aasimar"
, { density = 9000
, maturesAt = 20
, livesUntil = 160
, becomesParentUntil = 60
}
)
, ( "Orc"
, { density = 8000
, maturesAt = 12
, livesUntil = 50
, becomesParentUntil = 30
}
)
, ( "Goliath"
, { density = 7500
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Tabaxi"
, { density = 5000
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Hobgoblin"
, { density = 2500
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Lizardfolk"
, { density = 1500
, maturesAt = 14
, livesUntil = 60
, becomesParentUntil = 30
}
)
, ( "Goblin"
, { density = 1000
, maturesAt = 8
, livesUntil = 60
, becomesParentUntil = 30
}
)
, ( "Yuan-Ti Pureblood"
, { density = 1000
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Bugbear"
, { density = 1000
, maturesAt = 16
, livesUntil = 80
, becomesParentUntil = 35
}
)
, ( "Tortle"
, { density = 1000
, maturesAt = 15
, livesUntil = 50
, becomesParentUntil = 30
}
)
, ( "Warforged"
, { density = 1000
, maturesAt = 0
, livesUntil = 5000
, becomesParentUntil = 5000
}
)
, ( "Kobold"
, { density = 800
, maturesAt = 6
, livesUntil = 120
, becomesParentUntil = 25
}
)
, ( "Shifter"
, { density = 750
, maturesAt = 10
, livesUntil = 70
, becomesParentUntil = 30
}
)
, ( "Triton"
, { density = 400
, maturesAt = 15
, livesUntil = 200
, becomesParentUntil = 110
}
)
, ( "Firbolg"
, { density = 300
, maturesAt = 30
, livesUntil = 500
, becomesParentUntil = 300
}
)
, ( "Kalashtar"
, { density = 250
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Aarakocra"
, { density = 250
, maturesAt = 3
, livesUntil = 30
, becomesParentUntil = 20
}
)
, ( "Kenku"
, { density = 200
, maturesAt = 12
, livesUntil = 60
, becomesParentUntil = 36
}
)
, ( "Changeling"
, { density = 50
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Gith"
, { density = 75
, maturesAt = 20
, livesUntil = 90
, becomesParentUntil = 40
}
)
, ( "Air Genasi"
, { density = 10
, maturesAt = 20
, livesUntil = 120
, becomesParentUntil = 50
}
)
, ( "Earth Genasi"
, { density = 10
, maturesAt = 20
, livesUntil = 120
, becomesParentUntil = 50
}
)
, ( "Fire Genasi"
, { density = 10
, maturesAt = 20
, livesUntil = 120
, becomesParentUntil = 50
}
)
, ( "Water Genasi"
, { density = 10
, maturesAt = 20
, livesUntil = 120
, becomesParentUntil = 50
}
)
]

View File

@ -1,4 +1,4 @@
module Generatable exposing (..) module Generatable exposing (Generatable, content, fromGenerator, generateAndReturnFromSeed, generateFromSeed, generate, map, regenerate, setContent, removeContent, updateGenerator)
{-| The Generatable type contains information that can be stored, modified, {-| The Generatable type contains information that can be stored, modified,
generated or regenerated. generated or regenerated.
-} -}
@ -26,6 +26,25 @@ fromGenerator : Random.Generator a -> Generatable a
fromGenerator gr = fromGenerator gr =
ToBeGenerated gr ToBeGenerated gr
{-| Get the generator with which new values can be generated.
-}
generator : Generatable a -> Random.Generator a
generator gen =
case gen of
ToBeGenerated gr ->
gr
Generated _ gr ->
gr
{-| Create a new Generatable from a given seed, and return the value.
-}
generateAndReturnFromSeed : Random.Seed -> Generatable a -> ( Generatable a, a )
generateAndReturnFromSeed seed gen =
case Random.step (generator gen) seed of
( new, _ ) ->
( Generated new (generator gen), new )
{-| Create a new generatable from a given seed. {-| Create a new generatable from a given seed.
-} -}
generateFromSeed : Random.Seed -> Generatable a -> Generatable a generateFromSeed : Random.Seed -> Generatable a -> Generatable a
@ -65,23 +84,13 @@ already generated.
-} -}
regenerate : (a -> msg) -> Generatable a -> Cmd msg regenerate : (a -> msg) -> Generatable a -> Cmd msg
regenerate toMsg gen = regenerate toMsg gen =
case gen of Random.generate toMsg (generator gen)
ToBeGenerated gr ->
Random.generate toMsg gr
Generated _ gr ->
Random.generate toMsg gr
{-| Override the content with freshly generated content. {-| Override the content with freshly generated content.
-} -}
setContent : a -> Generatable a -> Generatable a setContent : a -> Generatable a -> Generatable a
setContent x gen = setContent x gen =
case gen of Generated x (generator gen)
ToBeGenerated gr ->
Generated x gr
Generated _ gr ->
Generated x gr
{-| Remove any previously generated content from the generatable. {-| Remove any previously generated content from the generatable.
-} -}

121
src/Generator.elm Normal file
View File

@ -0,0 +1,121 @@
module Generator exposing (..)
{-| The Generator module helps generate values based on a given hash.
-}
import Pick exposing (Generator)
commonerOccupation : Generator (Maybe String)
commonerOccupation =
Pick.inverseWeighted
[ ( "Baker", 25 )
, ( "Shoemaker", 37 )
, ( "Furrier", 62 )
, ( "Household helper", 62 )
, ( "Tailor", 62 )
, ( "Barber", 87 )
, ( "Unlicensed Doctor", 87 )
, ( "Jeweler", 100 )
, ( "Old cloth seller", 100 )
, ( "Pastry baker", 125 )
, ( "Mason", 125 )
, ( "Carpenter", 137 )
, ( "Ostler", 150 )
, ( "Porter", 150 )
, ( "Weaver", 150 )
, ( "Chandler", 175 )
, ( "Mercer", 175 )
, ( "Cooper", 175 )
, ( "Cobbler", 180 )
, ( "Watercarrier", 212 )
, ( "Scabbardmaker", 212 )
, ( "Wine-seller", 225 )
, ( "Hatmaker", 237 )
, ( "Saddler", 250 )
, ( "Chicken butcher", 250 )
, ( "Pursemaker", 275 )
, ( "Soapmaker", 275 )
, ( "Goldsmith", 300 )
, ( "Butcher", 300 )
, ( "Farrier", 300 )
, ( "Fishmonger", 300 )
, ( "Beer seller", 350 )
, ( "Buckle Maker", 350 )
, ( "Plasterer", 350 )
, ( "Spice Merchant", 350 )
, ( "Hedge-Mage", 350 )
, ( "Painter", 375 )
, ( "Hunter", 400 )
, ( "Licensed doctor", 425 )
, ( "Roofer", 450 )
, ( "Locksmith", 475 )
, ( "Bather", 475 )
, ( "Ropemaker", 475 )
, ( "Inn", 500 )
, ( "Tanner", 500 )
, ( "Copyist", 500 )
, ( "Sculptor", 500 )
, ( "Rugmaker", 500 )
, ( "Dyer", 500 )
, ( "Harness-Maker", 500 )
, ( "Bleacher", 525 )
, ( "Hay Merchant", 575 )
, ( "Cutler", 575 )
, ( "Glovemaker", 600 )
, ( "Woodcarver", 600 )
, ( "Woodseller", 600 )
, ( "Herald", 600 )
, ( "Lorimer", 600 )
, ( "Penmaker", 650 )
, ( "Horse Trainer", 700 )
, ( "Bookbinder", 750 )
, ( "Stationer (Ink/Pen Maker)", 700 )
, ( "Spurrer", 800 )
, ( "Dung Carter", 800 )
, ( "Costermonger", 800 )
, ( "Fortune Teller", 900 )
, ( "Chirurgeon", 900 )
, ( "Silversmith", 920 )
, ( "Illuminator", 975 )
, ( "Coppersmith", 1100 )
, ( "Master of Hounds", 1200 )
, ( "Leech Collector", 1300 )
, ( "Tinsmith", 1400 )
, ( "Castellan", 1500 )
, ( "Bookseller", 1575 )
, ( "Composer", 1600 )
, ( "Limeburner", 1600 )
, ( "Ale-conner", 1700 )
, ( "Amanuensis", 1800 )
, ( "Gong Farmer", 1800 )
, ( "Coiner", 1900 )
, ( "Philosopher", 2000 )
, ( "Noble", 3000 )
]
townName : Generator String
townName =
Pick.map2 (++)
(Pick.fromList
"Auden"
[ "Caia"
, "Ex Caia"
, "Gold"
, "Hirey"
, "Ideato"
, "Jairo"
, "Kalares"
, "Oosterdorp"
]
)
(Pick.fromList
""
[ ""
, ""
, " City"
, " Town"
, "'s Creek"
]
)

4
src/Objects.elm Normal file
View File

@ -0,0 +1,4 @@
module Objects exposing (..)
{-| This module exposes all types that can be generated.
-}

9
src/Objects/District.elm Normal file
View File

@ -0,0 +1,9 @@
module Objects.District exposing (..)
{-| Districts are sections of a town. These are mostly segregated to keep the
design of the procedurally generated town simple.
-}
{-| A district is a part of town. Usually, it is a neighbourhood.
-}
type District
= District {}

33
src/Objects/NPC.elm Normal file
View File

@ -0,0 +1,33 @@
module Objects.NPC exposing (..)
{-| The NPC is the character that is most ordinary.
-}
import Hash exposing (Hash)
import Objects.TownName as Town
type NPC
= NPC { town : Town.TownName, id : Int }
fromTown : { town : Town.TownName, id : Int } -> NPC
fromTown =
NPC
toTown : NPC -> Town.TownName
toTown (NPC { town }) =
town
toId : NPC -> Int
toId (NPC { id }) =
id
toHash : NPC -> Hash
toHash (NPC { town, id }) =
Hash.fromString "NPC"
|> Hash.dependent (Town.toHash town)
|> Hash.dependent (Hash.fromInt id)

105
src/Objects/Town.elm Normal file
View File

@ -0,0 +1,105 @@
module Objects.Town exposing (..)
import Array exposing (Array)
import Hash
import Random
import Generatable exposing (Generatable)
import Objects.District as District exposing (District)
{-| The Msg is the update telling what adjustments are made to the model.
-}
type Msg
= GenerateSize
{-| The Town is the comprehensive model that contains all the information about
a town.
-}
type Town
= Town
{ districts : Generatable (Array (Generatable District))
, name : String
, size : TownSize
}
type TownSize
= Tiny
| Small
| Medium
| Average
| Big
| Huge
-- | Humongous
-- | Massive
-- | Colossal
{-| Determine the number of districts in the town based on its size.
-}
districtNumberGenerator : TownSize -> Random.Generator Int
districtNumberGenerator ts =
case ts of
Tiny ->
Random.int 1 2
Small ->
Random.int 5 8
Medium ->
Random.int 15 20
Average ->
Random.int 35 45
Big ->
Random.int 90 140
Huge ->
Random.int 250 470
{-| Init a new town.
-}
init : { name : String, size : TownSize } -> Town
init data =
Town
{ districts =
data.size
|> districtNumberGenerator
|> Random.map (always never)
|> Generatable.fromGenerator
, name = data.name
, size = data.size
}
{-| Get a town's name.
-}
name : Town -> String
name (Town town) = town.name
{-| Get a town's size.
-}
size : Town -> Maybe Int
size (Town town) =
Generatable.content town.size
{-| Get a town's size. If it isn't generated yet, generate it.
-}
sizeGenerate : Town -> ( Town, Int )
sizeGenerate ((Town t) as town) =
case Generatable.content t.size of
Just s ->
( town, s )
Nothing ->
case Generatable.generateAndReturnFromSeed (sizeSeed town) t.size of
( newSize, s ) ->
( Town { t | size = newSize }, s )
{-| Generate a seed for the town's size.
-}
sizeSeed : Town -> Random.Seed
sizeSeed (Town town) =
Hash.fromString town.name
|> Hash.dependent (Hash.fromString "size")
|> Hash.toString
|> String.toInt
|> Maybe.withDefault 0
|> Random.initialSeed

28
src/Objects/TownName.elm Normal file
View File

@ -0,0 +1,28 @@
module Objects.TownName exposing (TownName, fromString, toHash, toString)
import Hash exposing (Hash)
type TownName
= TownName String
{-| Decode a town name from a string.
-}
fromString : String -> TownName
fromString =
TownName
{-| Encode a town name into a string.
-}
toString : TownName -> String
toString (TownName name) =
name
{-| Hash a town name.
-}
toHash : TownName -> Hash
toHash (TownName name) =
Hash.dependent (Hash.fromString "town-name") (Hash.fromString name)

68
src/Pick.elm Normal file
View File

@ -0,0 +1,68 @@
module Pick exposing (..)
{-| The pick module helps pick random values based on a given hash.
-}
import Hash exposing (Hash)
import Random
type alias Generator a =
Random.Generator a
generate : Hash -> Generator a -> a
generate hash picker =
let
seed : Random.Seed
seed =
hash
|> Hash.toString
|> String.toInt
|> Maybe.withDefault 0
|> Random.initialSeed
in
Random.step
fromList : a -> List a -> Generator a
fromList head tail =
Random.uniform head tail
inverseWeighted : List ( a, Int ) -> Generator (Maybe a)
inverseWeighted items =
let
chanceSum : Float
chanceSum =
items
|> List.map Tuple.second
|> List.map toFloat
|> List.map ((/) 1)
|> List.sum
in
items
|> List.map (Tuple.mapSecond (\i -> 1 / toFloat i))
|> List.map (\( a, b ) -> ( b, a ))
|> List.map (Tuple.mapSecond Just)
|> weighted ( max 0 (1 - chanceSum), Nothing )
map : (a -> b) -> Generator a -> Generator b
map =
Random.map
map2 : (a -> b -> c) -> Generator a -> Generator b -> Generator c
map2 =
Random.map2
value : a -> Generator a
value =
Random.constant
weighted : ( Float, a ) -> List ( Float, a ) -> Generator a
weighted =
Random.weighted

24
src/Seed/Town.elm Normal file
View File

@ -0,0 +1,24 @@
module Seed.Town exposing (TownSeed, fromName, toHash, toSeed)
{-| The Town Seed helps generate different towns.
-}
import Hash
import Random
type TownSeed = TownSeed String
fromName : String -> TownSeed
fromName name = TownSeed name
toHash : TownSeed -> Hash.Hash
toHash (TownSeed name) =
Hash.fromString name
toSeed : TownSeed -> Random.Seed
toSeed seed =
seed
|> toHash
|> Hash.toString
|> String.toInt
|> Maybe.withDefault 0
|> Random.initialSeed