Compare commits

...

4 Commits

8 changed files with 1273 additions and 49 deletions

File diff suppressed because one or more lines are too long

1025
horse/skill.kk Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,15 @@ import std/core-extras
// Character identity. // Character identity.
pub type character pub type character
{{- range $uma := $.Characters }} {{- range $uma := $.Characters }}
{{ kkenum $uma.Name }} {{ kkenum $uma.Name }}
{{- end }} {{- end }}
// The list of all characters in order by ID, for easy iterating. // The list of all characters in order by ID, for easy iterating.
pub val character/all = [ pub val character/all = [
{{- range $uma := $.Characters }} {{- range $uma := $.Characters }}
{{ kkenum $uma.Name }}, {{ kkenum $uma.Name }},
{{- end }} {{- end }}
] ]
// Get the character for a character ID. // Get the character for a character ID.
@@ -29,7 +29,7 @@ pub fun character/from-id(id: int): maybe<character>
_ -> Nothing _ -> Nothing
// Get the ID for a character. // Get the ID for a character.
pub fun character/id(c: character): int pub fun character/character-id(c: character): int
match c match c
{{- range $uma := $.Characters }} {{- range $uma := $.Characters }}
{{ kkenum $uma.Name }} -> {{ $uma.ID }} {{ kkenum $uma.Name }} -> {{ $uma.ID }}
@@ -61,7 +61,7 @@ pub fun character/(==)(a: character, b: character): bool
{{- end }} {{- end }}
_ -> False _ -> False
inline fip fun character/index(^c: character): int fip fun character/index(^c: character): int
match c match c
{{- range $uma := $.Characters }} {{- range $uma := $.Characters }}
{{ kkenum $uma.Name }} -> {{ $uma.Index }} {{ kkenum $uma.Name }} -> {{ $uma.Index }}
@@ -70,8 +70,20 @@ inline fip fun character/index(^c: character): int
// Create the table of all pair affinities. // Create the table of all pair affinities.
// The affinity is the value at a.index*count + b.index. // The affinity is the value at a.index*count + b.index.
extern global/create-pair-table(): vector<int> extern global/create-pair-table(): vector<int>
c inline "kk_intx_t arr[] = { {{- range $a := $.Characters }}{{ range $b := $.Characters }}{{ index $.PairMaps $a.ID $b.ID }},{{ end }}{{ end -}} };\nkk_vector_from_cintarray(arr, (kk_ssize_t){{ $.Count }}*(kk_ssize_t){{ $.Count }}, kk_context())" c inline "kk_intx_t arr[] = {
js inline "[ {{- range $a := $.Characters }}{{ range $b := $.Characters }}{{ index $.PairMaps $a.ID $b.ID }},{{ end }}{{ end -}} ]" {{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- index $.PairMaps $a.ID $b.ID }},
{{- end }}
{{- end -}}
};\nkk_vector_from_cintarray(arr, (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }}, kk_context())"
js inline "[
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- index $.PairMaps $a.ID $b.ID }},
{{- end }}
{{- end -}}
]"
val global/pair-table = global/create-pair-table() val global/pair-table = global/create-pair-table()
// Base affinity between a pair using the global ruleset. // Base affinity between a pair using the global ruleset.
@@ -81,8 +93,24 @@ pub fun global/pair-affinity(a: character, b: character): int
// Create the table of all trio affinities. // Create the table of all trio affinities.
// The affinity is the value at a.index*count*count + b.index*count + c.index. // The affinity is the value at a.index*count*count + b.index*count + c.index.
extern global/create-trio-table(): vector<int> extern global/create-trio-table(): vector<int>
c inline "kk_intx_t arr[] = { {{- range $a := $.Characters }}{{ range $b := $.Characters }}{{ range $c := $.Characters }}{{ index $.TrioMaps $a.ID $b.ID $c.ID }},{{ end }}{{ end }}{{ end -}} };\nkk_vector_from_cintarray(arr, (kk_ssize_t){{ $.Count }}*(kk_ssize_t){{ $.Count }}*(kk_ssize_t){{ $.Count }}, kk_context())" c inline "kk_intx_t arr[] = {
js inline "[ {{- range $a := $.Characters }}{{ range $b := $.Characters }}{{ range $c := $.Characters }}{{ index $.TrioMaps $a.ID $b.ID $c.ID }},{{ end }}{{ end }}{{ end -}} ]" {{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- range $c := $.Characters }}
{{- index $.TrioMaps $a.ID $b.ID $c.ID }},
{{- end }}
{{- end }}
{{- end -}}
};\nkk_vector_from_cintarray(arr, (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }}, kk_context())"
js inline "[
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- range $c := $.Characters }}
{{- index $.TrioMaps $a.ID $b.ID $c.ID }},
{{- end }}
{{- end }}
{{- end -}}
]"
val global/trio-table = global/create-trio-table() val global/trio-table = global/create-trio-table()
// Base affinity for a trio using the global ruleset. // Base affinity for a trio using the global ruleset.

