hpr2938 :: Naming pets in space game
How to use markov chains to generate names
Hosted by Tuula on Wednesday, 2019-11-06 is flagged as Clean and is released under a CC-BY-SA license.
haskell, markov chains.
(Be the first).
The show is available on the Internet Archive at: https://archive.org/details/hpr2938
Listen in ogg,
spx,
or mp3 format. Play now:
Duration: 00:20:36
Haskell.
A series looking into the Haskell (programming language)
Intro
In the two previous episodes we built a weighted list and used that to build markov chains. This time we’re going to use them to generate some names based on examples. I’m skipping over a lot of uninteresting code in this episode, concentrating only the parts that deal with names.
Idea
Person in game might hear scurrying sounds inside walls of their quarters. Then they have option of getting a cat, taming a rat or letting someone else deal with the problem. Depending on their choice, they might end up with a cat or a rat, that of course needs a name. Game offers 3 different options of names that haven’t been used before and person can always opt for completely random one.
Config
While we’re not going to dig very deep into making configurations for markov chains, we can have look at the overall process.
We have list of names to serve as examples and three functions, which implementation I won’t delve into:
start
for adding starting elementlinks
for recording link between two elementsend
adds ending element
addName
function is used to add single name into config:
addName :: Int -> Text -> Config Text -> Config Text
addName n s config =
links pairs $
end elements $
start elements config
where
elements = chunksOf n s
pairs = zip elements (safeTail elements)
First s
(name) is split into strings of length n
. These elements
are then combined into pairs, where consecutive elements form a pair. Final step is to add start and ending elements into config, followed by links between elements of pairs.
We can then fold a list of examples into config:
nameConfig :: [Text] -> Int -> Config Text
nameConfig xs n =
foldr (addName n) emptyConfig xs
This starts with emptyConfig
and calls addName
repeatedly until all elements of list containing examples have been processed.
Implementation
Now that we have configuration, we can start generating names. As usual, I like to keep things specific and generate PetName
instead of just Text
. I happened to have list of ancient greek names at hand, so I used that. Later on we’ll have to add more cultures, like Romans, Parthians, Persians, Germans, Phoenicians and so on.
General implementation of generating infinite list of strings of specific kind is shown below:
names :: (RandomGen g, Eq b) => (Text -> b) -> Config Text -> g -> [b]
names t config g =
nub $ (t . toTitle . concat) <$> chains config g
It’s easier to read if you start from right. chains config g
generates infinite list of markov chains with given configuration. Next we create a new function (t . toTitle . concat)
, which uses concat
to combine list of Text
into single Text
, toTitle
to capitalize is correctly and t
to transform it to something (PetName
in our case). <$>
is then used to apply this function to each element of our infinite list. Finally nub
is used to remove duplicate entries.
With names
we can then define petNames
:
petNames :: (RandomGen g) => g -> [PetName]
petNames =
names MkPetName greekNameConfig
MkPetName
is value constructor that turns Text
into PetName
(this is t
used by names
function).
Pets
Pets are currently very much work in progress. They have few attributes and there can be two different kinds of pets:
Pet json
name PetName
type PetType
dateOfBirth StarDate
dateOfDeath StarDate Maybe
ownerId PersonId
deriving Show Read Eq
data PetType
= Cat
| Rat
deriving (Show, Read, Eq, Ord, Enum, Bounded)
The actual beef is namingPetEvent
function. When applied with Entity Person
, Entity Pet
and StarDate
, it will create News
that can be saved into database and later on showed to player. While the code is shown below, I’m not going to go over it line by line:
namingPetEvent :: (PersistQueryRead backend, MonadIO m,
BaseBackend backend ~ SqlBackend) =>
Entity Person -> Entity Pet -> StarDate -> ReaderT backend m News
namingPetEvent personE petE date = do
pets <- selectList [ PetOwnerId ==. (entityKey personE)
, PetDateOfDeath ==. Nothing
] []
let names = (petName . entityVal) <$> pets
g <- liftIO getStdGen
let availableNames = take 3 $ filter (\x -> not (x `elem` names)) $ petNames g
let content = NamingPet (NamingPetEvent { namingPetEventPersonId = entityKey personE
, namingPetEventPetId = entityKey petE
, namingPetEventPetType = (petType . entityVal) petE
, namingPetEventDate = date
, namingPetNameOptions = availableNames
})
[] Nothing
return $ mkPersonalSpecialNews date (entityKey personE) content
General idea is to use selectList
to load living pets of given person and then extract their names. With random generator g
, we create a infinite list of PetName
s, remove already used names from it and take 3 first ones. These names are then used to create NamingPetEvent
.
In closing
Names are probably one of the most common applications of markov chains in games. Same technique can be used to generate nonsense books and articles that look realistic on a glance.
Questions, comments and feedback is welcomed, best way to reach is email or in fediverse where I’m Tuula@mastodon.social
. Or even better, record your own episode for Hacker Public Radio.
ad astra!