Initial commit: import and cleanup of the feuforeve.fr website to promote the Floraverse section into an independent website.

This commit is contained in:
Feufochmar 2023-08-18 14:37:13 +02:00
commit 2e6aea78f4
31 changed files with 5680 additions and 0 deletions

40
main.rkt Normal file
View File

@ -0,0 +1,40 @@
#lang racket/base
(module+ test
(require rackunit)
;; Tests to be run with raco test
)
(module+ main
(require
"src/webcontainer/webcontainer.rkt"
"src/webcontainer/website.rkt"
"src/pages/sitemap.rkt"
"src/pages/floraverse.rkt")
; Website
(define *website*
(website
"" weblet pages:floraverse
("CharacterGenerator" weblet pages:floraverse-character-generator
("Tweet" weblet pages:floraverse-character-generator-tweet))
("AboutCharacterGenerator" weblet pages:floraverse-character-generator-about)
("Calendar" weblet pages:floraverse-calendar)
("Calendar/{month}/{day}" matching-weblet pages:floraverse-calendar)
))
; Sitemap
(sitemap
'(("Home" "/" #f)
("Character Generator" "/CharacterGenerator" #t)
("Calendar" "/Calendar" #f)
("About the character generator" "/AboutCharacterGenerator" #f))
)
; Webcontainer
(define *webcontainer*
(make-webcontainer
#:static
(make-immutable-hash
`(("" . "./static")))))
(webcontainer-add-website! *webcontainer* *website*)
(webcontainer-set-404-weblet! *webcontainer* pages:not-found)
(display "Starting server...")(newline)
(webcontainer-start *webcontainer*))

1855
mime.types Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
#lang racket/base
(require
"random.rkt")
; Category generator
; Generate collections of items from named lists
(provide
jsexpr->category-generator
category-generate
category-generate-list
category-generate-dictionary
)
; Read from a json expresion
; The model is directly the structure read from json (which is a hash)
(define (jsexpr->category-generator js)
js)
; Generate one element from a given category
(define (category-generate gen (cat #f))
(random:from-list
(if cat
(hash-ref gen cat)
(cdr (random:from-hash gen)))))
; Generate a list of n elements from a category
(define (category-generate-list gen cat n)
(random:sublist (hash-ref gen cat) n))
; Generate a dictionary of category => item
(define (category-generate-dictionary gen)
(let ((ret (make-hash)))
(hash-for-each
gen
(lambda (k v)
(hash-set! ret k (random:from-list v))))
ret))

View File

@ -0,0 +1,182 @@
#lang racket/base
(provide
distribution
make-distribution
distribution-generate
distribution-add-to!
distribution-contains?
distribution-check-only
distribution-linear-combination
distribution-items
distribution-filter
distribution-empty?
; List format
distribution->jsexpr
jsexpr->distribution
; Object format
distribution->jsexpr/object
jsexpr->distribution/object
)
; Distribution structure
; Distributions are list of items associated to a number indicating its frequency.
; When an element is picked from the list, the frequency of the items is taken into account.
(struct s-distribution
([total #:mutable] ; total number of elements in the distribution
[lst-items #:mutable] ; same as items, but in a assoc-list
items)) ; hash item -> number of occurences
; Exported function
(define distribution-items s-distribution-items)
; Add an item to the distribution, with its number of occurences
(define (distribution-add-to! d itm [additionnal-occurences 1])
(define occurences (hash-ref (distribution-items d) itm 0))
(hash-set! (s-distribution-items d) itm (+ occurences additionnal-occurences))
(set-s-distribution-lst-items! d (hash->list (s-distribution-items d)))
(set-s-distribution-total! d (+ (s-distribution-total d) additionnal-occurences)))
; Constructor with a list of item/occurences pairs
; item is (car pair), occurence is (cdr pair)
(define (distribution . pairs)
(s-distribution
(foldl
(lambda (p res) (+ res (cdr p)))
0
pairs)
pairs
(make-hash pairs)))
; Syntax to build a distribution from the items and theirs occurences
; This syntax allow default values for items
; Note: the item is quoted
(define-syntax make-distribution
(syntax-rules (*)
; No arguments - default constructor
((make-distribution)
(s-distribution 0 (list) (make-hash)))
; default syntax
((make-distribution (id val) ...)
(let ((dist (distribution)))
(begin
(distribution-add-to! dist (quote id) val) ...)
dist))
; From a list and default values
((make-distribution lst (* default-val))
(let ((dist (distribution)))
(for-each
(lambda (x) (distribution-add-to! dist x default-val))
lst)
dist))
; From a list, default values and values overriding the defaults
((make-distribution lst (* default-val) (id val) ...)
(let ((h (make-hash))
(dist (distribution)))
(for-each
(lambda (x) (hash-set! h x default-val))
lst)
(begin
(hash-set! h (quote id) val) ...)
(hash-for-each h (lambda (k v) (distribution-add-to! dist k v)))
dist))
; With a useless list
((make-distribution lst (id val) ...)
(let ((dist (distribution)))
(begin
(distribution-add-to! dist (quote id) val) ...)
dist))
))
; Generate an item from a distribution
(define (distribution-generate d)
; Search function
(define (search lst subtotal roll)
(define new-subtotal (+ subtotal (cdar lst)))
(if (< roll new-subtotal)
(caar lst)
(search (cdr lst) new-subtotal roll)))
(search (s-distribution-lst-items d) 0 (random (s-distribution-total d))))
; Check if an item is in the distribution
(define (distribution-contains? d itm)
(hash-has-key? (s-distribution-items d) itm))
; Check if there are unkown items in the distribution
; Raise an error if an unkown item is present
(define (distribution-check-only d known-items)
(hash-for-each
(s-distribution-items d)
(lambda (k v)
(when (not (member k known-items))
(error "Distribution contains an unknown item: " k)))))
; Linear combination of distributions
; Make a new distribution from a dotted list of distributions and their weight
(define (distribution-linear-combination dists)
(define d (distribution))
(for-each
(lambda (x)
(define itms
(hash-map
(s-distribution-items (car x))
(lambda (k v)
(cons k (/ v (s-distribution-total (car x)))))))
(for-each
(lambda (itm)
(distribution-add-to! d (car itm) (truncate (* (cdr x) (cdr itm) 100)))) ; * 100 and truncate because (cdr itm) is reduced
itms))
dists)
d)
; Make a new distribution by filtering an existing distribution and keeping only the values that check a predicate
(define (distribution-filter d pred)
(apply
distribution
(filter
(lambda (x) (pred (car x)))
(s-distribution-lst-items d))))
; Return true if the distribution is empty: no element in it
(define (distribution-empty? d)
(eq? 0 (s-distribution-total d)))
; Serialisation to a json-compatible structure
; It is possible to pass a translator function for converting values that are not compatible to json
(define (distribution->jsexpr dist #:translator [tr (lambda (x) x)])
(hash-map
(s-distribution-items dist)
(lambda (k v)
(make-immutable-hash
`((value . ,(tr k))
(count . ,v))))))
; Object version: the distribution is serialised as an object whose attribute names are the distribution values and associated to their occurences.
; The translation function applies to the distribution values, and must return a symbol
(define (distribution->jsexpr/object dist #:translator [tr (lambda (x) x)])
(define ret (make-hash))
(hash-for-each
(s-distribution-items dist)
(lambda (k v)
(hash-set! (tr k) v)))
ret)
; Deserialisation from a json-compatible structure
; It is possible to pass a translator function for converting values that are not compatible to json
(define (jsexpr->distribution js #:translator [tr (lambda (x) x)])
(define dist (distribution))
(for-each
(lambda (x)
(distribution-add-to! dist (tr (hash-ref x 'value)) (hash-ref x 'count)))
js)
dist)
; Object version: the distribution is serialised as an object whose attribute names are the distribution values and associated to their occurences.
; The translation function applies to the distribution values, and must take a symbol in parameter
(define (jsexpr->distribution/object js #:translator [tr (lambda (x) x)])
(define dist (distribution))
(hash-for-each
js
(lambda (k v)
(distribution-add-to! dist (tr k) v)))
dist)

View File

@ -0,0 +1,108 @@
#lang racket/base
(require
racket/list
"distribution.rkt")
(provide make-markov
markov-add-example!
markov-add-next-item!
markov-generate
markov->jsexpr
jsexpr->markov)
; Markov chain structure
(struct markov
(order ; order of the chain
next-items)) ; table of given sequence -> distributions of next possible items
; Constructor
(define (make-markov [order 1])
(markov order (make-hash)))
; Add a example to the generator. The example is a sequence of values in a list.
(define (markov-add-example! mrk example)
(letrec
((fill
(lambda (lst prev)
(let ((distro (hash-ref (markov-next-items mrk) prev #f))
(next (if (null? lst) #f (car lst))))
(if distro
(distribution-add-to! distro next)
(let ((new-distro (make-distribution)))
(distribution-add-to! new-distro next)
(hash-set! (markov-next-items mrk) prev new-distro)))
(when next (fill (cdr lst) (append (cdr prev) (list next))))
))))
(fill example (make-list (markov-order mrk) #f))
))
; Add a possible next item from a sequence
; Use this to regenerate a markov chain from a serialized version (ie not from examples)
(define (markov-add-next-item! mrk seq next [occurences 1])
(let ((distro (hash-ref (markov-next-items mrk) seq #f)))
(if distro
(distribution-add-to! distro next occurences)
(let ((new-distro (make-distribution)))
(distribution-add-to! new-distro next occurences)
(hash-set! (markov-next-items mrk) seq new-distro)))))
; Generate a list with the given initial sequence
(define (generate-list next-items result previous)
(let ((next (distribution-generate (hash-ref next-items previous))))
(if (not next)
(reverse result)
(generate-list next-items (cons next result) (append (cdr previous) (list next))))))
; Get a suitable starting sequence from the given list
(define (find-starting-sequence next-items initial)
; If the given sequence is a suitable sequence, return it
(if (hash-has-key? next-items initial)
initial
(letrec
; rating function to compare two sequences
((rate
(lambda (result a b)
(if (or (null? a) (null? b))
result
(rate (+ result (if (equal? (car a) (car b)) 1 0)) (cdr a) (cdr b))))))
; the suitable starting sequence is the one that get the best result with the rate function when compared to the initial sequence
(caar
(sort
(map
(lambda (lst) (cons lst (rate 0 (reverse lst) (reverse initial)))) ; The starting sequence is more suitable is we start from the last elements
(shuffle
(hash-keys next-items)))
(lambda (a b) (> (cdr a) (cdr b))))))))
; Generate a list from the chain
(define (markov-generate mrk [initial #f])
(if initial
(let ((prev (find-starting-sequence (markov-next-items mrk) initial)))
(generate-list (markov-next-items mrk) (reverse initial) prev))
(generate-list (markov-next-items mrk) (list) (make-list (markov-order mrk) #f))))
; Serialisation to a json-compatible structure
; It is possible to pass a translator function for converting values that are not compatible to json
(define (markov->jsexpr mrk #:translator [tr (lambda (x) x)])
(make-immutable-hash
`((order . ,(markov-order mrk))
(next . ,(hash-map
(markov-next-items mrk)
(lambda (k v)
(make-immutable-hash
`((sequence . ,(map tr k))
(distribution . ,(distribution->jsexpr v #:translator tr)))))))
)))
; Deserialisation from a json-compatible structure
; It is possible to pass a translator function for converting values that are not compatible to json
(define (jsexpr->markov js #:translator [tr (lambda (x) x)])
(define mrk (make-markov (hash-ref js 'order)))
(for-each
(lambda (x)
(hash-set!
(markov-next-items mrk)
(map tr (hash-ref x 'sequence))
(jsexpr->distribution (hash-ref x 'distribution) #:translator tr)))
(hash-ref js 'next))
mrk)

View File

@ -0,0 +1,106 @@
#lang racket/base
; Various functions working with randomness
(require
racket/list
math/distributions
racket/flonum)
(provide
random:boolean random:normal
random:from-vector random:from-list random:from-hash
random:subvector random:sublist random:subhash
hash-key* hash-ref*
random:hash-combine
)
; Choose a random boolean
(define (random:boolean)
(eq? 0 (random 2)))
; Choose a number using the normal distribution
; It is possible to indicate a mean and a standard-deviation
(define (random:normal [mean 0.] [dev 1.])
(flvector-ref (flnormal-sample mean dev 1) 0))
; Choose a random element from a vector
(define (random:from-vector vec)
(if (< 0 (vector-length vec))
(vector-ref vec (random (vector-length vec)))
(error "Cannot choose an element from an empty vector.")))
; Choose a random element from a list
(define (random:from-list lst)
(if (not (null? lst))
(car (shuffle lst))
(error "Cannot choose an element from an empty list.")))
; Choose a random element from a hash
; return a pair (key . value)
(define (random:from-hash hsh)
(if (not (hash-empty? hsh))
(random:from-list (hash->list hsh))
(error "Cannot choose an element from an empty hash.")))
; Create a list of at most n elements choosen at random from a list
(define (random:sublist lst n)
; Take at most n element from a list
(define (take-at-most in n [out (list)])
(if (or (null? in) (eq? n 0))
(reverse out)
(take-at-most (cdr in) (- n 1) (cons (car in) out))))
(take-at-most (shuffle lst) n))
; Create a vector of at most n elements choosen at random from a vector
(define (random:subvector vec n)
(list->vector (random:sublist (vector->list vec) n)))
; Create a hash of at most n elements choosen at random from a hash
(define (random:subhash hsh n)
(let ((res (make-hash)))
(for-each
(lambda (c)
(hash-set! res (car c) (cdr c)))
(random:sublist (hash->list hsh) n))
res))
; Return the key if it is present in the hash or return a random key if it is absent
; Return #f if the hash is empty
(define (hash-key* hsh key)
(cond
( (hash-empty? hsh)
#f)
( (hash-has-key? hsh key)
key)
( #t
(car (random:from-hash hsh)))))
; Return the value associated to a key in the hash if the key is present
; or return a random value from the hash if absent
; Return #f if the hash is empty
(define (hash-ref* hsh key)
(cond
( (hash-empty? hsh)
#f)
( (hash-has-key? hsh key)
(hash-ref hsh key))
( #t
(cdr (random:from-hash hsh)))))
(define (random:hash-combine . hsh-lst)
(define tmp (make-hash))
(define ret (make-hash))
(for-each
(lambda (h)
(hash-for-each
h
(lambda (k v)
(if (hash-has-key? tmp k)
(hash-set! tmp k (cons v (hash-ref tmp k)))
(hash-set! tmp k (list v))))))
hsh-lst)
(hash-for-each
tmp
(lambda (k v)
(hash-set! ret k (random:from-list v))))
ret)

92
src/collection/tree.rkt Normal file
View File

@ -0,0 +1,92 @@
#lang racket/base
(provide
tree tree? tree-key tree-value tree-children
make-tree tree-ref tree-set!
tree-node tree-child tree->list
tree-for-each tree-find-path tree-fold)
; Tree structures, with key and possible value on nodes
; Like a table, with a list of keys as reference to a value
(struct tree
(key ; Key associated to a node. #f for the root of the tree
(value #:mutable) ; Value associated to a node. May be #f if the node has no value associated with (it should have children then)
(children #:mutable) ; Children list of a node
))
; Constructor
; New node of a tree.
(define (make-tree [key #f])
(tree key #f (list)))
; Get a child from its key
(define (tree-child t key)
(let ((child-list
(filter
(lambda (x) (equal? (tree-key x) key))
(tree-children t))))
(if (null? child-list)
#f
(car child-list))))
; Set a value on the given path of keys in the tree
(define (tree-set! tr path value)
(if (null? path)
(set-tree-value! tr value)
(let ((child (tree-child tr (car path))))
(if child
(tree-set! child (cdr path) value)
(let ((new-child (make-tree (car path))))
(set-tree-children! tr (cons new-child (tree-children tr)))
(tree-set! new-child (cdr path) value))))))
; Get a sub-tree from its path, or #f if not found
(define (tree-node tr path)
(if (null? path)
tr
(let ((child (tree-child tr (car path))))
(if child
(tree-node child (cdr path))
#f))))
; Get a value from its path, or #f if not found
(define (tree-ref tr path)
(let ((node (tree-node tr path)))
(and node (tree-value node))))
; Get the list of all values along a path, or #f if the path does not exist
(define (tree->list tr path [ret (list)])
(if (null? path)
(reverse (cons (tree-value tr) ret))
(let ((child (tree-child tr (car path))))
(if child
(tree->list child (cdr path) (cons (tree-value tr) ret))
#f))))
; Apply the given function on every node of the tree
(define (tree-for-each tr fun)
(fun (tree-value tr))
(for-each
(lambda (c) (tree-for-each c fun))
(tree-children tr)))
; Get the path to the first item whose value checks the predicate
(define (tree-find-path tr pred (path (list)))
(if (pred (tree-value tr))
(cdr (reverse (cons (tree-key tr) path))) ; cdr as the root node key should not be present
(findf
(lambda (x) x)
(map
(lambda (c) (tree-find-path c pred (cons (tree-key tr) path)))
(tree-children tr)))))
; Fold the tree into a single value
; This works around the fun function who takes two arguments : the value of a node, the list of results of tree-fold applied recursively on all children
(define (tree-fold tr fun)
(fun
(tree-value tr)
(map
(lambda (x)
(tree-fold x fun))
(tree-children tr))))

View File

@ -0,0 +1,346 @@
#lang racket/base
; Floraverse character generator
; Generates a character and transform it into various formats
(require
racket/string
"flora/character.rkt"
"flora/database.rkt"
"flora/calendar.rkt"
"flora/gender.rkt"
"flora/sex.rkt"
"flora/element.rkt"
"flora/affinity.rkt"
"flora/geography.rkt"
"flora/species.rkt"
"flora/ability.rkt"
"phonagen.rkt"
"../lang/english.rkt"
"../lang/case.rkt"
)
(provide
; Generation of a character description, with a format, and constraints
flora-character-generate
; Formating functions
flora-character->full-html
flora-character->tweets
)
(define (flora-character-generate floragen format [constraints (make-hash)])
; Generate the character
(define character (character-generate floragen constraints))
; Output with the format
(format character floragen))
; Formating functions
; Format to the full html description as sxml
(define (flora-character->full-html chr floragen)
; Gender and grammatical words
(define gender (character-gender chr))
(define plural? (gender-plural? gender))
(define subject (gender-subject gender))
(define genitive (gender-genitive gender))
; Name
(define name (character-name chr))
; Species name
(define sp-name (compute-species-name
floragen
(character-species-genes chr)
(character-species-start chr)
(character-species-imited chr)))
; Birth
(define birthday (character-birthday chr))
; Affinity
(define affinity (character-affinity chr))
; Output
`(section
(h2 ,(show-name name))
(p
(h3 "Identity")
,(gender-title-abbreviation gender) " "
,(show-name name) " "
,(english:3rd-person-of "be" plural?) " "
,(english:undefined-article (car sp-name)) " " ,(cdr sp-name) ". " (br)
,(case:upcase-1st genitive) " name is pronounced "
"/" ,(word-pronounciation name) "/. " (br)
,(case:upcase-1st subject) " " ,(english:3rd-person-of "live" plural?) " "
,(cdr (show-place (character-living-place chr) #t)) ". "
(details
(summary "Genes")
(table
(tr
(td (strong "Species"))
,@(map (lambda (x) `(td ,(car x))) (character-species-genes chr)))
(tr
(td (strong "Amount"))
,@(map (lambda (x) `(td ,(number->string (cdr x)) "%")) (character-species-genes chr)))
))
)
(p
(h3 "Birth")
,(case:upcase-1st subject) " " ,(english:3rd-person-of "be" plural?) " born "
,(sex-name (character-sex chr))
" on the " ,(english:ordinal (date+sign-day birthday)) " of " ,(date+sign-month-name birthday) " "
,(cdr (show-place (character-birth-place chr) #t)) ". " (br)
,(case:upcase-1st genitive) " astrological sign is "
(img ((src ,(string-append (flora-generator-path floragen) "/" (date+sign-sign-symbol birthday)))
(width "16")
(height "16")
))
" " ,(date+sign-sign-name birthday) ". "
)
(p
(h3 "Affinity")
,(case:upcase-1st subject) " " ,(english:3rd-person-of "have" plural?) " "
,(element-affinity-description
(elements-ref
(flora-generator-elements floragen)
(signed-element-name (car (affinity-composition affinity))))) ". "
(details
(summary "Affinity breakdown")
,(affinity-composition-table (affinity-composition affinity)))
(details
(summary "Primary affinity breakdown")
,(affinity-composition-table (affinity-non-reduced-composition affinity)))
)
(p
(h3 "Appearance and personnality")
; Size
,(if (character-size chr)
`(span
,(case:upcase-1st subject) " " ,(english:3rd-person-of "be" plural?) " "
,(character-size chr) " for " ,genitive " species. ")
"")
; Weight
,(if (character-weight chr)
`(span
,(case:upcase-1st subject) " " ,(english:3rd-person-of "be" plural?) " "
,(character-weight chr) ". " (br))
"")
; Natures
,(case:upcase-1st subject) " " ,(english:3rd-person-of "be" plural?) " "
,@(english:add-commas (character-natures chr)) ". " (br)
; Traits
,@(apply append (map (lambda (x) (list x '(br))) (character-traits chr)))
; Motto
,(case:upcase-1st genitive) " motto is “" ,(character-motto chr) ""
)
(p
(h3 "Abilities")
,@(map
(lambda (ab)
`(details
(summary ,(case:upcase-1st (ability-name ab)))
,(ability-description ab)))
(character-abilities chr))
)
,(if (or (character-father chr) (character-mother chr))
`(p
(h3 "Family")
,(if (character-father chr)
`(details
(summary "Father")
,(flora-character->full-html (character-father chr) floragen))
"")
,(if (character-mother chr)
`(details
(summary "Mother")
,(flora-character->full-html (character-mother chr) floragen))
"")
)
"")
))
; Generation into a series of tweets
; Return a hash with the following values:
; identity, birth, traits, motto
(define (flora-character->tweets chr floragen)
; Gender and grammatical words
(define gender (character-gender chr))
(define plural? (gender-plural? gender))
(define subject (gender-subject gender))
(define genitive (gender-genitive gender))
; Name
(define name (character-name chr))
; Species name
(define sp-name (compute-species-name
floragen
(character-species-genes chr)
(character-species-start chr)
(character-species-imited chr)))
; Birth
(define birthday (character-birthday chr))
; Affinity
(define affinity (character-affinity chr))
;
(make-immutable-hash
`((identity .
,(string-append
(gender-title-abbreviation gender) " " (show-name name) " "
(english:3rd-person-of "be" plural?) " " (english:with-undefined-article (car sp-name)) " with "
(element-affinity-description
(elements-ref
(flora-generator-elements floragen)
(signed-element-name (car (affinity-composition affinity))))) ". "
(case:upcase-1st genitive) " name is pronounced "
"/" (word-pronounciation name) "/."))
(birth .
,(string-append
(case:upcase-1st subject) " " (english:3rd-person-of "be" plural?) " born "
(sex-name (character-sex chr)) " on the " (english:ordinal (date+sign-day birthday)) " of " (date+sign-month-name birthday) " "
(car (show-place (character-birth-place chr) #t)) ". "
(case:upcase-1st genitive) " astrological sign is " (date+sign-sign-name birthday) "."))
(traits . ,(string-join (character-traits chr) " "))
(motto .
,(string-append
(case:upcase-1st genitive) " motto is “" (character-motto chr) ""))
)))
; Helping functions for the diverse outputs
; Display a name, and eventually its latin transcription
(define (show-name name)
(let ((native-name (word-native-transcription name))
(latin-name (word-latin-transcription name)))
(string-append
(case:upcase-1st native-name)
(if (equal? native-name latin-name)
""
(string-append " (" (case:upcase-1st latin-name) ")")))))
; Species names from genes, as plain text and sxml
; Compute the name of the species, as string and sxml pair: (cons str-name sxml-name)
; Note: genes are sorted by the more predominent to the less predominent
(define (compute-species-name floragen genes starting-species imited-species)
; Mimic?
(define mimic? (species-mimic? starting-species))
; Only keep the species name whose genes are predominent enough
(define main-gene-count (cdar genes))
(define kept-species-name
(map
car
(filter
(lambda (sp-name+count)
(>= (* 2 (cdr sp-name+count)) main-gene-count))
genes)))
; Get name from the kept species
; Return a string if a name is found or a list of string
(define sp-name
(cond
( (eq? 1 (length kept-species-name))
(car kept-species-name))
( (eq? 2 (length kept-species-name))
(species-database-find-crossbred-name
(flora-generator-species floragen)
kept-species-name))
( #t
kept-species-name)))
; Function to show a species name with its link, accept a string or a species
(define (with-link sp)
(define species
(if (string? sp)
(species-database-get (flora-generator-species floragen) sp)
sp))
`(a ((href ,(species-reference-link species))) ,(species-name species)))
; Return the result
(cond
; Mimic of a mix of species - imply the use of imited species genes
( (and (list? sp-name) mimic?)
(cons
; String version
(string-append
(species-name starting-species)
" looking like a mix of "
(string-join (english:add-commas sp-name) ""))
; Sxml version
`(span
,(with-link starting-species)
" looking like a mix of "
,@(english:add-commas
(map with-link sp-name)))))
; Mimic - use imited species genes
( (and mimic? (not (species-mimic-genes-used? starting-species)))
(cons
; String version
(string-append
sp-name
" "
(species-name starting-species))
; Sxml version
`(span
,(with-link sp-name)
" "
,(with-link starting-species))))
; Mimic - use mimic species genes
( mimic?
(cons
; String version
(string-append
(species-name imited-species)
" "
(species-name starting-species))
; Sxml version
`(span
,(with-link imited-species)
" "
,(with-link starting-species))))
; Unnamed hybrid of species
( (list? sp-name)
(cons
; String version
(string-append
"hybrid of "
(string-join (english:add-commas sp-name) ""))
; Sxml version
`(span
"hybrid of "
,@(english:add-commas
(map with-link sp-name)))))
; Variety
; sp-name corresponds to compatibility-name of starting-species
; => use species-name, so that the variety is kept
( (and (equal? sp-name (species-compatibility-name starting-species))
(species-name starting-species))
(cons
; String version
(species-name starting-species)
; Sxml version
`(span
,(with-link (species-name starting-species)))))
; Named species
( #t
(cons
; String version
sp-name
; Sxml version
`(span
,(with-link sp-name))))
))
; Affinity composition
(define (affinity-composition-table composition)
`(table
(tr
(td (strong "Element"))
,@(map (lambda (x) `(td ,(signed-element-name x))) composition))
(tr
(td (strong "Sign"))
,@(map (lambda (x) `(td ,(signed-element-sign x))) composition))
(tr
(td (strong "Amount"))
,@(map (lambda (x) `(td ,(number->string (signed-element-count x)) "%")) composition))
))
; Display a place
; As plain text and sxml
(define (show-place place in?)
(define preposition ((if in? location-type-preposition-in location-type-preposition-near)
(place-type place)))
(cons
; String version
(string-append preposition " " (place-name place))
; sxml version
`(span ,preposition " " (a ((href ,(place-reference-link place))) ,(place-name place)))
))

View File

@ -0,0 +1,121 @@
#lang racket/base
; Model and generation of abilities
(provide
; Ability database operations
jsexpr->ability-database
ability-database-select
; ability structure
ability-name
ability-description
ability-author
)
; Ability structure
(struct ability
(name
description
author
conditions ; list of conditions to get the ability
))
; Species condition
(struct species-condition
(species-name
gene-threshold
automatic?))
; Non-automatic random limit
; Number of faces of the dice used to get an ability
; Should be higher than the max number of genes, as the number of genes is used as a threshold to get the ability
(define non-automatic-dice 125)
; Check a species condition
(define (species-condition-check spcond species-genes)
(define gene (findf
(lambda (x)
(equal? (species-condition-species-name spcond) (car x)))
species-genes))
(and
gene
(<= (species-condition-gene-threshold spcond) (cdr gene))
(or (species-condition-automatic? spcond)
(< (random non-automatic-dice) (cdr gene))) ; If not automatic, the more gene, the more chance to get it
))
; Affinity condition
(struct affinity-condition
(element-name
automatic?))
; Check an affinity condition
(define (affinity-condition-check afcond affinity-genes)
(define gene (findf
(lambda (x)
(equal? (affinity-condition-element-name afcond) (car x)))
affinity-genes))
(and
gene
(or (affinity-condition-automatic? afcond)
(< (random non-automatic-dice) (cdr gene))) ; If not automatic, the more gene, the more chance to get it
))
; Convert a jsexpr to an ability condition
(define (jsexpr->ability-condition js)
(define type (hash-ref js 'type))
(cond
((equal? type "species")
(species-condition
(hash-ref js 'species)
(hash-ref js 'gene-threshold)
(hash-ref js 'automatic?)))
((equal? type "affinity")
(affinity-condition
(hash-ref js 'affinity)
(hash-ref js 'automatic?)))
(#t
(error "Invalid ability condition type: " type))
))
; Check an ability condition
(define (ability-condition-check abcond species-genes affinity-composition)
(cond
((species-condition? abcond)
(species-condition-check abcond species-genes))
((affinity-condition? abcond)
(affinity-condition-check abcond affinity-composition))
(#t
(error "Not an ability condition: " abcond))))
; Convert a jsexpr to an ability
(define (jsexpr->ability js)
(ability
(hash-ref js 'name)
(hash-ref js 'description)
(hash-ref js 'author)
(map
jsexpr->ability-condition
(hash-ref js 'conditions))))
; Ability database
(struct ability-database
(abilities ; a list of abilities
))
; Convert a jsexpr to an ability database
(define (jsexpr->ability-database js)
(ability-database
(map
jsexpr->ability
js)))
; Select the abilities from the database that match the criterias
; Note: species-genes and affinity-composition are in a form of '((name . count) ...), sorted by count
(define (ability-database-select db species-genes affinity-composition)
(filter
(lambda (ab)
(findf
(lambda (x)
(ability-condition-check x species-genes affinity-composition))
(ability-conditions ab)))
(ability-database-abilities db)))

View File

@ -0,0 +1,190 @@
#lang racket/base
; Dependencies
(require
racket/list
"element.rkt"
"../../base-generation/random.rkt"
"../../base-generation/distribution.rkt")
; Export
(provide
; Construction of affinity
make-affinity
combine-affinity
; To display
affinity-composition
affinity-non-reduced-composition
; Signed elements
signed-element-name
signed-element-sign
signed-element-count
)
; Affinity structure
(struct affinity
(genes ; List of exact primary signed elements, used for computing genetic transmission
non-reduced-composition ; Sorted list of signed elements, with count computed
composition)) ; Combined composition, with secondary and tertiary elements
; Signed elements
; Structure used in composition of affinity
(struct signed-element
(name
sign
count))
; Number of genes in the list
(define nb-genes 100)
; Construction of affinity from a distribution of element names
; Note: the distribution must contains only primary and secondary elements
(define (make-affinity elements elem-dist)
(define nb-elements 4)
(define nb-occurences 13)
(define (random:signed-element elem) (signed-element elem (string-append (if (random:boolean) "+" "-") (substring elem 0 1)) 1))
(define genes
(take
(shuffle
(apply
append
(build-list
nb-elements
(lambda (_)
(define el (elements-ref elements (distribution-generate elem-dist)))
(if (combined-element? el)
(flatten
(make-list
nb-occurences
(map
random:signed-element
(combined-element-components el))))
(make-list (* 2 nb-occurences) (random:signed-element (element-name el)))
)))))
nb-genes))
(define non-reduced-composition (compute-composition genes))
(affinity
genes
non-reduced-composition
(reduce-composition elements non-reduced-composition)))
; Construction of affinity from two affinities
(define (combine-affinity elements aff1 aff2)
(define genes
(take
(shuffle
(append
(affinity-genes aff1)
(affinity-genes aff2)))
nb-genes))
(define non-reduced-composition (compute-composition genes))
(affinity
genes
non-reduced-composition
(reduce-composition elements non-reduced-composition)))
; Reduce the genes to a list of genes with occurence count
(define (compute-composition genes)
(define table (make-hash))
(for-each
(lambda (x)
(define cell (cons (signed-element-name x) (signed-element-sign x)))
(if (hash-has-key? table cell)
(hash-set! table cell (+ 1 (hash-ref table cell)))
(hash-set! table cell 1)))
genes)
(sort-composition
(hash-map
table
(lambda (k v)
(signed-element
(car k)
(cdr k)
v)))))
; Sort and remove the element with null count
(define (sort-composition comp)
(sort
(filter
(lambda (x) (> (signed-element-count x) 0))
comp)
(lambda (x y) (> (signed-element-count x) (signed-element-count y)))))
; Return the combination, or #f if it doesn't exist
; Combination will combine the signs in the order of the combination table.
(define (combine-element elements a b)
(define combination-table (elements-combination-table elements))
(cond
((hash-has-key? combination-table (list (signed-element-name a) (signed-element-name b)))
(signed-element
(hash-ref combination-table (list (signed-element-name a) (signed-element-name b)))
(string-append (signed-element-sign a) "/" (signed-element-sign b))
1))
((hash-has-key? combination-table (list (signed-element-name b) (signed-element-name a)))
(signed-element
(hash-ref combination-table (list (signed-element-name b) (signed-element-name a)))
(string-append (signed-element-sign b) "/" (signed-element-sign a))
1))
(#t #f)))
; Combine the composition
; Algo: if the rest of the input list is not null:
; Consider the two first elements of the input list
; if combination is possible:
; - combine the elements:
; * with elements A, B, of occurence count cA, cB (cA >= cB):
; * replace in the list the items [(A, c1), (B, cB)] by:
; + if A and B are both primary or secondary: [(AB, cB+cB), (A, cA-cB)]
; + if A is primary and B secondary: [(AB, cB+cB/2), (A, cA-cB/2)]
; + if A is secondary and B primary, and cB <= cA/2: [(AB, 3*cB), (A, cA-2*cB)]
; + if A is secondary and B primary, and cB > cA/2: [(AB, cA+cA/2), (B, cB-cA/2)]
; - sort the list
; else:
; - remove the first element from the input list and put it in the result list
; Repeat
(define (reduce-composition elements comp [result (list)])
(define (compute-replacement A B AB)
(let ((A-secondary? (combined-element? (elements-ref elements (signed-element-name A))))
(B-secondary? (combined-element? (elements-ref elements (signed-element-name B))))
(cA (signed-element-count A))
(cB (signed-element-count B))
)
(cond
( (eq? A-secondary? B-secondary?)
(list
(signed-element (signed-element-name AB) (signed-element-sign AB) (+ cB cB))
(signed-element (signed-element-name A) (signed-element-sign A) (- cA cB))))
( B-secondary?
(list
(signed-element (signed-element-name AB) (signed-element-sign AB) (+ cB (floor (/ cB 2))))
(signed-element (signed-element-name A) (signed-element-sign A) (- cA (floor (/ cB 2))))))
( (<= cB (/ cA 2))
(list
(signed-element (signed-element-name AB) (signed-element-sign AB) (+ cB cB cB))
(signed-element (signed-element-name A) (signed-element-sign A) (- cA cB cB))))
( #t
(list
(signed-element (signed-element-name AB) (signed-element-sign AB) (+ cA (floor (/ cA 2))))
(signed-element (signed-element-name B) (signed-element-sign B) (- cB (floor (/ cA 2))))))
)))
(cond
((null? comp)
(sort-composition result))
((null? (cdr comp))
(reduce-composition elements (list) (append comp result)))
(#t
(let* ((a (car comp))
(b (cadr comp))
(c (combine-element elements a b)))
(if c
(reduce-composition
elements
(sort-composition
(append (compute-replacement a b c) (cddr comp)))
result)
(reduce-composition
elements
(cdr comp)
(cons (car comp) result))
)))))

View File

@ -0,0 +1,163 @@
#lang racket/base
; Model and generation of birth date
(require
"../../base-generation/distribution.rkt")
; Only exported: calendar, date+sign
; Other structures are only used internally
(provide
jsexpr->calendar calendar-generate-date calendar-get-date
date+sign-day date+sign-month-name date+sign-month-number date+sign-sign-name date+sign-sign-symbol
)
;;;;
; Month structure
(struct month
(number
name
days
named-after))
; Construction from json expressions
(define (jsexpr->month js)
(month
(hash-ref js 'number)
(hash-ref js 'name)
(hash-ref js 'days)
(hash-ref js 'named-after)))
;;;;
; Simple date structure
(struct simple-date
(month
day))
; Simple date comparators
; Equal
(define (simple-date= d1 d2)
(and
(eq? (month-number (simple-date-month d1)) (month-number (simple-date-month d2)))
(eq? (simple-date-day d1) (simple-date-day d2))))
; Inferior
(define (simple-date< d1 d2)
(let ((m1 (month-number (simple-date-month d1)))
(m2 (month-number (simple-date-month d2))))
(or
(< m1 m2)
(and
(eq? m1 m2)
(< (simple-date-day d1) (simple-date-day d2))))))
; Superior
(define (simple-date> d1 d2)
(let ((m1 (month-number (simple-date-month d1)))
(m2 (month-number (simple-date-month d2))))
(or
(> m1 m2)
(and
(eq? m1 m2)
(> (simple-date-day d1) (simple-date-day d2))))))
; Inferior or equal
(define (simple-date<= d1 d2)
(not (simple-date> d1 d2)))
; Superior or equal
(define (simple-date>= d1 d2)
(not (simple-date< d1 d2)))
; Construction from json expressions
(define (jsexpr->simple-date js months)
(simple-date
(hash-ref months (hash-ref js 'month))
(hash-ref js 'day)))
;;;;
; Astrological sign structure
(struct astrological-sign
(name
symbol
from
to))
; Construction from json expressions
(define (jsexpr->astrological-sign js months)
(astrological-sign
(hash-ref js 'name)
(hash-ref js 'symbol)
(jsexpr->simple-date (hash-ref js 'from) months)
(jsexpr->simple-date (hash-ref js 'to) months)))
;;;;
; Date and sign
(struct date+sign
(month
day
sign))
; Accessors on date+sign
; Month name
(define (date+sign-month-name ds)
(month-name (date+sign-month ds)))
; Month number
(define (date+sign-month-number ds)
(month-number (date+sign-month ds)))
; Sign name
(define (date+sign-sign-name ds)
(astrological-sign-name (date+sign-sign ds)))
; Sign symbol
(define (date+sign-sign-symbol ds)
(astrological-sign-symbol (date+sign-sign ds)))
;;;;
; Calendar stucture
(struct calendar
(months-map ; Map of month index to month
months-distribution ; when picking a random date, a distribution is needed to take into account the fact that months may not have the same number of days
astrological-signs))
; Calendar from json-expression
(define (jsexpr->calendar js)
(define months-list (map jsexpr->month (hash-ref js 'months)))
(define months-map (make-hash))
(define months-distribution (make-distribution))
(for-each
(lambda (x)
(hash-set! months-map (month-number x) x)
(distribution-add-to! months-distribution x (month-days x)))
months-list)
(define sign-list (map (lambda (j) (jsexpr->astrological-sign j months-map)) (hash-ref js 'astrological-signs)))
(calendar
months-map
months-distribution
sign-list))
; Find the sign from a date
(define (calendar-get-sign cal date)
(findf
(lambda (as)
(let ((accross-year? (simple-date< (astrological-sign-to as) (astrological-sign-from as))))
(or (and (not accross-year?)
(simple-date<= (astrological-sign-from as) date)
(simple-date<= date (astrological-sign-to as)))
(and accross-year?
(or (simple-date<= date (astrological-sign-from as))
(simple-date<= (astrological-sign-to as) date ))))))
(calendar-astrological-signs cal)))
; Choose a random date from a calendar
; Returns a date+sign
(define (calendar-generate-date cal)
(define m (distribution-generate (calendar-months-distribution cal))) ; Month
(define d (+ 1 (random (month-days m)))) ; Day
(date+sign
m
d
(calendar-get-sign cal (simple-date m d))))
; Get a date from the month number and day number
; Returns a date+sign
(define (calendar-get-date cal m d)
(let ((month (hash-ref (calendar-months-map cal) m)))
(date+sign
month
d
(calendar-get-sign cal (simple-date month d)))))

View File

@ -0,0 +1,385 @@
#lang racket/base
; Character structure for the floraverse character generator
(require
racket/string
racket/list
"../../base-generation/random.rkt"
"../../base-generation/category.rkt"
"../../base-generation/distribution.rkt"
"database.rkt"
"calendar.rkt"
"motto.rkt"
"gender.rkt"
"sex.rkt"
"element.rkt"
"affinity.rkt"
"geography.rkt"
"species.rkt"
"../phonagen.rkt"
"trait.rkt"
"ability.rkt"
"../../lang/english.rkt"
)
(provide
; Generation of a character
character-generate
; Character accessors
character-father
character-mother
character-birthday
character-motto
character-natures
character-gender
character-sex
character-affinity
character-birth-place
character-living-place
character-species-genes
character-species-start
character-species-imited
character-name
character-traits
character-abilities
character-size
character-weight
)
; Character structure
(struct character
(internal-natures-dict ; Dictionnary of natures - for internal use only
internal-traits-word-dict ; Dictionnary of words used in traits - for internal use only
internal-language ; Language used for generating the name - for internal use only
internal-traits ; Dictionnary of traits - for internal use only
father ; Father of character
mother ; Mother of character
birthday ; Date of birth, as a date+sign
motto ; Motto
natures ; List of main natures
sex ; Sex of character
gender ; Gender of character
affinity ; Elemental affinity of character
birth-place ; Place of birth
living-place ; Place where the character lives
species-genes ; List of species genes of the character
species-start ; The starting point for species generation
species-imited ; For mimic species, the imited species
name ; Name of the character
traits ; List of traits
abilities ; List of abilities
size ; Size of character.
weight ; Weight of character.
))
; Full reduction of affinity: merge all the affinity with the same elements into one, to make a gene-like list with element-name + count
(define (affinity-reduce-for-ability affinity)
(define composition (make-hash))
(for-each
(lambda (elem)
(hash-set!
composition
(signed-element-name elem)
(+ (signed-element-count elem)
(hash-ref composition (signed-element-name elem) 0))))
(affinity-composition affinity))
(hash->list composition))
; Generate a gene list
(define (generate-species-genes starting-species imited-species)
(define mimic? (species-mimic? starting-species))
(define mimic-genes-used? (species-mimic-genes-used? starting-species))
(define compatibility-name (species-compatibility-name imited-species))
(cond
; Mimic using the genes of the mimic species and not those of the imited species
( (and mimic? mimic-genes-used?)
(list (cons (species-compatibility-name starting-species) 100)))
; Crossbreed case
( (list? compatibility-name)
(map
(lambda (x) (cons x 50))
compatibility-name))
; Non-crossbreed
( #t
(list (cons compatibility-name 100)))))
; Size generator
(define (generate-size)
(let ((numeric (random:normal)))
(cond
((> numeric 1.35) "huge")
((> numeric 0.67) "large")
((> numeric -0.67) #f)
((> numeric -1.35) "small")
(#t "tiny"))))
; Weight generator
(define (generate-weight)
(let ((numeric (random:normal)))
(cond
((> numeric 1.35) "obese")
((> numeric 0.67) "fat")
((> numeric 0.15) "plump")
((> numeric -0.15) #f)
((> numeric -0.67) "slim")
((> numeric -1.35) "thin")
(#t "emaciated"))))
; Sex generation
(define (generate-sex-name floragen genes)
(distribution-generate
(distribution-linear-combination
(map
(lambda (x)
(cons
(species-sex-distribution
(species-database-get (flora-generator-species floragen) (car x)))
(cdr x)))
genes))))
; Generate a place
(define (generate-place floragen species (place #f))
(cond
( (and place (< (random 10) 6))
place)
( (or (species-restricted-to-endemic-areas? species) (< (random 10) 6))
(geography-random-subplace
(flora-generator-geography floragen)
(random:from-list (species-endemic-in species))))
(#t
(geography-random-subplace (flora-generator-geography floragen) "Floraverse"))
))
; Generate an affinity
(define (generate-affinity floragen genes)
(make-affinity
(flora-generator-elements floragen)
(distribution-linear-combination
(map
(lambda (x)
(cons
(species-affinity-distribution
(species-database-get (flora-generator-species floragen) (car x)))
(cdr x)))
genes))))
; Generate the parent couple for a given species
; Return a pair of species, first is father, second is mother
(define (generate-parent-species floragen starting-species imited-species)
(define reproduction (species-reproduction starting-species))
;
(cond
; Artifacts are built => no parents
((eq? reproduction 'artifact)
(cons #f #f))
; Asexual reproduction => mother only, from the asexual-parent-species, sex is set to #f
((eq? reproduction 'asexual)
(define parent-species
(species-database-get
(flora-generator-species floragen)
(random:from-list (species-asexual-parent-species starting-species))))
(cons #f parent-species))
; Mimic using imited species genes - one of two possible case, so 50% to trigger
((and (species-mimic? starting-species)
(not (species-mimic-genes-used? starting-species)) (random:boolean))
; one of the parent is of the mimic species, the other is of the imited species
(if (random:boolean)
(cons imited-species starting-species)
(cons starting-species imited-species)))
; Crossbred species, case 1: parents are from the two compatible species
((and (random:boolean) (list? (species-compatibility-name starting-species)))
(define compat-name
(map
(lambda (x) (species-database-get (flora-generator-species floragen) x))
(species-compatibility-name starting-species)))
(if (random:boolean)
(cons (car compat-name) (cadr compat-name))
(cons (cadr compat-name) (car compat-name))))
; Sexual reproduction, case 1 / Crossbred species, case 2: parents are both of the same species
((or (random:boolean) (list? (species-compatibility-name starting-species)))
(cons starting-species starting-species))
; Sexual reproduction, case 2: parents are of compatible species
(#t
; All possible parents
(define all-possible-parents (species-database-all-possible-parents (flora-generator-species floragen) starting-species))
(cons
(random:from-list all-possible-parents)
(random:from-list all-possible-parents)))
))
; Combine genes from two list of genes
(define (combine-genes genome1 genome2)
; Distribution for genome1
(define d1 (apply distribution genome1))
(define d2 (apply distribution genome2))
; Generate 60 genes from each distribution, append, shuffle, and drop the 20 first elements
(define lst-genes
(list-tail
(shuffle
(append
(build-list 60 (lambda (_) (distribution-generate d1)))
(build-list 60 (lambda (_) (distribution-generate d2)))))
20))
; Simplify the list into a list of (species . count)
; Use a distribution to do this
(define d (distribution))
(for-each
(lambda (x)
(distribution-add-to! d x))
lst-genes)
(hash->list (distribution-items d)))
; Mother sex for a species
(define (mother-sex floragen sp)
; Generate from the sex distribution with only the sexes that are mother
(define d
(distribution-filter
(species-sex-distribution sp)
(lambda (x)
(sex-mother? (sexes-ref (flora-generator-sexes floragen) x)))))
; If d is empty, take a sex from the species-sex-distribution
(distribution-generate
(if (distribution-empty? d) (species-sex-distribution sp) d)))
; Father sex for a species
(define (father-sex floragen sp)
; Generate from the sex distribution with only the sexes that are father
(define d
(distribution-filter
(species-sex-distribution sp)
(lambda (x)
(define mother? (sex-mother? (sexes-ref (flora-generator-sexes floragen) x)))
(or (not mother?) (eq? 'maybe mother?)))))
; If d is empty, take a sex from the species-sex-distribution
(distribution-generate
(if (distribution-empty? d) (species-sex-distribution sp) d)))
; Generator
; Generate a character from a hash of constraints
(define (character-generate floragen [constraints (make-immutable-hash)])
(define asked-language (hash-ref constraints 'language #f))
(define starting-species
(or
(hash-ref constraints 'species #f)
(species-database-random (flora-generator-species floragen) species-generable-as-character?)))
(define mimic? (species-mimic? starting-species))
(define imited-species
(if mimic?
(species-database-random (flora-generator-species floragen) (species-mimic-method starting-species))
starting-species))
; Generate parents if needed
(define generate-ascendents? (< 0 (hash-ref constraints 'nb-ascendents 0)))
(define parent-species (if generate-ascendents?
(generate-parent-species floragen starting-species imited-species)
(cons #f #f)))
(define father (and (car parent-species)
(character-generate
floragen
(make-immutable-hash
`((species . ,(car parent-species))
(nb-ascendents . ,(- (hash-ref constraints 'nb-ascendents 1) 1))
(sex . ,(father-sex floragen (car parent-species)))
(language . ,asked-language)
)))))
(define mother (and (cdr parent-species)
(character-generate
floragen
(make-immutable-hash
`((species . ,(cdr parent-species))
(nb-ascendents . ,(- (hash-ref constraints 'nb-ascendents 1) 1))
(sex . ,(mother-sex floragen (cdr parent-species)))
(language . ,asked-language)
)))))
; Generate species genes
(define unsorted-genes (or (and father mother (combine-genes (character-species-genes mother) (character-species-genes father)))
(generate-species-genes starting-species imited-species)))
(define species-genes (sort unsorted-genes (lambda (x y) (> (cdr x) (cdr y)))))
; Generate everything else
(define sex (sexes-ref
(flora-generator-sexes floragen)
(hash-ref
constraints
'sex
(generate-sex-name floragen species-genes))))
(define gender (genders-ref (flora-generator-genders floragen) (sex-generate-gender-name sex)))
(define birth-place (or (and father (random:boolean) (character-living-place father))
(and mother (random:boolean) (character-living-place mother))
(generate-place floragen starting-species)))
(define language (or (and asked-language (member asked-language (phonagen-generator-ids (flora-generator-name floragen))) asked-language)
(and father (random:boolean) (character-internal-language father))
(and mother (random:boolean) (character-internal-language mother))
(random:from-list (phonagen-generator-ids (flora-generator-name floragen)))))
(define nature-dict (random:hash-combine
(or (and father (character-internal-natures-dict father)) (make-hash))
(or (and mother (character-internal-natures-dict mother)) (make-hash))
(category-generate-dictionary (flora-generator-natures floragen))))
(define word-dict (random:hash-combine
(or (and father (character-internal-traits-word-dict father)) (make-hash))
(or (and mother (character-internal-traits-word-dict mother)) (make-hash))
(category-generate-dictionary (flora-generator-word-lists floragen))))
(define traits-dict (random:hash-combine
(or (and father (character-internal-traits father)) (make-hash))
(or (and mother (character-internal-traits mother)) (make-hash))
(traits-generate (flora-generator-traits floragen))))
(define affinity (generate-affinity floragen species-genes))
;
(character
; Internal nature dict
nature-dict
; Internal word dict
word-dict
; Internal language
language
; Internal traits dict
traits-dict
; Father
father
; Mother
mother
; Birthday
(calendar-generate-date (flora-generator-calendar floragen))
; Motto
(motto-generate (flora-generator-mottos floragen))
; Natures
(random:sublist (hash-values nature-dict) (+ 2 (random 3)))
; Sex
sex
; Gender
gender
; Affinity
affinity
; Birth place
birth-place
; Living place
(generate-place floragen starting-species birth-place)
; Species genes
species-genes
; Starting species
starting-species
; Imited species
imited-species
; Name
(phonagen-generate (flora-generator-name floragen) language)
; Traits
(map
(lambda (x)
(trait->string
x
gender
word-dict
(flora-generator-natures floragen)
(flora-generator-species floragen)
(flora-generator-geography floragen)))
(random:sublist (hash-values traits-dict) (+ 3 (random 3))))
; Abilities
(random:sublist
(ability-database-select
(flora-generator-abilities floragen)
species-genes
(affinity-reduce-for-ability affinity))
5)
; Size
(generate-size)
; Weight
(generate-weight)
))

View File

@ -0,0 +1,75 @@
#lang racket/base
; Floraverse generator database
(require
json
"../../base-generation/category.rkt"
"calendar.rkt"
"motto.rkt"
"gender.rkt"
"sex.rkt"
"element.rkt"
"geography.rkt"
"species.rkt"
"../phonagen.rkt"
"trait.rkt"
"ability.rkt"
)
(provide
; Build generator from a path containing the different json files
make-flora-generator
; Accessors to various parts of the generator
flora-generator-path
flora-generator-calendar
flora-generator-mottos
flora-generator-natures
flora-generator-word-lists
flora-generator-genders
flora-generator-sexes
flora-generator-elements
flora-generator-geography
flora-generator-species
flora-generator-name
flora-generator-traits
flora-generator-abilities
)
; Flora generator structure
(struct flora-generator
(path ; path to the database directory
calendar ; calendar informations, to generate dates
mottos ; Motto generator
natures ; Nature generator
word-lists ; Generator of words from lists
genders ; Dictionnary of genders
sexes ; Dictionnary of sexes
elements ; Dictionnary of magic elements
geography ; Geographical informations
species ; Species information
name ; Name generator
traits ; Traits generator
abilities ; Abilities information
))
; Helper to retrieve the files
(define (load-json-file prefix data-dir file-name)
(call-with-input-file (string-append prefix data-dir "/" file-name) read-json))
; Build flora generator from json files
(define (make-flora-generator prefix data-dir phonagen-file)
(flora-generator
data-dir
(jsexpr->calendar (load-json-file prefix data-dir "calendar.json"))
(jsexpr->motto-generator (load-json-file prefix data-dir "mottos.json"))
(jsexpr->category-generator (load-json-file prefix data-dir "natures.json"))
(jsexpr->category-generator (load-json-file prefix data-dir "word-lists.json"))
(jsexpr->genders (load-json-file prefix data-dir "genders.json"))
(jsexpr->sexes (load-json-file prefix data-dir "sexes.json"))
(jsexpr->elements (load-json-file prefix data-dir "elements.json"))
(jsexpr->geography (load-json-file prefix data-dir "geography.json"))
(jsexpr->species-database (load-json-file prefix data-dir "species.json"))
(jsexpr->phonagen (call-with-input-file (string-append prefix phonagen-file) read-json))
(jsexpr->traits-generator (load-json-file prefix data-dir "traits.json"))
(jsexpr->ability-database (load-json-file prefix data-dir "abilities.json"))
))

View File

@ -0,0 +1,79 @@
#lang racket/base
; Model and generation of elemental affinities
(provide
; elements collection functions
jsexpr->elements
elements-ref elements-combination-table
; Element structure accessors
element-name element-affinity-description
element-nouns element-adjectives
combined-element? combined-element-components
)
; Element structure
(struct element
(name
affinity-description
nouns
adjectives))
; Combined element structure
(struct combined-element element
(components))
; Construction of an Element from a json expression
(define (jsexpr->element js)
(element
(hash-ref js 'name)
(hash-ref js 'affinity-description)
(hash-ref js 'related-nouns)
(hash-ref js 'related-adjectives)))
; Construction of a Combined Element from a json expression
(define (jsexpr->combined-element js)
(define result (hash-ref js 'result))
(combined-element
(hash-ref result 'name)
(hash-ref result 'affinity-description)
(hash-ref result 'related-nouns)
(hash-ref result 'related-adjectives)
(hash-ref js 'components)))
; A collection of elements
(struct elements
(none
primary
secondary
tertiary
all
combination-table))
; From json
(define (jsexpr->elements js)
(define ret
(elements
(jsexpr->element (hash-ref js 'none))
(map jsexpr->element (hash-ref js 'primary))
(map jsexpr->combined-element (hash-ref js 'secondary))
(map jsexpr->combined-element (hash-ref js 'tertiary))
(make-hash)
(make-hash)))
; Populate the table of all elements
(define (add-element el)
(hash-set! (elements-all ret) (element-name el) el))
(add-element (elements-none ret))
(for-each add-element (elements-primary ret))
(for-each add-element (elements-secondary ret))
(for-each add-element (elements-tertiary ret))
; Populate the table of combinations
(define (add-combinations el)
(hash-set! (elements-combination-table ret) (combined-element-components el) (element-name el)))
(for-each add-combinations (elements-secondary ret))
(for-each add-combinations (elements-tertiary ret))
;
ret)
; Get an element from its name
(define (elements-ref elms name)
(hash-ref (elements-all elms) name))

View File

@ -0,0 +1,46 @@
#lang racket/base
; Model and generation of genders
(provide
; Gender collection functions
jsexpr->genders
genders-ref
; Gender structure accessors
gender-name gender-title gender-title-abbreviation
gender-subject gender-object gender-genitive gender-reflexive
gender-plural?
)
; Gender structure
(struct gender
(name
title
title-abbreviation
subject
object
genitive
reflexive
plural?))
; Gender collection constructor
(define (jsexpr->genders js)
(define ret (make-hash))
(map
(lambda (x)
(define name (hash-ref x 'name))
(hash-set! ret name
(gender
name
(hash-ref x 'title)
(hash-ref x 'title-abbreviation)
(hash-ref x 'subject)
(hash-ref x 'object)
(hash-ref x 'genitive)
(hash-ref x 'reflexive)
(hash-ref x 'plural?))))
js)
ret)
; Get a gender by its name
(define (genders-ref gnd name)
(hash-ref gnd name))

View File

@ -0,0 +1,113 @@
#lang racket/base
; Model and generation of locations like living places or birth places
; Dependencies
(require
"../../collection/tree.rkt"
"../../base-generation/random.rkt")
; Exported functions
(provide
; Location type
location-type-name
location-type-preposition-in
location-type-preposition-near
; Locations
place-name
place-reference-link
place-type
; Geography structure
jsexpr->geography
geography-get-place
geography-random-subplace
)
; Structure of type of location
(struct location-type
(name
preposition-in
preposition-near))
; Structure of a location
(struct place
(name
reference-link
type
restricted?))
; Geography structure
(struct geography
(location-types ; table of location types
place-tree)) ; tree of places
; Construction from JSON
; Location type
(define (jsexpr->location-type js)
(location-type
(hash-ref js 'name)
(hash-ref js 'preposition-in)
(hash-ref js 'preposition-near)))
; Location
(define (jsexpr->place js location-types (parent-reference-link #f))
(place
(hash-ref js 'name)
(hash-ref js 'reference-link parent-reference-link)
(hash-ref location-types (hash-ref js 'type))
(hash-ref js 'restricted? #f)))
; Tree node
(define (jsexpr->place-node js location-types (parent-reference-link #f))
(define value (jsexpr->place js location-types parent-reference-link))
(tree
(hash-ref js 'name)
value
(map
(lambda (x)
(jsexpr->place-node x location-types (place-reference-link value)))
(hash-ref js 'locations (list)))))
; Geography
(define (jsexpr->geography js)
; Fill location types
(define location-types (make-hash))
(for-each
(lambda (x)
(define type (jsexpr->location-type x))
(hash-set! location-types (location-type-name type) type))
(hash-ref js 'location-types))
;
(geography
location-types
(jsexpr->place-node (hash-ref js 'places) location-types)
))
; Find a place by its name
; Returns the path to the place or #f if it does not exists
(define (geography-find-place geo loc-name)
(tree-find-path
(geography-place-tree geo)
(lambda (loc)
(equal? loc-name (place-name loc)))))
; Get a place from its name or #f if it does not exists
(define (geography-get-place geo loc-name)
(tree-ref
(geography-place-tree geo)
(geography-find-place geo loc-name)))
; Get a subplace of a place from its name
(define (geography-random-subplace geo loc-name)
(define (next-place node)
(define children
(filter
(lambda (x) (not (place-restricted? (tree-value x))))
(tree-children node)))
(if (null? children)
(tree-value node)
(next-place (random:from-list children))))
(next-place
(tree-node
(geography-place-tree geo)
(geography-find-place geo loc-name))))

View File

@ -0,0 +1,31 @@
#lang racket/base
(require
"../../base-generation/random.rkt"
"../../lang/case.rkt")
; Model and generation of mottos
(provide
jsexpr->motto-generator motto-generate)
; Motto generator structure
(struct motto-generator
(prefix
prefixable
unprefixable))
; Build a motto generator structure from a json expression
(define (jsexpr->motto-generator js)
(motto-generator
(hash-ref js 'prefix)
(hash-ref js 'prefixable)
(hash-ref js 'unprefixable)))
; Generate a motto
(define (motto-generate gen)
(if (random:boolean)
(random:from-list (motto-generator-unprefixable gen))
(case:upcase-1st
(string-append
(random:from-list (motto-generator-prefix gen))
(random:from-list (motto-generator-prefixable gen))))))

View File

@ -0,0 +1,46 @@
#lang racket/base
(require
"../../base-generation/distribution.rkt")
; Model and generation of sexes
(provide
; Sexes collection functions
jsexpr->sexes
sexes-ref
; Sex structure accessors
sex-name sex-context sex-mother?
; Generation of gender
sex-generate-gender-name
)
; Sexes structure
(struct sex
(name
context
mother?
gender-distribution))
; Generation of gender from sex
(define (sex-generate-gender-name sx)
(distribution-generate (sex-gender-distribution sx)))
; Deserialisation from a json object
(define (jsexpr->sexes js)
(define ret (make-hash))
(map
(lambda (x)
(define name (hash-ref x 'name))
(define mother? (hash-ref x 'mother?))
(hash-set! ret name
(sex
name
(hash-ref x 'context)
(if (equal? mother? "maybe") 'maybe mother?)
(jsexpr->distribution/object (hash-ref x 'gender-distribution) #:translator symbol->string))))
js)
ret)
; Retrieval from the name
(define (sexes-ref sx name)
(hash-ref sx name))

View File

@ -0,0 +1,282 @@
#lang racket/base
; Model and generation of species
(require
racket/list
racket/set
"../../base-generation/distribution.rkt")
(provide
; Field of species
species?
species-name species-compatibility-name species-reference-link species-endemic-in species-restricted-to-endemic-areas?
species-affinity-distribution species-sex-distribution species-reproduction
species-asexual-parent-species species-generable-as-character? species-citizen?
species-pet? species-wild? species-vegetal? species-mimic? species-mimic-method species-mimic-genes-used?
; Functions on a species database structure
jsexpr->species-database
species-database-get
species-database-all
species-database-random
species-database-find-crossbred-name
species-database-all-possible-parents
)
; Species structure
(struct species
(name ; Name of species
compatibility-name ; Name of the species to use when checking in the compatibility tables. A list of names for crossbreed species.
reference-link ; link to reference
endemic-in ; List of the names of areas the species is endemic
restricted-to-endemic-areas? ; Indicate if the species only lives in endemic areas
affinity-distribution ; Distribution of affinities
sex-distribution ; Distribution of sexes
reproduction ; type of reproduction, among: sexual, asexual, artifact
asexual-parent-species ; List of the species names that can give birth to this species, in case of asexual reproduction
generable-as-character? ; Indicate if the species can be choosen in generation of characters
citizen? ; Indicate if the species is considered as citizen
pet? ; Indicate if the species is considered as pet
wild? ; Indicate if the species is considered as wild animal
vegetal? ; Indicate if the species is vegetal
mimic? ; Indicate if the species is a mimic species. An associated species must be generated to generate a character
mimic-method ; Indicate how the associated species is choosen
mimic-genes-used? ; If true, a character of a mimic species use the mimic species in its genes. If false, the character uses the imitated species genes
))
; Species database structure
(struct species-database
(table
crossbreed-table))
; Key to corresponding method - used for building mimic methods
(define keys->method
(make-immutable-hash
(list
(cons 'name species-name)
(cons 'reference-link species-reference-link)
(cons 'endemic-in species-endemic-in)
(cons 'restricted-to-endemic-areas? species-restricted-to-endemic-areas?)
; (cons 'affinity species-affinity-distribution)
; (cons 'sex species-sex-distribution)
(cons 'reproduction species-reproduction)
(cons 'asexual-parent-species species-asexual-parent-species)
(cons 'generable-as-character? species-generable-as-character?)
(cons 'citizen? species-citizen?)
(cons 'pet? species-pet?)
(cons 'wild? species-wild?)
(cons 'vegetal? species-vegetal?)
(cons 'mimic? species-mimic?)
; (cons 'mimic-method species-mimic-method)
(cons 'mimic-genes-used? species-mimic-genes-used?))))
; Mimic method builder
; Returns a function that can be given to species-database-random as choosing predicate
(define (jsexpr->mimic-method js (base #f))
(define mimic-method (hash-ref js 'species #f))
(cond
(mimic-method
; Create a species predicate from a species function and expected value
(define (make-species-pred method value)
(lambda (sp)
(equal? (method sp) value)))
; Create a species predicate from a species function and a list of excluded values
; Used when a {"not": [...]} is used as value
(define (make-species-excluding-pred method lst-values)
(lambda (sp)
(not (member (method sp) lst-values))))
; List of predicates to apply
(define lst-pred
(hash-map
mimic-method
(lambda (k v)
(cond
; Special case for reproduction as the string values must be converted to symbols
( (and (eq? 'reproduction k)
(hash? v) (hash-has-key? v 'not))
(make-species-excluding-pred (hash-ref keys->method k) (map string->symbol (hash-ref v 'not))))
( (eq? 'reproduction k)
(make-species-pred (hash-ref keys->method k) (string->symbol v)))
; Other cases
( (and (hash? v) (hash-has-key? v 'not))
(make-species-excluding-pred (hash-ref keys->method k) (hash-ref v 'not)))
( #t
(make-species-pred (hash-ref keys->method k) v))))))
; Built predicate
(lambda (sp)
(andmap
(lambda (fun)
(fun sp))
lst-pred))
)
(#t
base)))
; Create a species from a json expression
(define (jsexpr->species js (base #f))
(species
(hash-ref js 'name (and base (species-name base)))
(or (and base (species-compatibility-name base)) (hash-ref js 'name #f)) ; The compatibility name is the first species name encountered in the hierarchy of varieties
(hash-ref js 'reference-link (and base (species-reference-link base)))
(hash-ref js 'endemic-in (and base (species-endemic-in base)))
(hash-ref js 'restricted-to-endemic-areas? (and base (species-restricted-to-endemic-areas? base)))
(if (hash-has-key? js 'affinity)
(jsexpr->distribution/object (hash-ref js 'affinity) #:translator symbol->string)
(and base (species-affinity-distribution base)))
(if (hash-has-key? js 'sex)
(jsexpr->distribution/object (hash-ref js 'sex) #:translator symbol->string)
(and base (species-sex-distribution base)))
(if (hash-has-key? js 'reproduction)
(string->symbol (hash-ref js 'reproduction))
(and base (species-reproduction base)))
(hash-ref js 'asexual-parent-species (and base (species-asexual-parent-species base)))
(hash-ref js 'generable-as-character? (and base (species-generable-as-character? base)))
(hash-ref js 'citizen? (and base (species-citizen? base)))
(hash-ref js 'pet? (and base (species-pet? base)))
(hash-ref js 'wild? (and base (species-wild? base)))
(hash-ref js 'vegetal? (and base (species-vegetal? base)))
(hash-ref js 'mimic? (and base (species-mimic? base)))
(if (hash-has-key? js 'mimic-method)
(jsexpr->mimic-method (hash-ref js 'mimic-method) (and base (species-mimic-method base)))
(and base (species-mimic-method base)))
(hash-ref js 'mimic-genes-used? (and base (species-mimic-genes-used? base)))
))
; Create a crossbreed species from a json expression and the species table
(define (jsexpr->crossbreed js species-table)
(define (or-combine func a b)
(or (func a) (func b)))
(define (and-combine func a b)
(and (func a) (func b)))
(define parents (hash-ref js 'parents))
(define parent-1 (hash-ref species-table (car parents)))
(define parent-2 (hash-ref species-table (cadr parents)))
(define child-js (hash-ref js 'child))
(define child-species-name (hash-ref child-js 'name #f)) ; Note: if no name is given, name is set to false
; Returned value
(define child
(species
child-species-name
(list (species-compatibility-name parent-1) (species-compatibility-name parent-2)) ; Species compatibility is of both parents
(hash-ref child-js 'reference-link (or-combine species-reference-link parent-1 parent-2))
(hash-ref child-js 'endemic-in (append (species-endemic-in parent-1) (species-endemic-in parent-2)))
(hash-ref child-js 'restricted-to-endemic-areas? (or-combine species-restricted-to-endemic-areas? parent-1 parent-2))
(if (hash-has-key? child-js 'affinity)
(jsexpr->distribution/object (hash-ref child-js 'affinity) #:translator symbol->string)
(distribution-linear-combination
(list
(cons (species-affinity-distribution parent-1) 1)
(cons (species-affinity-distribution parent-2) 1))))
(if (hash-has-key? child-js 'sex)
(jsexpr->distribution/object (hash-ref child-js 'sex) #:translator symbol->string)
(distribution-linear-combination
(list
(cons (species-sex-distribution parent-1) 1)
(cons (species-sex-distribution parent-2) 1))))
'sexual ; Sexual reproduction is implied by the crossbreed
(list) ; asexual-parent-species is only applicable to asexual reproduction
(hash-ref child-js 'generable-as-character? (or-combine species-generable-as-character? parent-1 parent-2))
(hash-ref child-js 'citizen? (or-combine species-citizen? parent-1 parent-2))
(hash-ref child-js 'pet? (or-combine species-pet? parent-1 parent-2))
(hash-ref child-js 'wild? (or-combine species-wild? parent-1 parent-2))
(hash-ref child-js 'vegetal? (or-combine species-vegetal? parent-1 parent-2))
(hash-ref child-js 'mimic? (or-combine species-mimic? parent-1 parent-2))
(if (hash-has-key? js 'mimic-method)
(jsexpr->mimic-method (hash-ref js 'mimic-method) (or-combine species-mimic-method parent-1 parent-2))
(or-combine species-mimic-method parent-1 parent-2))
(hash-ref child-js 'mimic-genes-used? (and-combine species-mimic-genes-used? parent-1 parent-2))
))
; Add the crossbreed species to the table of species if it has a name and that the name is not already present in the species list
(when (and child-species-name (not (hash-has-key? species-table child-species-name)))
(hash-set! species-table child-species-name child))
; Return the species
child)
; Create a species database
(define (jsexpr->species-database js-species)
; Make the species table
(define species-defaults (jsexpr->species (hash-ref js-species 'defaults)))
(define species-table (make-hash))
(define (process-species js base)
(define sp (jsexpr->species js base))
(hash-set! species-table (species-name sp) sp)
(map
(lambda (x) (process-species x sp))
(hash-ref js 'varieties (list))))
(map
(lambda (x) (process-species x species-defaults))
(hash-ref js-species 'species))
; Make the compatiblity table and crossbreed species if they have a name
(define crossbreed-table
(make-immutable-hash
(map
(lambda (x)
(cons
(list->set (hash-ref x 'parents))
(jsexpr->crossbreed x species-table)))
(hash-ref js-species 'crossbreeds))))
; Return
(species-database
species-table
crossbreed-table))
; Get a species from its name
(define (species-database-get spdb name)
(hash-ref (species-database-table spdb) name #f))
; Get all species satisfying a predicate
; Return an empty list if no species satisfy the predicate
(define (species-database-all spdb (pred (lambda (sp) #t)))
(filter pred (hash-values (species-database-table spdb))))
; Get a random species satisfying a predicate
; Return #f if no species satisfy the predicate
(define (species-database-random spdb (pred (lambda (sp) #t)))
(define lst (shuffle (species-database-all spdb pred)))
(and (not (null? lst)) (car lst)))
; Get a crossbred name from a list of species names
; Return the input if no name is found
(define (species-database-find-crossbred-name spdb lst-names)
(define crossbred
(hash-ref
(species-database-crossbreed-table spdb)
(list->set lst-names)
#f))
(or
(and crossbred (species-name crossbred))
lst-names))
; Get all the possible parent species from a given species
; Based on the compatibility-name, return a list
(define (species-database-all-possible-parents spdb sp)
; Get the species
(define compat-name (species-compatibility-name sp))
;
(cond
; empty list if the species is unknown
((not sp) '())
; If a crossbread, combine the compatibilities from all base species
((list? compat-name)
(apply
append
(map
(lambda (x)
(species-database-all-possible-parents spdb (species-database-get spdb x)))
(species-compatibility-name sp))))
; Else => base species
(#t
; All the species with the same compatibility name
(define same-compat
(species-database-all
spdb
(lambda (s) (equal? compat-name (species-compatibility-name s)))))
; All the crossbred containing the compatibility-name
(define cross-compat
(map
cdr
(filter
(lambda (x) (set-member? (car x) compat-name))
(hash->list (species-database-crossbreed-table spdb)))))
; Append the two lists
(append same-compat cross-compat))))

View File

@ -0,0 +1,93 @@
#lang racket/base
; Model and generation of traits
(require
racket/string
"../../base-generation/category.rkt"
"../../lang/english.rkt"
"../../lang/case.rkt"
"gender.rkt"
"species.rkt"
"geography.rkt")
(provide
jsexpr->traits-generator
traits-generate
trait->string)
; Internally, it's a category generator
(struct traits-generator
(categories))
(define (jsexpr->traits-generator js)
(traits-generator
(jsexpr->category-generator js)))
(define (traits-generate gen)
(category-generate-dictionary (traits-generator-categories gen)))
; Return the corresponding accessor of gender
(define (gender-accessor name)
(case (string->symbol name)
((title) gender-title)
((title-abbreviation) gender-title-abbreviation)
((subject) gender-subject)
((object) gender-object)
((genitive) gender-genitive)
((reflexive) gender-reflexive)
(else (error "Invalid gender attribute: " name))))
; Return the corresponding predicate of species
(define (species-predicate name)
(case (string->symbol name)
((restricted-to-endemic-areas?) species-restricted-to-endemic-areas?)
((generable-as-character?) species-generable-as-character?)
((citizen?) species-citizen?)
((pet?) species-pet?)
((wild?) species-wild?)
((vegetal?) species-vegetal?)
((mimic?) species-mimic?)
((mimic-genes-used?) species-mimic-genes-used?)
(else (error "Invalid species predicate: " name))))
; Interpolate a trait into a string
; Each trait is a list of tokens
; The argument list is:
; trait: the trait to interpolate
; gender: the gender used to interpolate
; wordlist: a dictionary of words to use in interpolation
; nature-generator: a category generator
; species-database: the database of species
; geography-database: the database of geography loacations
(define (trait->string trait gender wordlist nature-generator species-database geography-database)
; Interpret a token
(define (interpret-token token)
(cond
((string? token)
token)
((and (hash? token) (hash-has-key? token 'gender))
((gender-accessor (hash-ref token 'gender)) gender))
((and (hash? token) (hash-has-key? token 'verb))
(english:3rd-person-of (hash-ref token 'verb) (gender-plural? gender)))
((and (hash? token) (hash-has-key? token 'undefined-article))
(english:with-undefined-article (interpret-token (hash-ref token 'undefined-article))))
((and (hash? token) (hash-has-key? token 'plural))
(english:plural-of (interpret-token (hash-ref token 'plural))))
((and (hash? token) (hash-has-key? token 'word))
(hash-ref wordlist (string->symbol (hash-ref token 'word))))
((and (hash? token) (hash-has-key? token 'species))
(species-name (species-database-random species-database (species-predicate (hash-ref token 'species)))))
((and (hash? token) (hash-has-key? token 'nature))
(category-generate nature-generator))
((and (hash? token) (hash-has-key? token 'location))
(place-name (geography-random-subplace geography-database "Floraverse")))
(#t
(error "Invalid token: " token))
))
(string-append
(case:upcase-1st
(string-join
(map interpret-token trait)
" "))
"."))

265
src/generators/phonagen.rkt Normal file
View File

@ -0,0 +1,265 @@
#lang racket/base
; Phonagen generator - Racket implementation
; Dependencies
(require
racket/string
racket/list
"../base-generation/random.rkt"
"../base-generation/markov.rkt"
"../base-generation/distribution.rkt"
)
; Exported functions
(provide
; Generator
jsexpr->phonagen
phonagen-generate
phonagen-generator-ids
; Word descriptions
word-transcriptions
word-pronounciation
word-native-transcription
word-latin-transcription
)
; Phoneme structure
(struct phoneme
(id ; Id of phoneme
description ; Description
transcriptions ; Transcriptions of phoneme
))
; Create a phoneme from a json description
(define (jsexpr->phoneme js)
(phoneme
(string->symbol (hash-ref js 'id))
(hash-ref js 'description)
; Transcriptions: the js object minus 'id and 'description
(hash-remove
(hash-remove
js
'id)
'description)
))
; Get a transcription of the phoneme from its id
(define (phoneme-transcription-get phon tr)
(hash-ref (phoneme-transcriptions phon) tr))
; Phonology
(struct phonology
(id ; Id of phonology
description ; Description
transcriptions ; Transcriptions
main-transcription ; Main transcription
phonemes ; Phonemes
))
; Create a phonology from a json description
(define (jsexpr->phonology js)
(phonology
(string->symbol (hash-ref js 'id))
(hash-ref js 'description)
(map string->symbol (hash-ref js 'transcriptions))
(string->symbol (hash-ref js 'main-transcription))
; Phonemes
(make-immutable-hash
(map
(lambda (x)
(define phonm (jsexpr->phoneme x))
(cons (phoneme-id phonm) phonm))
(hash-ref js 'entries)))
))
; Get a phoneme from a phonology from its id
(define (phonology-get phon id)
(hash-ref (phonology-phonemes phon) id))
; Generator (chain-based)
(struct chain-generator
(id ; Id of generator
description ; Description
phonology ; Phonology used
chains ; Markov Generator
))
; Create a chain-generator from a json description
(define (jsexpr->chain-generator js)
(define chains (make-markov (hash-ref js 'order)))
(define (->symbol x) (and (non-empty-string? x) (string->symbol x)))
(for-each
(lambda (chain)
(define input (map ->symbol (hash-ref chain 'input)))
(for-each
(lambda (output)
(markov-add-next-item!
chains
input
(->symbol (hash-ref output 'value))
(hash-ref output 'occurences)))
(hash-ref chain 'possible-outputs)))
(hash-ref js 'chains))
(chain-generator
(string->symbol (hash-ref js 'id))
(hash-ref js 'description)
(string->symbol (hash-ref js 'phonology))
chains))
; Generate a list of phoneme id from a chain-generator
(define (chain-generator-generate gen)
(markov-generate (chain-generator-chains gen)))
; Generator (rule-based)
(struct rule-generator
(id ; Id of generator
description ; Description
phonology ; Phonology used
rules ; Rules set
))
; Create a rule-generator from a json description
(define (jsexpr->rule-generator js)
(rule-generator
(string->symbol (hash-ref js 'id))
(hash-ref js 'description)
(string->symbol (hash-ref js 'phonology))
; Rules
(make-immutable-hash
(map
(lambda (rule)
(define dist (make-distribution))
(for-each
(lambda (x)
(distribution-add-to!
dist
(map string->symbol (hash-ref x 'pattern))
(hash-ref x 'occurences)))
(hash-ref rule 'distribution))
(cons (string->symbol (hash-ref rule 'id)) dist))
(hash-ref js 'rules)))
))
; Generate a list of phoneme id from a rule-generator
(define (rule-generator-generate gen)
(define (replace pattern-id)
(if (hash-has-key? (rule-generator-rules gen) pattern-id)
(map replace (distribution-generate (hash-ref (rule-generator-rules gen) pattern-id)))
pattern-id))
(flatten (replace 'word)))
; Make a generator, according to the corresponding type
(define (jsexpr->generator js)
(case (hash-ref js 'type)
(("rules") (jsexpr->rule-generator js))
(("chains") (jsexpr->chain-generator js))
(else (error "Unsupported generator: " (hash-ref js 'type)))))
; Get the generator id from a generator
(define (generator-id gen)
(cond
((rule-generator? gen) (rule-generator-id gen))
((chain-generator? gen) (chain-generator-id gen))
(else (error "Not a generator: " gen))))
; Get the phonology from a generator
(define (generator-phonology gen)
(cond
((rule-generator? gen) (rule-generator-phonology gen))
((chain-generator? gen) (chain-generator-phonology gen))
(else (error "Not a generator: " gen))))
; Generate a list of phoneme id from a generator
(define (generator-generate gen)
(cond
((rule-generator? gen) (rule-generator-generate gen))
((chain-generator? gen) (chain-generator-generate gen))
(else (error "Not a generator: " gen))))
; Word structure
(struct word
(phonemes ; List of phonemes of the words
main-transcription ; Id of the main transcription
transcriptions ; Map of transcriptions
))
; Generate a word from a generator and a phonology
(define (generate-word gen phon)
; List of phonemes of the word
(define phonemes
(map
(lambda (x)
(phonology-get phon x))
(generator-generate gen)))
; Return the word
(word
phonemes
(phonology-main-transcription phon)
(make-immutable-hash
(map
(lambda (tr)
(cons
tr
(string-join
(map
(lambda (x)
(phoneme-transcription-get x tr))
phonemes)
"")))
(phonology-transcriptions phon)))
))
; Word->string function
(define (word->string wrd tr)
(hash-ref (word-transcriptions wrd) tr))
; Get pronounciation string
(define (word-pronounciation wrd)
(string-normalize-nfc (word->string wrd 'phoneme)))
; Get native transcription string
(define (word-native-transcription wrd)
(word->string wrd (word-main-transcription wrd)))
; If there is a 'latin language, use it, else return the main transcription
(define (word-latin-transcription wrd)
(if (hash-has-key? (word-transcriptions wrd) 'latin)
(word->string wrd 'latin)
(word-native-transcription wrd)))
; Phonagen generator
(struct phonagen
(phonologies
generators))
; Create a phonagen generator from a json expression
(define (jsexpr->phonagen js)
(phonagen
; Phonologies
(make-immutable-hash
(map
(lambda (x)
(define phon (jsexpr->phonology x))
(cons (phonology-id phon) phon))
(hash-ref js 'phonologies)))
; Generators
(make-immutable-hash
(map
(lambda (x)
(define gen (jsexpr->generator x))
(cons (generator-id gen) gen))
(hash-ref js 'generators)))
))
; Generate a word from a phonagen file, with the generator optionally given
(define (phonagen-generate phgen [genid #f])
(define gen (or (and genid (hash-ref (phonagen-generators phgen) genid))
(cdr (random:from-hash (phonagen-generators phgen)))))
(generate-word
gen
(hash-ref (phonagen-phonologies phgen) (generator-phonology gen))))
; Get the list of generator ids
(define (phonagen-generator-ids phgen)
(hash-keys (phonagen-generators phgen)))

17
src/lang/case.rkt Normal file
View File

@ -0,0 +1,17 @@
#lang racket/base
; Utilities to manipulate case in strings
(require
racket/string)
(provide
case:upcase-1st)
; Capitalize the first character of a string
; Similar to string-titlecase, but only on the first word
(define (case:upcase-1st str)
(if (non-empty-string? str)
(string-append
(string-upcase (substring str 0 1))
(substring str 1))
""))

104
src/lang/english.rkt Normal file
View File

@ -0,0 +1,104 @@
#lang racket/base
(require
racket/string
racket/list
(prefix-in srfi13: srfi/13))
; Various functions for formatting words and sentences written in English.
(provide
english:ordinal
english:3rd-person-of
english:undefined-article
english:with-undefined-article
english:plural-of
english:add-commas
)
; Get the ordinal of a given number
(define (english:ordinal N)
(cond
((and (not (eq? 11 (modulo N 100))) (eq? 1 (modulo N 10))) (string-append (number->string N) "st"))
((and (not (eq? 12 (modulo N 100))) (eq? 2 (modulo N 10))) (string-append (number->string N) "nd"))
((and (not (eq? 13 (modulo N 100))) (eq? 3 (modulo N 10))) (string-append (number->string N) "rd"))
(#t (string-append (number->string N) "th"))))
; Conjugate a verb at the 3rd person (present mode)
(define (english:3rd-person-of verb plural?)
(cond
((equal? "be" verb) (if plural? "are" "is"))
((equal? "have" verb) (if plural? "have" "has"))
(plural? verb)
; Only singular cases
((or (string-suffix? verb "s") (string-suffix? verb "sh") (string-suffix? verb "ch")
(string-suffix? verb "x") (string-suffix? verb "o"))
(string-append verb "es"))
((or (string-suffix? verb "ay") (string-suffix? verb "ey") (string-suffix? verb "iy")
(string-suffix? verb "oy") (string-suffix? verb "uy"))
(string-append verb "s"))
((string-suffix? verb "y")
(string-append (srfi13:string-drop-right verb 1) "ies"))
;;
(#t (string-append verb "s"))
))
; Get the undefined article that would prefix a given string
(define (english:undefined-article str)
(define initial (string-downcase (substring str 0 1)))
(if (string-contains? "aeiou" initial)
"an"
"a"))
; Return a string prefixed with the corresponding undefined article
(define (english:with-undefined-article str)
(string-append (english:undefined-article str) " " str))
; Return the plural variant of a string
(define (english:plural-of str)
(cond
((string-suffix? str "mouse")
(string-append (srfi13:string-drop-right str 5) "mice"))
((string-suffix? str "louse")
(string-append (srfi13:string-drop-right str 5) "lice"))
((string-suffix? str "goose")
(string-append (srfi13:string-drop-right str 5) "geese"))
((string-suffix? str "foot")
(string-append (srfi13:string-drop-right str 4) "feet"))
((string-suffix? str "tooth")
(string-append (srfi13:string-drop-right str 5) "teeth"))
((string-suffix? str "man")
(string-append (srfi13:string-drop-right str 3) "men"))
((or (string-suffix? str "s") (string-suffix? str "sh") (string-suffix? str "ch")
(string-suffix? str "x") (string-suffix? str "o"))
(string-append str "es"))
((or (string-suffix? str "ay") (string-suffix? str "ey") (string-suffix? str "iy")
(string-suffix? str "oy") (string-suffix? str "uy"))
(string-append str "s"))
((string-suffix? str "y")
(string-append (srfi13:string-drop-right str 1) "ies"))
((string-suffix? str "ff")
(string-append (srfi13:string-drop-right str 2) "ves"))
((string-suffix? str "f")
(string-append (srfi13:string-drop-right str 1) "ves"))
((string-suffix? str "fe")
(string-append (srfi13:string-drop-right str 2) "ves"))
(#t
(string-append str "s"))
))
; Add comma between elements of a list, with the last element introduced by "and"
(define (english:add-commas lst)
(cond
; Nothing to add => return the list as is
( (>= 1 (length lst))
lst)
; Add commas
(#t
(append
(foldl
(lambda (x result)
(append result (list ", " x)))
(list (car lst))
(drop-right (cdr lst) 1))
(list ", and " (last lst)))
)))

205
src/pages/floraverse.rkt Normal file
View File

@ -0,0 +1,205 @@
#lang racket/base
; Floraverse-related pages
; Includes the floraverse character generator
(require
json
racket/string
racket/date
"templates.rkt"
"../generators/flora-character.rkt"
"../generators/flora/database.rkt"
"../generators/flora/calendar.rkt"
"../generators/flora/species.rkt"
"../generators/phonagen.rkt"
"../webcontainer/weblets.rkt"
"../webcontainer/weblet-parameter.rkt"
"../lang/english.rkt"
"../lang/case.rkt")
(provide
pages:not-found
pages:floraverse
pages:floraverse-character-generator-about
pages:floraverse-character-generator
pages:floraverse-character-generator-tweet
pages:floraverse-calendar)
; Not found page
(define pages:not-found
(pages:template
#:title "Not found"
#:content '(article (p "Sorry, there is nothing here."))
#:error-code 404))
; The generator
(define *FloraGenDataDir* "/data/flora-generator-data")
(define *FloraGen* (make-flora-generator "./static" *FloraGenDataDir* "/data/phonagen.json"))
; Floraverse welcome page
(define pages:floraverse
(pages:template
#:title "Home: Generator.Beleth.Pink"
#:content
'(article
(p "As I'm a fan of the " (a ((href "http://floraverse.com")) "Floraverse comic") ", I wrote a generator making characters living in the world of the comic. "
"Initially on my main website, I moved the generator and other related Floraverse tools on its own website here, next to the "
(a ((href "http://beleth.pink")) "Toy Cat Creator") ". ")
)))
; About the generator
(define pages:floraverse-character-generator-about
(pages:template
#:title "About the Floraverse Character Generator"
#:content
'(article
(section
(h3 "Origin")
(p "The character generator is inspired by " (a ((href "http://fav.me/d7569je")) "this journal post on deviantArt") ", "
"which used the " (a ((href "http://marnok.com/content/_adventure/101npcs.php")) "Marnok's NPC generator") " to generate personality traits. "))
(section
(h3 "History")
(p "The current generator is the fifth iteration of the generator. " (br)
"The first version was a written in Java and started as a desktop application, before I added features to run it as a webservice. "
"This lead to the creation of the " (a ((href "http://feuforefe.fr")) "Feuforeve.fr website") ", to initially host that generator. " (br)
"This generator has since moved to its own website." (br)
"The second version was written in Guile Scheme and ran only as a webservice. " (br)
"The third version was based on the previous version and was the result of the merger "
"of several generators I wrote and hosted on this website into a single project. " (br)
"The fourth version was caused by a rewrite of this website in Racket. " (br)
"The fifth and current version is an iteration of the previous to separate the data used by the generators from the code. "))
(section
(h3 "Data")
(p "The data used by the generator is available in a "
(a ((href "https://projects.feuforeve.fr/Feufochmar/flora-generator-data")) "git repository")
", and is mainly written in JSON. "
"The file structures are documented in the " (code "README.md") " file. "))
(section
(h3 "Code")
(p "The code of the generator is available along with the code of this website in a "
(a ((href "https://projects.feuforeve.fr/Feufochmar/generator.beleth.pink")) "git repository") ". "
"It is released under the terms of the GNU General Public License version 2 or any later version (GPLv2+). "
"The code is written in " (a ((href "http://racket-lang.org/")) "Racket") ". "))
)))
; Generator page
(define *generable-species*
(sort
(species-database-all (flora-generator-species *FloraGen*) species-generable-as-character?)
(lambda (x y) (string<? (species-name x) (species-name y)))))
(define *languages*
(sort
(phonagen-generator-ids (flora-generator-name *FloraGen*))
(lambda (x y) (string<? (symbol->string x) (symbol->string y)))))
(define pages:floraverse-character-generator
(pages:template
#:title "Floraverse Character Generator"
#:content
(lambda (param)
(define asked-species (weblet-parameter-ref param 'species #f))
(define species (and asked-species (species-database-get (flora-generator-species *FloraGen*) asked-species)))
(define forced? (equal? "on" (weblet-parameter-ref param 'forced #f)))
(define asked-language (weblet-parameter-ref param 'language #f))
(define constraints (make-immutable-hash
`((species . ,species)
(nb-ascendents . ,(if forced? 0 3))
(language . ,(and asked-language (string->symbol asked-language)))
)))
;
`(article
(section
(form
((action "/CharacterGenerator"))
(label "Species "
(select ((name "species"))
(option ((value "")) "— random species —")
,@(map
(lambda (x)
(define sp-name (species-name x))
`(option ((value ,sp-name)
,@(if (equal? sp-name asked-species) '((selected "true")) '()))
,sp-name))
*generable-species*)
))
""
(input ((name "forced")(type "checkbox")
,@(if forced? '((checked "true")) '()))
"No family (force the species)")
(br)
(label "Names from "
(select ((name "language"))
(option ((value "")) "— random word generator —")
,@(map
(lambda (x)
(define str-lang (symbol->string x))
`(option ((value ,str-lang)
,@(if (equal? str-lang asked-language) '((selected "true")) '()))
,str-lang))
*languages*)
))
(br)
(button "New character"))
)
,(flora-character-generate *FloraGen* flora-character->full-html constraints)))))
; Tweet output, as json
(define pages:floraverse-character-generator-tweet
(raw-data-weblet
#:content-type #"application/json;charset=utf-8"
#:body
(lambda (param)
(jsexpr->bytes
(flora-character-generate *FloraGen* flora-character->tweets)))))
; Calendar Tool
(define pages:floraverse-calendar
(pages:template
#:title "Floraverse Calendar"
#:content
(lambda (param)
(define today (current-date))
(define today-flora (calendar-get-date (flora-generator-calendar *FloraGen*) (date-month today) (date-day today)))
(define req-date (weblet-parameter-ref param 'date #f))
(define split-date (and req-date (string-split req-date "-")))
(define req-month (and split-date (eq? 3 (length split-date)) (cadr split-date)))
(define req-day (and split-date (eq? 3 (length split-date)) (caddr split-date)))
(define match-month (weblet-parameter-ref param 'month #f))
(define match-day (weblet-parameter-ref param 'day #f))
(define month (or (and req-month (string->number req-month))
(and match-month (string->number match-month))))
(define day (or (and req-day (string->number req-day))
(and match-day (string->number match-day))))
(define req-flora
(with-handlers ([exn:fail? (lambda (v) #f)])
(calendar-get-date (flora-generator-calendar *FloraGen*) month day)))
(define (show-date d+s)
`(p
"The " ,(english:ordinal (date+sign-day d+s)) " of " ,(date+sign-month-name d+s) " "
"is under the astrological sign of " ,(date+sign-sign-name d+s) ". " (br)
(img ((src ,(string-append *FloraGenDataDir* "/" (date+sign-sign-symbol d+s)))
(width "64")
(height "64")
))))
`(article
,(if req-flora
`(section
(h3 ,(string-append "YYYY-" (number->string month) "-" (number->string day)))
,(show-date req-flora))
"")
(section
(h3 "Convert a date to Floraverse date")
(form ((action "/Calendar"))
(label "Pick a date: "
(input ((name "date")
(type "date")
(value ,(string-append
(number->string (date-year today)) "-"
(number->string (date-month today)) "-"
(number->string (date-day today)))))))
(br)
(button "Convert to Floraverse date")))
(section
(h3 "Today")
,(show-date today-flora))
))))

41
src/pages/sitemap.rkt Normal file
View File

@ -0,0 +1,41 @@
#lang racket/base
; Utility to build a sitemap and provide links in headers
(require
racket/string)
(provide
sitemap
build-headers)
(define *sitemap-list* '()) ; sitemap
; Site structure
(struct sitepage
(name ; Name displayed in headers, or #f if the page should not be shown in the header (but still considered when displaying the headers, mostly relevant for second-level pages)
path ; Absolute path to the page
link? ; Indicate if the link is clickable when on the same page
))
; Function to set the sitemap
(define (sitemap lst)
(set! *sitemap-list*
(map (lambda (x) (apply sitepage x)) lst)))
; Navigation link
(define (navigation-link str-path page)
(if (not (sitepage-name page))
""
`(section ((class "nav-item"))
,(if (or (not (equal? str-path (sitepage-path page)))
(sitepage-link? page))
`(a ((href ,(sitepage-path page))) ,(sitepage-name page))
(sitepage-name page)))))
; Build the navigation header
(define (build-headers current-path)
(let ((str-path (string-join current-path "/" #:before-first "/")))
(map
(lambda (x)
(navigation-link str-path x))
*sitemap-list*)))

73
src/pages/templates.rkt Normal file
View File

@ -0,0 +1,73 @@
#lang racket/base
; Templates for the pages
(require
"../webcontainer/weblets.rkt"
"../webcontainer/weblet-parameter.rkt"
"sitemap.rkt"
racket/date
)
(provide
make-nav-header
pages:template
pages:adaptable-template)
; Make the header
(define (make-nav-header param)
(define path (weblet-parameter-path param))
`(header
; Current page
(nav ,@(build-headers path))))
; Main template for pages
(define (pages:template
#:title title ; Title
#:content content ; Content, may be a function taking a weblet-parameter
#:date [date #f] ; Date
#:stylesheets [stylesheets (list)] ; Stylesheets to use on the page
#:scripts [scripts (list)] ; Scripts to use of the page
#:on-load [on-load #f] ; the onload attribute of the body of the page
#:error-code [error-code 200] ; Error code returned
)
(html-page-weblet
#:error-code error-code
#:body
(lambda (param)
`(html
(head
(title ,title)
(meta ((charset "UTF-8")))
(link ((href "/css/beleth.css")(rel "stylesheet")(type "text/css")(media "all")))
,@(map
(lambda (x)
`(link ((href ,x)(rel "stylesheet")(type "text/css")(media "all"))))
stylesheets)
,@(map
(lambda (x)
`(script ((src ,x)) " "))
scripts)
)
(body (,@(if on-load `((onload ,on-load)) '()))
,(make-nav-header param)
(main
(h1 ,title)
(hr)
,(apply-or-return content param))
(footer ,(string-append "©2015-" (number->string (date-year (current-date))) " Feufochmar"))
)))))
; Adaptable template
; Similar to pages:template, but the argument is a function taking the weblet parameters and returning a hash containing the arguments for pages:template
(define (pages:adaptable-template content)
(lambda (param)
(define result (content param))
((pages:template
#:title (hash-ref result 'title "Untitled page")
#:content (hash-ref result 'content)
#:date (hash-ref result 'date #f)
#:stylesheets (hash-ref result 'stylesheets (list))
#:scripts (hash-ref result 'scripts (list))
#:on-load (hash-ref result 'on-load #f)
#:error-code (hash-ref result 'error-code 200))
param)))

View File

@ -0,0 +1,59 @@
#lang racket/base
(require racket/match)
(provide http-message)
; Return the message corresponding to the given code
(define (http-message code)
(match code
(200 #"OK")
(404 #"Not found")
(201 #"Created")
(202 #"Accepted")
(203 #"Non-Authoritative Information")
(204 #"No Content")
(205 #"Reset Content")
(206 #"Partial Content")
(226 #"IM Used")
(300 #"Multiple Choices")
(301 #"Moved Permanently")
(302 #"Found")
(303 #"See Other")
(304 #"Not Modified")
(305 #"Use Proxy")
(307 #"Temporary Redirect")
(308 #"Permanent Redirect")
(310 #"Too many Redirects")
(400 #"Bad Request")
(401 #"Unauthorized")
(402 #"Payment Required")
(403 #"Forbidden")
(405 #"Method Not Allowed")
(406 #"Not Acceptable")
(407 #"Proxy Authentication Required")
(408 #"Request Time-out")
(409 #"Conflict")
(410 #"Gone")
(411 #"Length Required")
(412 #"Precondition Failed")
(413 #"Request Entity Too Large")
(414 #"Request-URI Too Long")
(415 #"Unsupported Media Type")
(416 #"Requested range unsatisfiable")
(417 #"Expectation failed")
(418 #"I'm a teapot")
(421 #"Misdirected Request")
(426 #"Upgrade Required")
(428 #"Precondition Required")
(429 #"Too Many Requests")
(431 #"Request Header Fields Too Large")
(451 #"Unavailable For Legal Reasons")
(500 #"Internal Server Error")
(501 #"Not Implemented")
(502 #"Bad Gateway")
(503 #"Service Unavailable")
(504 #"Gateway Time-out")
(505 #"HTTP Version not supported")
(506 #"Variant Also Negotiates")
(510 #"Not extended")
(511 #"Network authentication")
(_ #"Other")))

View File

@ -0,0 +1,319 @@
#lang racket/base
(require
racket/string
net/url
web-server/web-server
web-server/dispatchers/filesystem-map
web-server/dispatchers/dispatch
(prefix-in servlet: web-server/dispatchers/dispatch-servlets)
(prefix-in sequencer: web-server/dispatchers/dispatch-sequencer)
(prefix-in files: web-server/dispatchers/dispatch-files)
web-server/servlet/setup
web-server/stuffers
web-server/managers/none
web-server/http/redirect
web-server/http/request-structs
web-server/private/mime-types
"../collection/tree.rkt"
"weblet-parameter.rkt"
"website.rkt"
)
(provide
make-webcontainer
webcontainer-start
webcontainer-add-weblet! webcontainer-set-404-weblet!
webcontainer-add-symlink! webcontainer-add-redirection!
webcontainer-add-matching-weblet!
webcontainer-add-website!)
; webcontainer - a webserver to run weblets
; A weblet is a procedure taking a weblet-parameter and returning a response
(struct webcontainer
(server-port ; Port to listen
server-address ; Address to listen
static ; Path to non-dynamic assets
weblets ; weblets - dynamic pages
symlinks ; symlinks for path linking to another weblet
redirections ; redirections for path going to another website
matching-weblets ; like weblets, but extract parameters from the url
dispatcher ; servlet dispatcher
not-found-dispatcher ; Dispatcher for not found elements
))
; Helper: Make a servlet
(define (make-servlet function)
(make-stateless.servlet
"."
default-stuffer
(create-none-manager #f)
function))
; Helper: convert to a string list path, remove the empty elements in the path
(define (url->path url)
(filter
non-empty-string?
(map
path/param-path
(url-path url))))
; Helper: check if the path match an element in the tree
(define (has-matching? tr path)
(not (not (get-matching tr path))))
; Helper: produce a pair of weblet and hash table of params
(define (make-weblet-params weblet params)
(let ((hparams (make-hash)))
(if weblet
(begin
(for-each
(lambda (x)
(hash-set! hparams (car x) (cdr x)))
params)
(cons weblet hparams))
#f)))
; Helper: get the weblet and extracted parameters if the path match an element in the tree
(define (get-matching tr path [params (list)])
(cond
((and (eq? #f (tree-key tr)) (null? path))
; ROOT of the tree, and itself asked for
(if (tree-value tr)
(cons (tree-value tr) (make-hash))
#f))
((null? path)
; Should not happen outside the ROOT => not found...
#f)
((eq? #f (tree-key tr))
; ROOT of the tree, but not itself asked => pass to children with the same path
(let ((ret
(filter
(lambda (x) x)
(map
(lambda (x)
(get-matching x path params))
(tree-children tr)))))
(if (null? ret)
#f
(car ret))))
((and (symbol? (tree-key tr)) (null? (cdr path)))
; match pattern, node found
(make-weblet-params
(tree-value tr)
(cons (cons (tree-key tr) (car path))
params)))
((symbol? (tree-key tr))
; match pattern + check children
(let* ((p (cons (cons (tree-key tr) (car path)) params))
(ret
(filter
(lambda (x) x)
(map
(lambda (x)
(get-matching x (cdr path) p))
(tree-children tr)))))
(if (null? ret)
#f
(car ret))))
((and (equal? (tree-key tr) (car path)) (null? (cdr path)))
; Node found
(make-weblet-params (tree-value tr) params))
((equal? (tree-key tr) (car path))
; Check children
(let ((ret
(filter
(lambda (x) x)
(map
(lambda (x)
(get-matching x (cdr path) params))
(tree-children tr)))))
(if (null? ret)
#f
(car ret))))
(#t #f)
))
; Webcontainer constructor
(define (make-webcontainer
#:port [port 8080]
#:address [address #f]
#:static [static (make-immutable-hash '(("" . "./static")))]
)
(let* ((weblets (make-hash))
(symlinks (make-hash))
(redirections (make-hash))
(matching-weblets (make-tree))
; Servlet for weblets
(weblet-servlet
(make-servlet
(lambda (req)
(let* ((path (url->path (request-uri req)))
(weblet (hash-ref weblets path)))
(weblet (make-weblet-parameter #:request req #:path path))))))
; Servlet for symlinks
(symlink-servlet
(make-servlet
(lambda (req)
(let* ((path (url->path (request-uri req)))
(real-path (hash-ref symlinks path))
(weblet (hash-ref weblets real-path)))
(weblet (make-weblet-parameter #:request req #:path real-path))))))
; Servlet for redirections
(redirection-servlet
(make-servlet
(lambda (req)
(let* ((path (url->path (request-uri req)))
(to (hash-ref redirections path)))
(redirect-to to permanently)))))
; Servlet for using matching servlets
(matching-servlet
(make-servlet
(lambda (req)
(let* ((path (url->path (request-uri req)))
(weblet-params (get-matching matching-weblets path)))
((car weblet-params) (make-weblet-parameter #:request req #:path path #:match (cdr weblet-params)))))))
; Not found servlet
(not-found-servlet
(make-servlet
(lambda (req)
(let* ((path (url->path (request-uri req)))
(weblet (hash-ref weblets 404)))
(weblet (make-weblet-parameter #:request req #:path path))))))
)
(webcontainer
port
address
static
weblets
symlinks
redirections
matching-weblets
; dispatcher
(lambda (url)
(let ((path (url->path url)))
(cond
((hash-has-key? redirections path) redirection-servlet)
((hash-has-key? symlinks path) symlink-servlet)
((hash-has-key? weblets path) weblet-servlet)
((has-matching? matching-weblets path) matching-servlet)
(#t (next-dispatcher)))))
; not-found-dispatcher
(lambda (url)
not-found-servlet)
)))
(define (remove-prefix path pfx)
(cond
((null? pfx) path)
((null? path) path)
((equal? (car path) (car pfx))
(remove-prefix (cdr path) (cdr pfx)))
(#t path)))
; Start the server - do not return
(define (webcontainer-start wcontainer)
(serve
#:dispatch
(apply
sequencer:make
(append
; File dispatchers for each entry in the static map
(hash-map
(webcontainer-static wcontainer)
(lambda (root dir)
(define root-path (clean-path root))
(define dir-path (string->path dir))
(files:make
#:url->path (lambda (url)
; Remove the root from the url
(define path-without-root
(remove-prefix
(url->path url)
root-path))
; Build the new url
(define url-without-root
(path->url
(string->path
(string-join path-without-root "/" #:before-first "/"))))
;
((make-url->path dir-path) url-without-root))
#:path->mime-type (make-path->mime-type "./mime.types"))))
(list
; Servlet dispatcher
(servlet:make (webcontainer-dispatcher wcontainer))
; 404 error servlet
(servlet:make (webcontainer-not-found-dispatcher wcontainer))
)))
#:port (webcontainer-server-port wcontainer)
#:listen-ip (webcontainer-server-address wcontainer))
(do-not-return))
; Helper when defining a path to remove the unnecessary "/"
(define (clean-path str)
(filter
non-empty-string?
(string-split str "/")))
; Add a weblet to the container
(define (webcontainer-add-weblet! wcontainer path weblet)
(hash-set!
(webcontainer-weblets wcontainer)
(clean-path path)
weblet))
; Set the 404 error page weblet
(define (webcontainer-set-404-weblet! wcontainer weblet)
(hash-set!
(webcontainer-weblets wcontainer)
404
weblet))
; Add an internal redirection: the link given refer to another weblet
(define (webcontainer-add-symlink! wcontainer from to)
(hash-set!
(webcontainer-symlinks wcontainer)
(clean-path from)
(clean-path to)))
; Add an external redirection: the link given goes to another website
(define (webcontainer-add-redirection! wcontainer from to)
(hash-set!
(webcontainer-redirections wcontainer)
(clean-path from)
to))
; Matching weblet: the path contain parameters to extract when matching
(define (webcontainer-add-matching-weblet! wcontainer path weblet)
(tree-set!
(webcontainer-matching-weblets wcontainer)
(map
(lambda (x)
(if (and (string-prefix? x "{")
(string-suffix? x "}"))
(string->symbol
(substring x 1 (- (string-length x) 1)))
x))
(clean-path path))
weblet))
; Add a full website to the webcontainer
(define (webcontainer-add-website! wcontainer wsite [parent-path ""])
(define node (tree-value wsite))
(define path (string-append parent-path "/" (website-node-url node)))
(define type (website-node-type node))
(define weblet (website-node-weblet node))
(case type
((weblet)
(webcontainer-add-weblet! wcontainer path weblet))
((symlink)
(webcontainer-add-symlink! wcontainer path weblet))
((redirection)
(webcontainer-add-redirection! wcontainer path weblet))
((matching-weblet)
(webcontainer-add-matching-weblet! wcontainer path weblet))
(else
(error "Unexpected type: " type)))
(for-each
(lambda (child)
(webcontainer-add-website! wcontainer child path))
(tree-children wsite)))

View File

@ -0,0 +1,101 @@
#lang racket/base
(require
web-server/http
web-server/http/cookie-parse
net/url-structs
racket/string)
(provide
make-weblet-parameter
weblet-parameter-ref
weblet-parameter-cookie-ref
weblet-parameter-file-port-ref
weblet-parameter-method
weblet-parameter-protocol
weblet-parameter-host
; Fields of struct
weblet-parameter-request
weblet-parameter-path
weblet-parameter-match)
; weblet-parameter
; Contains: the request, the path of the request and a hash of matching parameters in the path
(struct weblet-parameter
(request ; HTTP request
path ; path called for the request, as a list of strings
match ; hash used in paramters
))
; Constructor
(define (make-weblet-parameter
#:request request
#:path path
#:match [match (make-hash)]
#:cookies [cookies (make-hash)])
(weblet-parameter
request
path
match))
; Get a value from the request or the match parameters
; key is a symbol
(define (weblet-parameter-ref wp key (default #f))
(let* ((query (request-bindings/raw (weblet-parameter-request wp)))
(req (bindings-assq (string->bytes/utf-8 (symbol->string key)) query))
(mat (hash-ref (weblet-parameter-match wp) key default)))
; Priority: from request, from match
(or (and req (bytes->string/utf-8
(or (and (binding:file/port? req) (binding:file-filename req))
(binding:form-value req))))
mat)))
; Get a cookie from the request. Return the cookie value as a string or #f if the asked value was not passed.
; Key is a string
(define (weblet-parameter-cookie-ref wp key (default #f))
(define cookie
(findf
(lambda (c) (equal? key (client-cookie-name c)))
(request-cookies (weblet-parameter-request wp))))
(and cookie (client-cookie-value cookie)))
; Get the file port from the request. Return the port or #f if the asked file was not passed.
; Key is a symbol
(define (weblet-parameter-file-port-ref wp key)
(define query (request-bindings/raw (weblet-parameter-request wp)))
(define req (bindings-assq (string->bytes/utf-8 (symbol->string key)) query))
(and req (binding:file/port? req)
(binding:file/port-in req)))
; Get the method used for the request.
; Method is returned as a symbol
(define (weblet-parameter-method wp)
(define method (request-method (weblet-parameter-request wp)))
(case method
((#"GET") 'get)
((#"HEAD") 'head)
((#"POST") 'post)
((#"PUT") 'put)
((#"DELETE") 'delete)
((#"TRACE") 'trace)
((#"OPTIONS") 'options)
((#"CONNECT") 'connect)
((#"PATCH") 'patch)
(else 'other-method)))
; Get the protocol used for the request, as a symbol
(define (weblet-parameter-protocol wp)
; Check the header: X-Scheme
; As the application is expected to be behind a proxy, the proxy must set the X-Scheme header to indicate which protocol was used.
(define scheme (headers-assq #"X-Scheme" (request-headers/raw (weblet-parameter-request wp))))
(and scheme (string->symbol (bytes->string/utf-8 (header-value scheme)))))
; Get the host used for the request
(define (weblet-parameter-host wp)
; Header: Host (as string)
; As the application is expected to be behind a proxy, the proxy must set the Host header to indicate which address was asked
(define host-header (headers-assq #"Host" (request-headers/raw (weblet-parameter-request wp))))
(define host (or (and host-header (bytes->string/utf-8 (header-value host-header)))
"localhost"))
; Remove the port part
(car (string-split host ":")))

View File

@ -0,0 +1,73 @@
#lang racket/base
; Helpers to simplify the writing of weblets
(require
web-server/http
web-server/http/redirect
net/url-string
xml
"http-message.rkt"
"weblet-parameter.rkt")
(provide
apply-or-return
raw-data-weblet
html-page-weblet
redirect-to-https-weblet)
; Apply or return - If value is a procedure, apply it to the request, or else return value
(define (apply-or-return value param)
(if (procedure? value)
(value param)
value))
; Weblet to return raw data
(define (raw-data-weblet
#:body body ; body, as a bytes-string or a procedure taking the request and returning a bytes-string
#:content-type [content-type #"text/plain;charset=utf-8"] ; type of the content, as a bytes-string
#:error-code [error-code 200] ; page error code
#:headers [headers (list)] ; headers of the response, or a procedure taking the request and returning the list of headers
)
(lambda (param)
(response/full
error-code (http-message error-code)
(current-seconds)
content-type
(apply-or-return headers param)
(list
(apply-or-return body param)))))
; Weblet to return html data
(define (html-page-weblet
#:body body ; body, as an xexpr or a procedure taking the request and returning an xexpr
#:error-code [error-code 200] ; page error code
#:headers [headers (list)] ; headers of the response, or a procedure taking the request and returning the list of headers
)
(lambda (param)
(response/full
error-code (http-message error-code)
(current-seconds)
TEXT/HTML-MIME-TYPE
(apply-or-return headers param)
(list
(string->bytes/utf-8
(xexpr->string
(apply-or-return body param)))))))
; Weblet to redirect to https, same place, same method
(define redirect-to-https-weblet
(lambda (param)
(define uri (request-uri (weblet-parameter-request param)))
(define new-uri
(make-url
"https"
(url-user uri)
(url-host uri)
(url-port uri)
(url-path-absolute? uri)
(url-path uri)
(url-query uri)
(url-fragment uri)))
(redirect-to
(url->string new-uri)
temporarily/same-method)))

View File

@ -0,0 +1,32 @@
#lang racket/base
; An helper to fill a webcontainer with weblets
(require
"../collection/tree.rkt")
(provide
website
website-node-url
website-node-type
website-node-weblet)
; Values in the website tree
(struct website-node
(url ; the partial url of the link, relative to parent
type ; the type of weblet
weblet ; the weblet associated (or a link for symlinks and redirections)
))
; website syntax to build the structure
(define-syntax website
(syntax-rules ()
( (website path type weblet (childparam ...) ...)
(tree
(if (equal? path "") #f path)
(website-node
path
(quote type)
weblet)
(list
(website childparam ...)
...)))))