View File

@@ -1,15 +1,17 @@
package main package main
import ( import (
_ "embed" "embed"
"fmt" "fmt"
"io" "io"
"regexp"
"strings" "strings"
"text/template" "text/template"
"unicode"
) )
//go:embed character.kk.template //go:embed character.kk.template skill.kk.template
var characterKK string var templates embed.FS
// LoadTemplates sets up templates to render game data to source code. // LoadTemplates sets up templates to render game data to source code.
func LoadTemplates() (*template.Template, error) { func LoadTemplates() (*template.Template, error) {
@@ -17,15 +19,11 @@ func LoadTemplates() (*template.Template, error) {
t.Funcs(template.FuncMap{ t.Funcs(template.FuncMap{
"kkenum": kkenum, "kkenum": kkenum,
}) })
t, err := t.Parse(characterKK) return t.ParseFS(templates, "*")
if err != nil {
return nil, fmt.Errorf("parsing characterKK: %w", err)
}
return t, nil
} }
// ExecCharacterKK renders the Koka character module to w. // ExecCharacterKK renders the Koka character module to w.
func ExecCharacterKK(t *template.Template, w io.Writer, c []Character, pairs, trios []AffinityRelation) error { func ExecCharacterKK(t *template.Template, w io.Writer, c []NamedID[Character], pairs, trios []AffinityRelation) error {
if len(pairs) != len(c)*len(c) { if len(pairs) != len(c)*len(c) {
return fmt.Errorf("there are %d pairs but there must be %d for %d characters", len(pairs), len(c)*len(c), len(c)) return fmt.Errorf("there are %d pairs but there must be %d for %d characters", len(pairs), len(c)*len(c), len(c))
} }
@@ -52,7 +50,7 @@ func ExecCharacterKK(t *template.Template, w io.Writer, c []Character, pairs, tr
} }
data := struct { data := struct {
Characters []Character Characters []NamedID[Character]
Pairs []AffinityRelation Pairs []AffinityRelation
Trios []AffinityRelation Trios []AffinityRelation
PairMaps map[int]map[int]int PairMaps map[int]map[int]int
@@ -63,8 +61,56 @@ func ExecCharacterKK(t *template.Template, w io.Writer, c []Character, pairs, tr
return t.ExecuteTemplate(w, "koka-character", &data) return t.ExecuteTemplate(w, "koka-character", &data)
} }
func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup]) error {
data := struct {
Groups []NamedID[SkillGroup]
}{g}
return t.ExecuteTemplate(w, "koka-skill", &data)
}
const replaceDash = " ,!?/+();#○◎☆♡'&=♪∀゚∴"
var (
kkReplace = func() *strings.Replacer {
r := []string{
"Triple 7s", "Triple-Sevens", // hard to replace with the right thing automatically
"1,500,000 CC", "Million-CC",
"1st", "First",
".", "",
"'s", "s",
"ó", "o",
"∞", "Infinity",
}
for _, c := range replaceDash {
r = append(r, string(c), "-")
}
return strings.NewReplacer(r...)
}()
kkMultidash = regexp.MustCompile(`-+`)
kkDashNonletter = regexp.MustCompile(`-[^A-Za-z]`)
)
func kkenum(name string) string { func kkenum(name string) string {
name = strings.ReplaceAll(name, ".", "") orig := name
name = strings.ReplaceAll(name, " ", "-") name = kkReplace.Replace(name)
name = kkMultidash.ReplaceAllLiteralString(name, "-")
name = strings.Trim(name, "-")
if len(name) == 0 {
panic(fmt.Errorf("%q became empty as Koka enum variant", orig))
}
name = strings.ToUpper(name[:1]) + name[1:]
if !unicode.IsLetter(rune(name[0])) {
panic(fmt.Errorf("Koka enum variant %q (from %q) starts with a non-letter", name, orig))
}
for _, c := range name {
if c > 127 {
// Koka does not allow non-ASCII characters in source code.
// Don't proceed if we've missed one.
panic(fmt.Errorf("non-ASCII character %q (%[1]U) in Koka enum variant %q (from %q)", c, name, orig))
}
}
if kkDashNonletter.MatchString(name) {
panic(fmt.Errorf("non-letter character after a dash in Koka enum variant %q (from %q)", name, orig))
}
return name return name
} }

View File

@@ -17,15 +17,26 @@ var characterAffinity2SQL string
//go:embed character.affinity3.sql //go:embed character.affinity3.sql
var characterAffinity3SQL string var characterAffinity3SQL string
type Character struct { //go:embed skill-group.sql
var skillGroupSQL string
type (
Character struct{}
SkillGroup struct{}
)
type NamedID[T any] struct {
// Disallow conversions between NamedID types.
_ [0]*T
ID int ID int
Name string Name string
// For internal use, the index of the character. // For internal use, the index of the identity, when it's needed.
// We don't show this in public API, but it lets us use vectors for lookups. // We don't show this in public API, but it lets us use vectors for lookups.
Index int Index int
} }
func Characters(ctx context.Context, db *sqlitex.Pool) ([]Character, error) { func Characters(ctx context.Context, db *sqlitex.Pool) ([]NamedID[Character], error) {
conn, err := db.Take(ctx) conn, err := db.Take(ctx)
defer db.Put(conn) defer db.Put(conn)
if err != nil { if err != nil {
@@ -37,7 +48,7 @@ func Characters(ctx context.Context, db *sqlitex.Pool) ([]Character, error) {
} }
defer stmt.Finalize() defer stmt.Finalize()
var r []Character var r []NamedID[Character]
for { for {
ok, err := stmt.Step() ok, err := stmt.Step()
if err != nil { if err != nil {
@@ -46,7 +57,7 @@ func Characters(ctx context.Context, db *sqlitex.Pool) ([]Character, error) {
if !ok { if !ok {
break break
} }
c := Character{ c := NamedID[Character]{
ID: stmt.ColumnInt(0), ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1), Name: stmt.ColumnText(1),
Index: stmt.ColumnInt(2), Index: stmt.ColumnInt(2),
@@ -133,3 +144,33 @@ func CharacterTrios(ctx context.Context, db *sqlitex.Pool) ([]AffinityRelation,
} }
return r, nil return r, nil
} }
func SkillGroups(ctx context.Context, db *sqlitex.Pool) ([]NamedID[SkillGroup], error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for skill groups: %w", err)
}
stmt, _, err := conn.PrepareTransient(skillGroupSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for skill groups: %w", err)
}
defer stmt.Finalize()
var r []NamedID[SkillGroup]
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping skill groups: %w", err)
}
if !ok {
break
}
g := NamedID[SkillGroup]{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
}
r = append(r, g)
}
return r, nil
}

View File

@@ -3,7 +3,7 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"fmt" "log/slog"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@@ -29,38 +29,60 @@ func main() {
t, err := LoadTemplates() t, err := LoadTemplates()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "loading templates: %s\n", err) slog.Error("loading templates", slog.Any("err", err))
os.Exit(2) os.Exit(2)
} }
slog.Info("open", slog.String("mdb", mdb))
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("opening mdb", slog.String("mdb", mdb), slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
slog.Info("get characters")
charas, err := Characters(ctx, db) charas, err := Characters(ctx, db)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("getting characters", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
slog.Info("get pairs")
pairs, err := CharacterPairs(ctx, db) pairs, err := CharacterPairs(ctx, db)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("getting pairs", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
slog.Info("get trios")
trios, err := CharacterTrios(ctx, db) trios, err := CharacterTrios(ctx, db)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("getting trios", slog.Any("err", err))
os.Exit(1)
}
slog.Info("get skill groups")
sg, err := SkillGroups(ctx, db)
if err != nil {
slog.Error("getting skill groups", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
cf, err := os.Create(filepath.Join(out, "character.kk")) cf, err := os.Create(filepath.Join(out, "character.kk"))
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("creating character.kk", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
slog.Info("write characters")
if err := ExecCharacterKK(t, cf, charas, pairs, trios); err != nil { if err := ExecCharacterKK(t, cf, charas, pairs, trios); err != nil {
fmt.Fprintln(os.Stderr, err) slog.Error("writing character.kk", slog.Any("err", err))
// continue on
}
sf, err := os.Create(filepath.Join(out, "skill.kk"))
if err != nil {
slog.Error("creating skill.kk", slog.Any("err", err))
os.Exit(1)
}
slog.Info("write skills")
if err := ExecSkillKK(t, sf, sg); err != nil {
slog.Error("writing skill.kk", slog.Any("err", err))
// continue on // continue on
} }
} }

14
horsegen/skill-group.sql Normal file
View File

@@ -0,0 +1,14 @@
WITH skill_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 47
)
SELECT
group_id,
name
FROM skill_data d
JOIN skill_names n ON d.id = n.id
WHERE d.group_rate = 1
ORDER BY group_id

View File

@@ -0,0 +1,48 @@
{{ define "koka-skill" -}}
module horse/skill
// Automatically generated with the horsegen tool; DO NOT EDIT
// Skill groups.
// A skill group may contain white, circle, double-circle, gold, and purple skills
// for the same effect.
// Sparks that grant skills refer to a skill group.
pub type skill-group
{{- range $g := $.Groups }}
{{ kkenum $g.Name }}
{{- end }}
// Map a skill group to its ID.
pub fip fun skill-group/group-id(^sg: skill-group): int
match sg
{{- range $g := $.Groups }}
{{ kkenum $g.Name }} -> {{ $g.ID }}
{{- end }}
// Get the skill group for an ID.
pub fip(1) fun skill-group/from-id(^id: int): maybe<skill-group>
match id
{{- range $g := $.Groups }}
{{ $g.ID }} -> Just( {{- kkenum $g.Name -}} )
{{- end }}
_ -> Nothing
// Get names for skill groups.
// Skill group names are the name of the base skill in the group.
pub fun skill-group/show(sg: skill-group): string
match sg
{{- range $g := $.Groups }}
{{ kkenum $g.Name }} -> {{ printf "%q" $g.Name }}
{{- end }}
// Compare two skill groups by ID order.
pub fip fun skill-group/order2(a: skill-group, b: skill-group): order2<skill-group>
match cmp(a.group-id, b.group-id)
Lt -> Lt2(a, b)
Eq -> Eq2(a)
Gt -> Gt2(a, b)
pub fun skill-group/(==)(a: skill-group, b: skill-group): bool
a.group-id == b.group-id
{{- end }}