#lang racket ;; start with some basic utilities (define (d n) (random 1 (+ n 1))) ;; the logic for judging the 'outcome' of an Ironsworn roll or progress track (define (judge-outcome attempt challenge-1 challenge-2) (let [(outcome (cond [(and (> attempt challenge-1) (> attempt challenge-2)) 'success] [(or (> attempt challenge-1) (> attempt challenge-2)) 'partial] [else 'failure]))] `(,outcome ,attempt (,challenge-1 ,challenge-2)))) ;; a typical Ironsworn roll: (define (roll-plus-mod m) (let [ ;; roll two challenge dice (challenge-1 (d 10)) (challenge-2 (d 10)) ;; roll an attempt die (attempt (+ (d 6) m))] ;; judge the outome (judge-outcome attempt challenge-1 challenge-2))) ;; this just keeps us honest that this is playable with dice (define *allowed-table-sizes* '(2 4 6 8 10 12 20 36 50 100)) ;; this is used in define-oracle to actually process the information, and ought to ;; return a zero-argument function that will roll on the table ;; TODO: also allow generating print structures from the same data! (define (oracle table) (cond [(andmap valid-oracle-result? table) (if (member (length table) *allowed-table-sizes*) (lambda () (car (shuffle table))) (error 'oracle "Length ~a is not a die-size-compatible length" (length table)))] [(andmap valid-table-member? table) (let [(max (max-table-size table))] (lambda () (table-index (d max) table)))] [else (error 'oracle "Unexpected argument: ~a" table)])) ;; given a range-based table, find the highest value to roll (define (max-table-size table) (let [(final (last table))] (if (= (length final) 3) (cadr final) (car final)))) ;; true if the `thing` is a symbol or string (define (valid-oracle-result? thing) (or (symbol? thing) (string? thing))) ;; true if the thing is a list of the form (num num string) or (num string) (define (valid-table-member? member) (and (list? member) (or (and (= (length member) 3) (number? (car member)) (number? (cadr member)) (valid-oracle-result? (caddr member))) (and (= (length member) 2) (number? (car member)) (valid-oracle-result? (cadr member)))))) ;; look up a number and find it in the table. (this assumes the table is sorted and ;; includes all possible values covered! ;; TODO: make this more resilient to bad input (define (table-index idx table) (if (null? table) (error 'table-index "Unexpected empty table") (let [(head (car table))] (cond [(and (= (length head) 3) (>= idx (car head)) (<= idx (cadr head))) (caddr head)] [(and (= (length head) 2) (= idx (car head))) (cadr head)] [else (table-index idx (cdr table))])))) (define-syntax-rule [define-oracle name . table] (define name (oracle (quote table)))) ;; create an asset (define (asset name table) (define (mk-feature feature) (list (car feature) (smart-string-append (cdr feature)))) (let [(stuff (map mk-feature table))] `(asset ,name ,stuff))) ;; concatenate strings, adding whitespace if necessary (define (smart-string-append list) (define (ensure-leading-space str) (if (char-whitespace? (string-ref str 0)) str (string-append " " str))) (apply string-append (cons (car list) (map ensure-leading-space (cdr list))))) (define-syntax-rule [define-asset-path name . table] (define name (asset (quote name) (quote table)))) (provide judge-outcome roll-plus-mod oracle define-oracle define-asset-path)