diff --git a/README.md b/README.md index a623dad..63f7d02 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,108 @@ # gfn -Generate fantasy names for games and stories + +Generate fantasy names for games and stories. It uses the fine +[fantasyname module](https://github.com/s0rg/fantasyname) by +[s0rg](https://github.com/s0rg/), which implements the code created by +the [rinkworks fantasy name +generator](http://rinkworks.com/namegen/). The code itself is +[outlined here](http://rinkworks.com/namegen/instr.shtml), or take a +quick look at the [reference +guide](http://rinkworks.com/namegen/reference.shtml). + +In case the site vanishes some day, a copy of those documents is +contained here in the repository. + +# Install + +Execute + +```shell +% go build +% cp gfn $HOME/bin +``` + +# Usage + +There are a bunch of pre compiled fantasy name codes builtin, you can +get a list with: + +```shell +% gfn -l +``` + +To use one of them and limit the number of +words generated: + +```shell +% gfn JapaneseNamesDiverse -c 24 +yumufuchi afuchin keyu amorekin ekimuwo ashihewani rosa chireki +oterun ruwahi uwamine emiyumu temimon yuwa awayason fuki +emiwa nushiron achihora yomichi saniyutan kewaritsu saroru uhashi +``` + +You can also write a code yourself: + +```shell +gfn '!sVm' -c 24 +Quaoobunker Emeemoopsie Angeepookie Osousmoosh Umuisweetie Ustoesnookum Sulealover Imopookie +Skelaesnoogle Echiapookie Cereepoochie Gariwuddle Echaewookie Tiaieschmoopie Queaubooble Athesmoosh +Undousnuggy Urnuigooble Mosoesnuggy Eldoegooble Denoipoochie Mosoosmooch Shyucuddly Tiaeylovey +``` + +A short outline of the code will be printed if you add the `-h` +parameter: +```shell +This is gfn, a fantasy name generator cli. + +Usage: gfn [-vlc] + +Options: +-c --count How many names to generate +-l --list List pre-compiled shortcuts +-v --version Show program version + +pattern syntax The letters s, v, V, c, B, C, i, m, M, D, and d +represent different types of random replacements: + +s - generic syllable +v - vowel +V - vowel or vowel combination +c - consonant +B - consonant or consonant combination suitable for beginning a word +C - consonant or consonant combination suitable anywhere in a word +i - insult +m - mushy name +M - mushy name ending +D - consonant suited for a stupid person's name +d - syllable suited for a stupid person's name (begins with a vowel) +Everything else is emitted literally. + +All characters between parenthesis () are emitted literally. For +example, the pattern s(dim), emits a random generic syllable followed +by dim. + +Characters between angle brackets <> emit patterns from the table +above. Imagine the entire pattern is wrapped in one of these. + +In both types of groupings, a vertical bar | denotes a random +choice. Empty groups are allowed. For example, (foo|bar) emits either +foo or bar. The pattern emits a constant, vowel, or nothing at +all. + +An exclamation point ! means to capitalize the component that follows +it. For example, !(foo) will emit Foo and v!s will emit a lowercase +vowel followed by a capitalized syllable, like eRod. +``` + +# Report bugs + +[Please open an issue](https://github.com/TLINDEN/gfn/issues). Thanks! + +# License + +This work is licensed under the terms of the General Public Licens +version 3. + +# Author + +Copyleft (c) 2024 Thomas von Dein diff --git a/config.go b/config.go index d88e4d7..f6b3700 100644 --- a/config.go +++ b/config.go @@ -79,33 +79,35 @@ type Template struct { Tmpl string } -var Templates = []Template{ - {Name: "MiddleEarth", Tmpl: MIDDLE_EARTH}, - {Name: "JapaneseNamesConstrained", Tmpl: JAPANESE_NAMES_CONSTRAINED}, - {Name: "JapaneseNamesDiverse", Tmpl: JAPANESE_NAMES_DIVERSE}, - {Name: "ChineseNames", Tmpl: CHINESE_NAMES}, - {Name: "GreekNames", Tmpl: GREEK_NAMES}, - {Name: "HawaiianNames1", Tmpl: HAWAIIAN_NAMES_1}, - {Name: "HawaiianNames2", Tmpl: HAWAIIAN_NAMES_2}, - {Name: "OldLatinPlaceNames", Tmpl: OLD_LATIN_PLACE_NAMES}, - {Name: "DragonsPern", Tmpl: DRAGONS_PERN}, - {Name: "DragonRiders", Tmpl: DRAGON_RIDERS}, - {Name: "Pokemon", Tmpl: POKEMON}, - {Name: "FantasyR", Tmpl: FANTASY_VOWELS_R}, - {Name: "FantasySA", Tmpl: FANTASY_S_A}, - {Name: "FantasyHL", Tmpl: FANTASY_H_L}, - {Name: "FantasyNL", Tmpl: FANTASY_N_L}, - {Name: "FantasyKN", Tmpl: FANTASY_K_N}, - {Name: "FantasyJGZ", Tmpl: FANTASY_J_G_Z}, - {Name: "FantasyKJY", Tmpl: FANTASY_K_J_Y}, - {Name: "FantasySE", Tmpl: FANTASY_S_E}, - {Name: "Funny", Tmpl: "sdD"}, - {Name: "Idiots", Tmpl: "ii"}, +var Templates = map[string]string{ + "MiddleEarth": MIDDLE_EARTH, + "JapaneseNamesConstrained": JAPANESE_NAMES_CONSTRAINED, + "JapaneseNamesDiverse": JAPANESE_NAMES_DIVERSE, + "ChineseNames": CHINESE_NAMES, + "GreekNames": GREEK_NAMES, + "HawaiianNames1": HAWAIIAN_NAMES_1, + "HawaiianNames2": HAWAIIAN_NAMES_2, + "OldLatinPlaceNames": OLD_LATIN_PLACE_NAMES, + "DragonsPern": DRAGONS_PERN, + "DragonRiders": DRAGON_RIDERS, + "Pokemon": POKEMON, + "FantasyR": FANTASY_VOWELS_R, + "FantasySA": FANTASY_S_A, + "FantasyHL": FANTASY_H_L, + "FantasyNL": FANTASY_N_L, + "FantasyKN": FANTASY_K_N, + "FantasyJGZ": FANTASY_J_G_Z, + "FantasyKJY": FANTASY_K_J_Y, + "FantasySE": FANTASY_S_E, + "Funny": "sdD", + "Idiots": "ii", } const ( - VERSION string = "0.0.1" - Usage string = `This is gfn, a fantasy name generator cli. + VERSION string = "0.0.1" + DefaultCount int = 160 // number of words to generate if -c is omitted + Columns int = 8 // number of columns to print + Usage string = `This is gfn, a fantasy name generator cli. Usage: gfn [-vlc] @@ -151,6 +153,7 @@ type Config struct { Showversion bool `koanf:"version"` // -v Listshortcuts bool `koanf:"list"` // -l Code string // arg + Count int `koanf:"count"` // -c } func InitConfig(output io.Writer) (*Config, error) { @@ -158,7 +161,7 @@ func InitConfig(output io.Writer) (*Config, error) { // Load default values using the confmap provider. if err := kloader.Load(confmap.Provider(map[string]interface{}{ - "count": 10, + "count": DefaultCount, }, "."), nil); err != nil { return nil, fmt.Errorf("failed to load default values into koanf: %w", err) } @@ -173,6 +176,7 @@ func InitConfig(output io.Writer) (*Config, error) { // parse commandline flags flagset.BoolP("list", "l", false, "show list of precompiled codes") flagset.BoolP("version", "V", false, "show program version") + flagset.IntP("count", "c", 1, "how many names to generate") if err := flagset.Parse(os.Args[1:]); err != nil { return nil, fmt.Errorf("failed to parse program arguments: %w", err) diff --git a/generate.go b/generate.go index d065e10..38c34a5 100644 --- a/generate.go +++ b/generate.go @@ -14,12 +14,12 @@ func Generate(count int, code string) ([]string, error) { gen, err := fn.Compile(code, fn.Collapse(true)) if err != nil { - return nil, fmt.Errorf("Could not compile FN code:", err) + return nil, fmt.Errorf("could not compile FN code: %w", err) } for i := 0; i < count; i++ { name := gen.String() - if !exists(reg, name) { + if !Exists(reg, name) { reg[name] = 1 } } diff --git a/gfn b/gfn index 3b0f0a1..da5f034 100755 Binary files a/gfn and b/gfn differ diff --git a/go.mod b/go.mod index 176cce4..d04aa73 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require github.com/s0rg/fantasyname v1.3.4 require ( + github.com/alecthomas/repr v0.4.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect diff --git a/go.sum b/go.sum index a1d4687..a065457 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= diff --git a/main.go b/main.go index bd810fe..2f34eb3 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "os" ) @@ -24,42 +23,26 @@ func Main(output io.Writer) int { } if conf.Listshortcuts { - for _, name := range Templates { - fmt.Println(name) - } + ListTemplates(output) return 0 } - // FIXME: this is a slice of Template{}, turn it into a map - if Contains(Templates, conf.Code) { + if len(conf.Code) == 0 { + fmt.Fprintln(output, Usage) + } + + if Exists(Templates, conf.Code) { conf.Code = Templates[conf.Code] } names, err := Generate(conf.Count, conf.Code) + if err != nil { + return Die(err) + } + + if err = PrintColumns(names, output); err != nil { + return Die(err) + } return 0 } - -func exists[K comparable, V any](m map[K]V, v K) bool { - if _, ok := m[v]; ok { - return true - } - return false -} - -func Die(err error) int { - log.Fatal("Error", err.Error()) - - return 1 -} - -// find an item in a list, generic variant -func Contains[E comparable](s []E, v E) bool { - for _, vs := range s { - if v == vs { - return true - } - } - - return false -} diff --git a/printer.go b/printer.go new file mode 100644 index 0000000..e8dea56 --- /dev/null +++ b/printer.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "io" + "sort" +) + +func ListTemplates(output io.Writer) { + names := []string{} + + for name := range Templates { + names = append(names, name) + } + + sort.Strings(names) + + for _, name := range names { + fmt.Fprintln(output, name) + } +} + +func PrintColumns(names []string, output io.Writer) error { + count := len(names) + + // no need for the hassle to calculate columns + if count <= Columns { + for _, name := range names { + fmt.Fprintln(output, name) + } + + return nil + } + + // get a transposed list of columns + padlist, max := Getcolumns(names, Columns) + + // make sure there's enough spacing between the columns + format := fmt.Sprintf("%%-%ds", max+1) + + for _, row := range padlist { + for _, word := range row { + fmt.Fprintf(output, format, word) + } + fmt.Fprintln(output) + } + + return nil +} + +func Getcolumns(names []string, columns int) ([][]string, int) { + words := len(names) + max := 0 + + // we'll have a list of $columns columns + padlist := make([][]string, columns) + + // initialize'em + for col := 0; col < columns; col++ { + padlist[col] = []string{} + } + + // fill from input + for idx := 0; idx < words; idx += columns { + for col := 0; col < columns; col++ { + if idx+col >= words { + padlist[col] = append(padlist[col], "") + } else { + padlist[col] = append(padlist[col], names[idx+col]) + length := len(names[idx+col]) + if length > max { + max = length + } + } + + } + } + + // turn columns to rows + return Transpose(padlist), max +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..4397550 --- /dev/null +++ b/util.go @@ -0,0 +1,48 @@ +package main + +import "log" + +func Exists[K comparable, V any](m map[K]V, v K) bool { + if _, ok := m[v]; ok { + return true + } + return false +} + +func Die(err error) int { + log.Fatal("Error: ", err.Error()) + + return 1 +} + +// find an item in a list, generic variant +func Contains[E comparable](s []E, v E) bool { + for _, vs := range s { + if v == vs { + return true + } + } + + return false +} + +// Transpose a matrix, x=>y, y=>x +// via https://gist.github.com/tanaikech/5cb41424ff8be0fdf19e78d375b6adb8 +func Transpose(slice [][]string) [][]string { + xl := len(slice[0]) + yl := len(slice) + + result := make([][]string, xl) + + for i := range result { + result[i] = make([]string, yl) + } + + for i := 0; i < xl; i++ { + for j := 0; j < yl; j++ { + result[i][j] = slice[j][i] + } + } + + return result +}