From cc70fd5b58bcb9a41d1ba8dc9ac9db4ad815a25c Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 30 Jan 2025 18:07:04 +1300 Subject: [PATCH 1/8] rewrote grammar - currently only checks half the req --- test/clj/game/core/async_test.clj | 488 +++++++++++++++++++----------- 1 file changed, 317 insertions(+), 171 deletions(-) diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index 742e029b1e..a4793e5609 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -1,48 +1,80 @@ (ns game.core.async-test (:require [clojure.test :refer :all] + [clojure.java.io :as io] [instaparse.core :as insta] [clojure.string :as str])) -(def scuffed-grammar - (insta/parser - "=P_EXPR* - - =(S_EXPR|TOKEN|STR) - =EXPR - ='#_' - (* note that S_EXPR cannot contain tokens or strings, to prevent left-expansion *) - (* from eradicating the heap *) - =VEC|MAP|FN|SET - VEC=<'['>P_EXPR*<']'> - MAP=<'{'>KEYPAIR*<'}'> - FN=<('#(' | '(' | '\\'(' )>P_EXPR*<')'> - SET=<'#{'>P_EXPR*<'}'> - =P_EXPR P_EXPR - - (* basic strings and tokens *) - S_TOKEN=STR|TOKEN - =<'\"'> - =#'[^\\s()\\[\\]{}#\\\"]*' - =<#'\\s'*> - (* note that we dont care about the contents of strings, we can just hide them *) - STR=<'#'?>QUOT<('\\\"' | #'[^\"]')*>QUOT")) - (def card-base-str "src/clj/game/cards/") (def relevant-cards-files ["basic.clj" "agendas.clj" "assets.clj" "events.clj" "hardware.clj" "ice.clj" "identities.clj" "operations.clj" "programs.clj" "resources.clj" "upgrades.clj"]) -(defn stitch-and-split-card-files +(def clojure-grammar + (insta/parser + "clojure = form+ +
= (anon-fn | fn | list | vector | map | set | symbol | literal | metadata | comment) + fn = <'(' ws> form* + = fn + list = fn + vector = <'[' ws> form* + map = <'{' ws> (form form)* <'}'> + set = <'#{' ws> form* <'}'> + comment = <'#_' form> + = '@'? identifier + = #'[\\'&%a-zA-Z_+\\-*/<>=?!\\.][%a-zA-Z0-9_+\\-*/<>=?!\\.]*' + (* throw away the content of everything except for keywords and strings *) + = number | string | truthy | keyword | character + number = <#'-?[0-9]+'> + string = <'#'?> #'\"([^\\\"\\\\]|\\\\.)*\"' + truthy = <'true' | 'false' | 'nil'> + keyword = <':'> identifier + (* I doubt we will ever use any of these, but it can't hurt *) + character = <#'\\\\[a-zA-Z0-9]' + | #'\\\\newline' + | #'\\\\space' + | #'\\\\tab' + | #'\\\\backspace' + | #'\\\\formfeed' + | #'\\\\return'> + (*TODO: allow specifying 'ignore-async-check' in metadata*) + metadata = <'^'> map form + (* handle whitespace*) + = <#'[\\s,\\n]+'?> ")) + +(defn- stitch-and-split-card-files [file] (let [split-file (str/split file #"(?=\(def)") restitch-fn (fn [chunk] (let [lines (str/split-lines chunk) - ;; special case specifically for the hydra subs, which have semicolons - sans-comments (map first (map #(str/split % #"(? (count chunk) 2) (completes? (last chunk) (inc depth))))) + (contains? #{"if" "if-not" "if-let"} ide) + (let [[_ _ body lhs rhs] chunk] + (and (completes-eid? lhs memory (inc depth)) (completes-eid? rhs memory (inc depth)))) + ;; `when ... complete` is a bad pattern, and leaves us open to unclosed eids + (contains? #{"when" "when-not" "when-let"} ide) nil + ;; cond - every RHS element completes + (= ide "cond") + (let [assignments (take-nth 2 (drop 3 chunk))] + (every? #(completes-eid? % memory (inc depth)) assignments)) + ;; condp - every RHS, and the terminal element, complete + (= ide "condp") + (let [assignments (concat (take-nth 2 (drop 5 chunk)) [(last chunk)])] + (every? #(completes-eid? % memory (inc depth)) assignments)) + ;; case - every RHS, and the terminal element, complete + (= ide "case") + (let [assignments (concat (take-nth 2 (drop 4 chunk)) [(last chunk)])] + (every? #(completes-eid? % memory (inc depth)) assignments)) + ;; cond+ - every RHS element of the leaves completes + (= ide "cond+") + (let [assignments (map last (drop 2 chunk))] + (every? #(completes-eid? % memory (inc depth)) assignments)) + ;; regular fn which contains an eid, or continue-abi + (or (= ide "continue-ability") (contains-eid? chunk 0)) :maybe - ;; other fns - see if the rightmost member completes - (and (vector? chunk) (= (first chunk) :FN)) - (completes? (last chunk) (inc depth)) - :else nil)) - -;; TODO - can add a few more to these as errors get picked up down the line -(def terminal-fns #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" "damage" "draw" "effect-completed" - "gain-credits" "gain-tags" "make-run" "reveal" "rez" "resolve-ability" "runner-install" - "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) -(defn should-complete? - "Should a chunk (probably) complete an eid?" - [chunk depth] - (cond - (and (string? chunk) (zero? depth)) nil - (and (vector? chunk) (= 2 (count chunk)) (zero? depth)) nil - (and (vector? chunk) (= (first chunk) :FN)) - (let [func-name (second chunk)] - (if (contains? terminal-fns func-name) - true - (some #(should-complete? % (inc depth)) (rest (rest chunk))))) - :else nil)) - -(defn is-valid-chunk? - ([chunk] + ;; leftover fn - check the RHS member completes + :else (and (> (count chunk) 2) (completes-eid? (last chunk) memory (inc depth))) + )) + +(defn- is-valid-chunk? + "checks if a chunk of code is 'valid' in terms of sync/async classification. + This is intended to be at least 99% accurate, but if something is getting missed, + you can tag it with the metadata ^{:ignore-async-check true} and it will be ignored!" + ([chunk] (is-valid-chunk? chunk (atom {}))) + ([[sig :as chunk] memory] + (when (= :fn sig) (bank-fn! chunk memory)) + (println "chunk: " chunk) + ;; (println "memory: " memory) (cond - (not (sequential? chunk)) true - (= :FN (first chunk)) (every? is-valid-chunk? (rest chunk)) - (= :VEC (first chunk)) (every? is-valid-chunk? (rest chunk)) - (= :SET (first chunk)) (every? is-valid-chunk? (rest chunk)) - (= [:STR] chunk) true - (= :MAP (first chunk)) (is-valid-chunk? (rest chunk) :MAP) - (sequential? chunk) (every? is-valid-chunk? chunk) - :else true)) - ([chunk conditional?] + (contains? #{:string :keyword :number :character :truthy :list :comment} sig) :fine + (string? chunk) :fine + (contains? #{:fn :vector :set} sig) (every? #(is-valid-chunk? % memory) (rest chunk)) + ;; maps -> require more complicated logic + (= :map sig) (is-valid-chunk? (rest chunk) memory :map) + :else (do (println "invalid chunk: " chunk) + nil))) + ([chunk memory sig] + (println "sig: " sig " - mchunk: " chunk) (cond - (= conditional? :MAP) - (do (let [keypairs (partition 2 chunk) - mapped (zipmap (map first keypairs) (map second keypairs))] - ;; if it contains an :effect, then: - ;; see if it contains :async true. If it does, effect must complete eid - ;; if it does not, effect should not complete the eid - (and - (if (:effect mapped) - (if (:async mapped) - (is-valid-chunk? (:effect mapped) :async) - (is-valid-chunk? (:effect mapped) :sync)) - (every? is-valid-chunk? (vals mapped))) - ;; note that cancel effects must always complete eids, - ;; as there is no provision for async-checking them baked into the engine - ;; this comment is valid as of Jan '25 -nbk - (if (:cancel-effect mapped) - (is-valid-chunk? (:cancel-effect mapped) :async) - true)))) - (= conditional? :async) - (and (completes? chunk 0) (is-valid-chunk? chunk)) - (= conditional? :sync) - (and (not (should-complete? chunk 0)) (is-valid-chunk? chunk))))) - -(defn clean-chunks - "remove the empties and nils, and swaps keywords in" - [chunks] - (cond - (= "" chunks) nil - (= "true" chunks) true - (= "nil" chunks) :nil - (and (string? chunks) (str/starts-with? chunks ":")) (keyword (subs chunks 1)) - (sequential? chunks) (filterv identity (map clean-chunks chunks)) - :else chunks)) - -(defn validate-chunk [chunk] (->> chunk scuffed-grammar clean-chunks is-valid-chunk?)) + (= sig :map) + (let [keypairs (partition 2 chunk) + mapped (zipmap (map (comp assemble-keywords first) keypairs) (map second keypairs))] + (and + (if (:effect mapped) + (if (:async mapped) + (is-valid-chunk? (:effect mapped) memory :async) + (is-valid-chunk? (:effect mapped) memory :sync)) + true) + (if (:cancel-effect mapped) + (is-valid-chunk? (:cancel-effect mapped) memory :async) + true) + (if-not (or (:effect mapped) (:cancel-effect mapped)) + (every? #(is-valid-chunk? % memory) (map second keypairs)) + true))) + ;; things that are async should be completing eids + (= sig :async) (and (completes-eid? chunk memory 0) (is-valid-chunk? chunk memory)) + (= sig :sync) + true))) (deftest cards-are-async-test - (doseq [fname relevant-cards-files] + (doseq [fname (take 15 relevant-cards-files)] (let [f (slurp (str card-base-str fname)) - chunks (stitch-and-split-card-files f)] - (let [invalid-chunks (filter (complement validate-chunk) chunks) - titles (map #(re-find #" \".+?\"" %) invalid-chunks)] - (when (seq titles) - (is nil (str "The following cards/fns in file '" fname "' may be invalid (async/sync): " (str/join ", " titles)))))))) - -(deftest async-test-if-block-is-correct? - (let [c1 "{:async true :effect (req (if (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state)))}" - c2 "{:async true :effect (req (if-not (some corp-installable-type? (:hand corp)) (damage 2) (damage state side eid 1)))}" - c3 "{:async true :effect (req (if-let (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state side eid 1)))}"] - (is (not (validate-chunk c1)) "If block C1 is picked up as being wrong (RHS does not complete)") - (is (not (validate-chunk c2)) "If block C2 is picked up as being wrong (LHS does not complete)") - (is (validate-chunk c3) "If block C3 is picked up as being right (LHS and RHS both complete)"))) - -(deftest async-test-when-block-is-correct? - (let [c1 "{:async true :effect (req (when x (effect-completed state side eid)))}" - c2 "{:async true :effect (req (do (when x y) (effect-completed state side eid)))}"] - (is (not (validate-chunk c1)) "When block C1 is picked up as being wrong (conditional may not complete)") - (is (validate-chunk c2) "When block C2 is picked up as being right (conditional does not block completion)"))) - -(deftest async-test-case-block-is-correct? - (let [c1 "{:async true :effect (req (case x a (effect-completed state side eid) (system-msg state side \"whoops\")))}" - c2 "{:async true :effect (req (case x a (system-msg state side \"whoops\") (effect-completed state side eid)))}" - c3 "{:async true :effect (req (case x a (effect-completed state side eid) (effect-completed state side eid)))}"] - (is (not (validate-chunk c1)) "Case block C1 is picked up as being wrong (terminal does not complete)") - (is (not (validate-chunk c2)) "Case block C2 is picked up as being wrong (LHS does not complete)") - (is (validate-chunk c3) "Case block C3 is picked up as being right (LHS and terminal both complete)"))) - -(deftest async-test-cond+-is-correct? - (let [c1 "{:async true :effect (req (cond+ [a (damage state :runner)] [:else (effect-completed state side eid)]))}" - c2 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (damage state side)]))}" - c3 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (effect-completed state side eid)]))}"] - (is (not (validate-chunk c1)) "Cond+ block C1 is picked up as being wrong (RHS does not complete)") - (is (not (validate-chunk c2)) "Cond+ block C2 is picked up as being wrong (LHS does not complete)") - (is (validate-chunk c3) "Cond+ block C3 is picked up as being right (LHS and RHS both complete)"))) + chunks (rest (stitch-and-split-card-files f)) + name-if-invalid (fn [parsed] (when-not (is-valid-chunk? parsed) (get-fn-name parsed)))] + ;; final form should look like this + ;; (doseq [c (rest chunks)] + ;; ;;(println c) + ;; (let [parsed (clojure-grammar c)] + ;; (println parsed) + ;; (is (not ((comp name-if-invalid second) parsed)) (str "wrong: " parsed))))))) + (let [invalids (->> chunks + (map (comp name-if-invalid second clojure-grammar)) + (filterv identity))] + (is (empty? invalids) + (str "the following definitions in " fname + " may have sync/async issues: " (str/join ", " invalids))))))) + +;; ;; TODO - can add a few more to these as errors get picked up down the line +;; (def terminal-fns #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" "damage" "draw" "effect-completed" +;; "gain-credits" "gain-tags" "make-run" "reveal" "rez" "resolve-ability" "runner-install" +;; "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) +;; (defn should-complete? +;; "Should a chunk (probably) complete an eid?" +;; [chunk depth] +;; (cond +;; (and (string? chunk) (zero? depth)) nil +;; (and (vector? chunk) (= 2 (count chunk)) (zero? depth)) nil +;; (and (vector? chunk) (= (first chunk) :FN)) +;; (let [func-name (second chunk)] +;; (if (contains? terminal-fns func-name) +;; true +;; (some #(should-complete? % (inc depth)) (rest (rest chunk))))) +;; :else nil)) + +;; (defn is-valid-chunk? +;; ([chunk] +;; (cond +;; (not (sequential? chunk)) true +;; (= :FN (first chunk)) (every? is-valid-chunk? (rest chunk)) +;; (= :VEC (first chunk)) (every? is-valid-chunk? (rest chunk)) +;; (= :SET (first chunk)) (every? is-valid-chunk? (rest chunk)) +;; (= [:STR] chunk) true +;; (= :MAP (first chunk)) (is-valid-chunk? (rest chunk) :MAP) +;; (sequential? chunk) (every? is-valid-chunk? chunk) +;; :else true)) +;; ([chunk conditional?] +;; (cond +;; (= conditional? :MAP) +;; (do (let [keypairs (partition 2 chunk) +;; mapped (zipmap (map first keypairs) (map second keypairs))] +;; ;; if it contains an :effect, then: +;; ;; see if it contains :async true. If it does, effect must complete eid +;; ;; if it does not, effect should not complete the eid +;; (and +;; (if (:effect mapped) +;; (if (:async mapped) +;; (is-valid-chunk? (:effect mapped) :async) +;; (is-valid-chunk? (:effect mapped) :sync)) +;; (every? is-valid-chunk? (vals mapped))) +;; ;; note that cancel effects must always complete eids, +;; ;; as there is no provision for async-checking them baked into the engine +;; ;; this comment is valid as of Jan '25 -nbk +;; (if (:cancel-effect mapped) +;; (is-valid-chunk? (:cancel-effect mapped) :async) +;; true)))) +;; (= conditional? :async) +;; (and (completes? chunk 0) (is-valid-chunk? chunk)) +;; (= conditional? :sync) +;; (and (not (should-complete? chunk 0)) (is-valid-chunk? chunk))))) + +;; (defn clean-chunks +;; "remove the empties and nils, and swaps keywords in" +;; [chunks] +;; (cond +;; (= "" chunks) nil +;; (= "true" chunks) true +;; (= "nil" chunks) :nil +;; (and (string? chunks) (str/starts-with? chunks ":")) (keyword (subs chunks 1)) +;; (sequential? chunks) (filterv identity (map clean-chunks chunks)) +;; :else chunks)) + +;; (defn validate-chunk [chunk] (->> chunk scuffed-grammar clean-chunks is-valid-chunk?)) + +;; (deftest cards-are-async-test +;; (doseq [fname relevant-cards-files] +;; (let [f (slurp (str card-base-str fname)) +;; chunks (stitch-and-split-card-files f)] +;; (let [invalid-chunks (filter (complement validate-chunk) chunks) +;; titles (map #(re-find #" \".+?\"" %) invalid-chunks)] +;; (when (seq titles) +;; (is nil (str "The following cards/fns in file '" fname "' may be invalid (async/sync): " (str/join ", " titles)))))))) + +;; (deftest async-test-if-block-is-correct? +;; (let [c1 "{:async true :effect (req (if (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state)))}" +;; c2 "{:async true :effect (req (if-not (some corp-installable-type? (:hand corp)) (damage 2) (damage state side eid 1)))}" +;; c3 "{:async true :effect (req (if-let (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state side eid 1)))}"] +;; (is (not (validate-chunk c1)) "If block C1 is picked up as being wrong (RHS does not complete)") +;; (is (not (validate-chunk c2)) "If block C2 is picked up as being wrong (LHS does not complete)") +;; (is (validate-chunk c3) "If block C3 is picked up as being right (LHS and RHS both complete)"))) + +;; (deftest async-test-when-block-is-correct? +;; (let [c1 "{:async true :effect (req (when x (effect-completed state side eid)))}" +;; c2 "{:async true :effect (req (do (when x y) (effect-completed state side eid)))}"] +;; (is (not (validate-chunk c1)) "When block C1 is picked up as being wrong (conditional may not complete)") +;; (is (validate-chunk c2) "When block C2 is picked up as being right (conditional does not block completion)"))) + +;; (deftest async-test-case-block-is-correct? +;; (let [c1 "{:async true :effect (req (case x a (effect-completed state side eid) (system-msg state side \"whoops\")))}" +;; c2 "{:async true :effect (req (case x a (system-msg state side \"whoops\") (effect-completed state side eid)))}" +;; c3 "{:async true :effect (req (case x a (effect-completed state side eid) (effect-completed state side eid)))}"] +;; (is (not (validate-chunk c1)) "Case block C1 is picked up as being wrong (terminal does not complete)") +;; (is (not (validate-chunk c2)) "Case block C2 is picked up as being wrong (LHS does not complete)") +;; (is (validate-chunk c3) "Case block C3 is picked up as being right (LHS and terminal both complete)"))) + +;; (deftest async-test-cond+-is-correct? +;; (let [c1 "{:async true :effect (req (cond+ [a (damage state :runner)] [:else (effect-completed state side eid)]))}" +;; c2 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (damage state side)]))}" +;; c3 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (effect-completed state side eid)]))}"] +;; (is (not (validate-chunk c1)) "Cond+ block C1 is picked up as being wrong (RHS does not complete)") +;; (is (not (validate-chunk c2)) "Cond+ block C2 is picked up as being wrong (LHS does not complete)") +;; (is (validate-chunk c3) "Cond+ block C3 is picked up as being right (LHS and RHS both complete)"))) + + + + +;; [:fn defcard +;; [:string "Deuces Wild"] +;; [:fn let +;; [:vector +;; all [:vector +;; [:map +;; [:keyword effect] [:fn effect [:fn gain-credits eid [:number]]] +;; [:keyword async] [:truthy] +;; [:keyword msg] [:string "gain 3 [Credits]"]] +;; [:map +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn effect [:fn draw eid [:number]]] +;; [:keyword msg] [:string "draw 2 cards"]] +;; [:map +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn effect [:fn lose-tags eid [:number]]] +;; [:keyword msg] [:string "remove 1 tag"]] +;; [:map +;; [:keyword prompt] [:string "Choose 1 piece of ice to expose"] +;; [:keyword msg] [:string "expose 1 ice and make a run"] +;; [:keyword choices] [:map +;; [:keyword card] [:fn and [:fn installed? %] [:fn ice? %]]] +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn req +;; [:fn wait-for +;; [:fn expose state side target] +;; [:fn continue-ability +;; state side +;; [:map +;; [:keyword prompt] [:string "Choose a server"] +;; [:keyword choices] [:fn req runnable-servers] +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn effect [:fn make-run eid target]]] +;; card [:truthy]]]] +;; [:keyword cancel-effect] [:fn effect +;; [:fn continue-ability +;; [:map +;; [:keyword prompt] [:string "Choose a server"] +;; [:keyword choices] [:fn req runnable-servers] +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn effect [:fn make-run eid target]]] +;; card [:truthy]]]]] +;; choice [:fn fn +;; choice [:vector abis] [:map +;; [:keyword prompt] [:string "Choose an ability to resolve"] +;; [:keyword choices] [:fn map [:fn capitalize [:fn [:keyword msg] %]] abis] +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn req [:fn let [:vector chosen [:fn some [:fn when [:fn = target [:fn capitalize [:fn [:keyword msg] %]]] %] abis]] +;; [:fn wait-for +;; [:fn resolve-ability state side chosen card [:truthy]] +;; [:fn if +;; [:fn = [:fn count abis] [:number]] +;; [:fn continue-ability state side [:fn choice [:fn remove-once [:fn = % chosen] abis]] card [:truthy]] +;; [:fn effect-completed state side eid]]]]]]]] +;; [:map +;; [:keyword on-play] [:map +;; [:keyword async] [:truthy] +;; [:keyword effect] [:fn effect [:fn continue-ability [:fn choice all] card [:truthy]]]]]]] From b520cf57703aec015db4504853ecf9a9e7211bca Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 30 Jan 2025 19:47:47 +1300 Subject: [PATCH 2/8] pretty sure the test looks good now --- test/clj/game/core/async_test.clj | 279 ++++++++++-------------------- 1 file changed, 88 insertions(+), 191 deletions(-) diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index a4793e5609..2f449c3ff1 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -80,9 +80,10 @@ [chunk depth] (some #(cond (string? %) (= % "eid") - (and (vector? %) (= (second %) "make-eid")) true - (and (vector? %) (= (second %) "assoc")) (contains-eid? % (inc depth)) - (and (vector? %) (zero? depth)) (contains-eid? % 1) + (not (vector? %)) nil + (= (second %) "make-eid") true + (contains? #{"assoc" "assoc-in"} (second %)) (contains-eid? % (inc depth)) + (zero? depth) (contains-eid? % 1) :else nil) chunk)) @@ -93,7 +94,8 @@ (and (string? chunk) (contains? @memory chunk) (< depth 15)) (completes-eid? (get @memory chunk) memory (inc depth)) ;; if it's not a function, it doesn't complete - (not= sig :fn) (println "does not complete: " chunk) + (not= sig :fn) nil + ;; referring to a pre-deffed fn (and ide (contains? @memory ide) (< depth 15)) (completes-eid? (get @memory ide) memory (inc depth)) ;; both sides of the ifn should complete @@ -122,9 +124,25 @@ (or (= ide "continue-ability") (contains-eid? chunk 0)) :maybe ;; leftover fn - check the RHS member completes - :else (and (> (count chunk) 2) (completes-eid? (last chunk) memory (inc depth))) - )) + :else (and (> (count chunk) 2) (completes-eid? (last chunk) memory (inc depth))))) +;; TODO - add more things as needed, if issues arise +(def terminal-fns #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" + "damage" "draw" "effect-completed" "gain-credits" "gain-tags" "make-run" + "reveal" "rez" "resolve-ability" "runner-install" + "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) + +(defn- should-be-async? + "should a chunk (probably) complete an eid?" + [[sig ide :as chunk] memory depth] + (cond + (>= depth 15) nil + (contains? @memory chunk) (should-be-async? (get @memory chunk) memory (inc depth)) + (and ide (contains? @memory ide)) (should-be-async? (get @memory ide) memory (inc depth)) + (= :fn sig) (or (contains? terminal-fns ide) + (some #(should-be-async? % memory (inc depth)) (drop 2 chunk))))) + +;; TODO - check function metadata for ignore-async-check, just incase we purposely break shit (defn- is-valid-chunk? "checks if a chunk of code is 'valid' in terms of sync/async classification. This is intended to be at least 99% accurate, but if something is getting missed, @@ -132,18 +150,14 @@ ([chunk] (is-valid-chunk? chunk (atom {}))) ([[sig :as chunk] memory] (when (= :fn sig) (bank-fn! chunk memory)) - (println "chunk: " chunk) - ;; (println "memory: " memory) (cond (contains? #{:string :keyword :number :character :truthy :list :comment} sig) :fine (string? chunk) :fine (contains? #{:fn :vector :set} sig) (every? #(is-valid-chunk? % memory) (rest chunk)) ;; maps -> require more complicated logic (= :map sig) (is-valid-chunk? (rest chunk) memory :map) - :else (do (println "invalid chunk: " chunk) - nil))) + :else nil)) ([chunk memory sig] - (println "sig: " sig " - mchunk: " chunk) (cond (= sig :map) (let [keypairs (partition 2 chunk) @@ -161,191 +175,74 @@ (every? #(is-valid-chunk? % memory) (map second keypairs)) true))) ;; things that are async should be completing eids - (= sig :async) (and (completes-eid? chunk memory 0) (is-valid-chunk? chunk memory)) - (= sig :sync) - true))) + (= sig :async) (and (completes-eid? chunk memory 0) + (is-valid-chunk? chunk memory)) + (= sig :sync) (and (not (should-be-async? chunk memory 0)) + (not (completes-eid? chunk memory 0)) + (is-valid-chunk? chunk memory)) + :else true))) + +(defn- invalid-chunk? + [chunk] + (let [[sig body :as parsed] (clojure-grammar chunk)] + (if-not (is-valid-chunk? body) + (get-fn-name body) + nil))) (deftest cards-are-async-test (doseq [fname (take 15 relevant-cards-files)] + (println "checking" fname) (let [f (slurp (str card-base-str fname)) - chunks (rest (stitch-and-split-card-files f)) - name-if-invalid (fn [parsed] (when-not (is-valid-chunk? parsed) (get-fn-name parsed)))] - ;; final form should look like this - ;; (doseq [c (rest chunks)] - ;; ;;(println c) - ;; (let [parsed (clojure-grammar c)] - ;; (println parsed) - ;; (is (not ((comp name-if-invalid second) parsed)) (str "wrong: " parsed))))))) - (let [invalids (->> chunks - (map (comp name-if-invalid second clojure-grammar)) - (filterv identity))] + chunks (rest (stitch-and-split-card-files f))] + (let [invalids (->> chunks (map invalid-chunk?) (filterv identity))] (is (empty? invalids) (str "the following definitions in " fname " may have sync/async issues: " (str/join ", " invalids))))))) -;; ;; TODO - can add a few more to these as errors get picked up down the line -;; (def terminal-fns #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" "damage" "draw" "effect-completed" -;; "gain-credits" "gain-tags" "make-run" "reveal" "rez" "resolve-ability" "runner-install" -;; "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) -;; (defn should-complete? -;; "Should a chunk (probably) complete an eid?" -;; [chunk depth] -;; (cond -;; (and (string? chunk) (zero? depth)) nil -;; (and (vector? chunk) (= 2 (count chunk)) (zero? depth)) nil -;; (and (vector? chunk) (= (first chunk) :FN)) -;; (let [func-name (second chunk)] -;; (if (contains? terminal-fns func-name) -;; true -;; (some #(should-complete? % (inc depth)) (rest (rest chunk))))) -;; :else nil)) - -;; (defn is-valid-chunk? -;; ([chunk] -;; (cond -;; (not (sequential? chunk)) true -;; (= :FN (first chunk)) (every? is-valid-chunk? (rest chunk)) -;; (= :VEC (first chunk)) (every? is-valid-chunk? (rest chunk)) -;; (= :SET (first chunk)) (every? is-valid-chunk? (rest chunk)) -;; (= [:STR] chunk) true -;; (= :MAP (first chunk)) (is-valid-chunk? (rest chunk) :MAP) -;; (sequential? chunk) (every? is-valid-chunk? chunk) -;; :else true)) -;; ([chunk conditional?] -;; (cond -;; (= conditional? :MAP) -;; (do (let [keypairs (partition 2 chunk) -;; mapped (zipmap (map first keypairs) (map second keypairs))] -;; ;; if it contains an :effect, then: -;; ;; see if it contains :async true. If it does, effect must complete eid -;; ;; if it does not, effect should not complete the eid -;; (and -;; (if (:effect mapped) -;; (if (:async mapped) -;; (is-valid-chunk? (:effect mapped) :async) -;; (is-valid-chunk? (:effect mapped) :sync)) -;; (every? is-valid-chunk? (vals mapped))) -;; ;; note that cancel effects must always complete eids, -;; ;; as there is no provision for async-checking them baked into the engine -;; ;; this comment is valid as of Jan '25 -nbk -;; (if (:cancel-effect mapped) -;; (is-valid-chunk? (:cancel-effect mapped) :async) -;; true)))) -;; (= conditional? :async) -;; (and (completes? chunk 0) (is-valid-chunk? chunk)) -;; (= conditional? :sync) -;; (and (not (should-complete? chunk 0)) (is-valid-chunk? chunk))))) - -;; (defn clean-chunks -;; "remove the empties and nils, and swaps keywords in" -;; [chunks] -;; (cond -;; (= "" chunks) nil -;; (= "true" chunks) true -;; (= "nil" chunks) :nil -;; (and (string? chunks) (str/starts-with? chunks ":")) (keyword (subs chunks 1)) -;; (sequential? chunks) (filterv identity (map clean-chunks chunks)) -;; :else chunks)) - -;; (defn validate-chunk [chunk] (->> chunk scuffed-grammar clean-chunks is-valid-chunk?)) - -;; (deftest cards-are-async-test -;; (doseq [fname relevant-cards-files] -;; (let [f (slurp (str card-base-str fname)) -;; chunks (stitch-and-split-card-files f)] -;; (let [invalid-chunks (filter (complement validate-chunk) chunks) -;; titles (map #(re-find #" \".+?\"" %) invalid-chunks)] -;; (when (seq titles) -;; (is nil (str "The following cards/fns in file '" fname "' may be invalid (async/sync): " (str/join ", " titles)))))))) - -;; (deftest async-test-if-block-is-correct? -;; (let [c1 "{:async true :effect (req (if (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state)))}" -;; c2 "{:async true :effect (req (if-not (some corp-installable-type? (:hand corp)) (damage 2) (damage state side eid 1)))}" -;; c3 "{:async true :effect (req (if-let (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state side eid 1)))}"] -;; (is (not (validate-chunk c1)) "If block C1 is picked up as being wrong (RHS does not complete)") -;; (is (not (validate-chunk c2)) "If block C2 is picked up as being wrong (LHS does not complete)") -;; (is (validate-chunk c3) "If block C3 is picked up as being right (LHS and RHS both complete)"))) - -;; (deftest async-test-when-block-is-correct? -;; (let [c1 "{:async true :effect (req (when x (effect-completed state side eid)))}" -;; c2 "{:async true :effect (req (do (when x y) (effect-completed state side eid)))}"] -;; (is (not (validate-chunk c1)) "When block C1 is picked up as being wrong (conditional may not complete)") -;; (is (validate-chunk c2) "When block C2 is picked up as being right (conditional does not block completion)"))) - -;; (deftest async-test-case-block-is-correct? -;; (let [c1 "{:async true :effect (req (case x a (effect-completed state side eid) (system-msg state side \"whoops\")))}" -;; c2 "{:async true :effect (req (case x a (system-msg state side \"whoops\") (effect-completed state side eid)))}" -;; c3 "{:async true :effect (req (case x a (effect-completed state side eid) (effect-completed state side eid)))}"] -;; (is (not (validate-chunk c1)) "Case block C1 is picked up as being wrong (terminal does not complete)") -;; (is (not (validate-chunk c2)) "Case block C2 is picked up as being wrong (LHS does not complete)") -;; (is (validate-chunk c3) "Case block C3 is picked up as being right (LHS and terminal both complete)"))) - -;; (deftest async-test-cond+-is-correct? -;; (let [c1 "{:async true :effect (req (cond+ [a (damage state :runner)] [:else (effect-completed state side eid)]))}" -;; c2 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (damage state side)]))}" -;; c3 "{:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (effect-completed state side eid)]))}"] -;; (is (not (validate-chunk c1)) "Cond+ block C1 is picked up as being wrong (RHS does not complete)") -;; (is (not (validate-chunk c2)) "Cond+ block C2 is picked up as being wrong (LHS does not complete)") -;; (is (validate-chunk c3) "Cond+ block C3 is picked up as being right (LHS and RHS both complete)"))) - - - - -;; [:fn defcard -;; [:string "Deuces Wild"] -;; [:fn let -;; [:vector -;; all [:vector -;; [:map -;; [:keyword effect] [:fn effect [:fn gain-credits eid [:number]]] -;; [:keyword async] [:truthy] -;; [:keyword msg] [:string "gain 3 [Credits]"]] -;; [:map -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn effect [:fn draw eid [:number]]] -;; [:keyword msg] [:string "draw 2 cards"]] -;; [:map -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn effect [:fn lose-tags eid [:number]]] -;; [:keyword msg] [:string "remove 1 tag"]] -;; [:map -;; [:keyword prompt] [:string "Choose 1 piece of ice to expose"] -;; [:keyword msg] [:string "expose 1 ice and make a run"] -;; [:keyword choices] [:map -;; [:keyword card] [:fn and [:fn installed? %] [:fn ice? %]]] -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn req -;; [:fn wait-for -;; [:fn expose state side target] -;; [:fn continue-ability -;; state side -;; [:map -;; [:keyword prompt] [:string "Choose a server"] -;; [:keyword choices] [:fn req runnable-servers] -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn effect [:fn make-run eid target]]] -;; card [:truthy]]]] -;; [:keyword cancel-effect] [:fn effect -;; [:fn continue-ability -;; [:map -;; [:keyword prompt] [:string "Choose a server"] -;; [:keyword choices] [:fn req runnable-servers] -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn effect [:fn make-run eid target]]] -;; card [:truthy]]]]] -;; choice [:fn fn -;; choice [:vector abis] [:map -;; [:keyword prompt] [:string "Choose an ability to resolve"] -;; [:keyword choices] [:fn map [:fn capitalize [:fn [:keyword msg] %]] abis] -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn req [:fn let [:vector chosen [:fn some [:fn when [:fn = target [:fn capitalize [:fn [:keyword msg] %]]] %] abis]] -;; [:fn wait-for -;; [:fn resolve-ability state side chosen card [:truthy]] -;; [:fn if -;; [:fn = [:fn count abis] [:number]] -;; [:fn continue-ability state side [:fn choice [:fn remove-once [:fn = % chosen] abis]] card [:truthy]] -;; [:fn effect-completed state side eid]]]]]]]] -;; [:map -;; [:keyword on-play] [:map -;; [:keyword async] [:truthy] -;; [:keyword effect] [:fn effect [:fn continue-ability [:fn choice all] card [:truthy]]]]]]] +(deftest async-test-defferred-fns-are-correct + (let [c1 "(defcard \"c1\" (let [x (req (do-something state side eid))] {:async true :effect x}))" + c2 "(defcard \"c2\" (let [x (req (do-something state side nil))] {:async true :effect x}))" + c3 "(defcard \"c3\" (let [x (req (do-something state side eid))] {:effect x}))" + c4 "(defcard \"c4\" (let [x (req (do-something state side nil))] {:effect x}))" + c5 "(defcard \"c5\" (letfn [(x [] (req (do-something state side eid)))] {:async true :effect (x)}))" + c6 "(defcard \"c6\" (letfn [(x [] (req (do-something state side nil)))] {:async true :effect (x)}))" + c7 "(defcard \"c7\" (letfn [(x [] (req (do-something state side eid)))] {:effect (x)}))" + c8 "(defcard \"c8\" (letfn [(x [] (req (do-something state side nil)))] {:effect (x)}))"] + (is (not (invalid-chunk? c1)) "deffered block c1 is picked up as being correct (x completes)") + (is (invalid-chunk? c2) "deffered block c2 is picked up as being wrong (x should complete)") + (is (invalid-chunk? c3) "deffered block c3 is picked up as being wrong (x should complete)") + (is (not (invalid-chunk? c4)) "deffered block c4 is picked up as being correct (x should not complete)") + (is (not (invalid-chunk? c5)) "deffered block c5 is picked up as being correct (x completes)") + (is (invalid-chunk? c6) "deffered block c6 is picked up as being wrong (x should complete)") + (is (invalid-chunk? c7) "deffered block c7 is picked up as being wrong (x is not async, but should be)") + (is (not (invalid-chunk? c8)) "deffered block c8 is picked up as being correct (x should not complete)"))) + +(deftest async-test-if-block-is-correct? + (let [c1 (invalid-chunk? "(defcard \"c1\" {:async true :effect (req (if (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state)))})") + c2 (invalid-chunk? "(defcard \"c2\" {:async true :effect (req (if-not (some corp-installable-type? (:hand corp)) (damage 2) (damage state side eid 1)))})") + c3 (invalid-chunk? "(defcard \"c3\" {:async true :effect (req (if-let (some corp-installable-type? (:hand corp)) (continue-ability state side select-ability card nil) (damage state side eid 1)))})")] + (is c1 "If block C1 is picked up as being wrong (RHS does not complete)") + (is c2 "If block C2 is picked up as being wrong (LHS does not complete)") + (is (not c3) "If block C3 is picked up as being right (LHS and RHS both complete)"))) + +(deftest async-test-when-block-is-correct? + (let [c1 "(defcard \"c1\" {:async true :effect (req (when x (effect-completed state side eid)))})" + c2 "(defcard \"c2\" {:async true :effect (req (do (when x y) (effect-completed state side eid)))})"] + (is (invalid-chunk? c1) "When block C1 is picked up as being wrong (conditional may not complete)") + (is (not (invalid-chunk? c2)) "When block C2 is picked up as being right (conditional does not block completion)"))) + +(deftest async-test-case-block-is-correct? + (let [c1 "(defcard \"c1\" {:async true :effect (req (case x a (effect-completed state side eid) (system-msg state side \"whoops\")))})" + c2 "(defcard \"c2\" {:async true :effect (req (case x a (system-msg state side \"whoops\") (effect-completed state side eid)))})" + c3 "(defcard \"c3\" {:async true :effect (req (case x a (effect-completed state side eid) (effect-completed state side eid)))})"] + (is (invalid-chunk? c1) "Case block C1 is picked up as being wrong (terminal does not complete)") + (is (invalid-chunk? c2) "Case block C2 is picked up as being wrong (LHS does not complete)") + (is (not (invalid-chunk? c3)) "Case block C3 is picked up as being right (LHS and terminal both complete)"))) + +(deftest async-test-cond+-is-correct? + (let [c1 "(defcard \"c1\" {:async true :effect (req (cond+ [a (damage state :runner)] [:else (effect-completed state side eid)]))})" + c2 "(defcard \"c2\" {:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (damage state side)]))})" + c3 "(defcard \"c3\" {:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (effect-completed state side eid)]))})"] + (is (invalid-chunk? c1) "Cond+ block C1 is picked up as being wrong (RHS does not complete)") + (is (invalid-chunk? c2) "Cond+ block C2 is picked up as being wrong (LHS does not complete)") + (is (not (invalid-chunk? c3)) "Cond+ block C3 is picked up as being right (LHS and RHS both complete)"))) From c23b3caf54502552ffddd662467c9c4a0e348f98 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 30 Jan 2025 20:53:09 +1300 Subject: [PATCH 3/8] async test is polished and 'done' --- test/clj/game/core/async_test.clj | 71 ++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index 2f449c3ff1..bb031bece7 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -5,6 +5,23 @@ [instaparse.core :as insta] [clojure.string :as str])) +;; This is intended to be a (mostly) exhaustive test for if cards that are +;; marked as async to in-fact complete eids, and if cards that are not +;; are also correct. I'm aiming to be accurate, and I think it's pretty much there. +;; +;; I currently don't have support for macros (mainly, just the tokens). +;; +;; If something is incorrect, you can prevent it being evaluated by adding +;; the following metadata to a card-def: ^:ignore-async-check +;; like: (defcard "Fall Guy" ^:ignore-async-check {:effect (req (do-something-cool))}) +;; +;; There's a list of 'safe fns' (right-most rns that can contain an eid) and +;; terminal fns. If something gets caught out at some point in the future, it probably +;; means that one of these needs to be updated. +;; +;; --nbk, Jan 2025 + + (def card-base-str "src/clj/game/cards/") (def relevant-cards-files ["basic.clj" "agendas.clj" "assets.clj" "events.clj" "hardware.clj" "ice.clj" "identities.clj" "operations.clj" "programs.clj" @@ -38,7 +55,7 @@ | #'\\\\formfeed' | #'\\\\return'> (*TODO: allow specifying 'ignore-async-check' in metadata*) - metadata = <'^'> map form + metadata = <'^'> form form (* handle whitespace*) = <#'[\\s,\\n]+'?> ")) @@ -76,12 +93,25 @@ (doseq [[_ k _ rhs] (rest bindings)] (when (string? k) (swap! memory assoc k rhs))))) +;; TODO - add more things as needed, if issues arise +(def terminal-fns + "functions which should complete an eid, or indicate one needs to be completed" + #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" + "damage" "draw" "effect-completed" "gain-credits" "gain-tags" "make-run" + "reveal" "rez" "resolve-ability" "runner-install" + "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) + +(def safe-fns + "functions which probably contain an eid, but do not complete it" + #{"can-pay?" "cost-value"}) + (defn- contains-eid? [chunk depth] (some #(cond (string? %) (= % "eid") (not (vector? %)) nil (= (second %) "make-eid") true + (contains? safe-fns (second %)) nil (contains? #{"assoc" "assoc-in"} (second %)) (contains-eid? % (inc depth)) (zero? depth) (contains-eid? % 1) :else nil) @@ -98,6 +128,8 @@ ;; referring to a pre-deffed fn (and ide (contains? @memory ide) (< depth 15)) (completes-eid? (get @memory ide) memory (inc depth)) + ;; if it's a safe function, it does not complete + (contains? safe-fns ide) nil ;; both sides of the ifn should complete (contains? #{"if" "if-not" "if-let"} ide) (let [[_ _ body lhs rhs] chunk] @@ -126,12 +158,6 @@ ;; leftover fn - check the RHS member completes :else (and (> (count chunk) 2) (completes-eid? (last chunk) memory (inc depth))))) -;; TODO - add more things as needed, if issues arise -(def terminal-fns #{"checkpoint" "complete-with-result" "continue-ability" "corp-install" - "damage" "draw" "effect-completed" "gain-credits" "gain-tags" "make-run" - "reveal" "rez" "resolve-ability" "runner-install" - "trash" "trash-cards" "trigger-event-simult" "trigger-event-sync" "wait-for"}) - (defn- should-be-async? "should a chunk (probably) complete an eid?" [[sig ide :as chunk] memory depth] @@ -142,6 +168,11 @@ (= :fn sig) (or (contains? terminal-fns ide) (some #(should-be-async? % memory (inc depth)) (drop 2 chunk))))) +(defn- read-metadata + [[_ meta body :as metadata]] + (when-not (= meta [:keyword "ignore-async-check"]) + body)) + ;; TODO - check function metadata for ignore-async-check, just incase we purposely break shit (defn- is-valid-chunk? "checks if a chunk of code is 'valid' in terms of sync/async classification. @@ -156,6 +187,9 @@ (contains? #{:fn :vector :set} sig) (every? #(is-valid-chunk? % memory) (rest chunk)) ;; maps -> require more complicated logic (= :map sig) (is-valid-chunk? (rest chunk) memory :map) + ;; note, metadata can signify that a function does not need to be checked + (= :metadata sig) + (if-let [next-chunk (read-metadata chunk)] (is-valid-chunk? next-chunk memory) :fine) :else nil)) ([chunk memory sig] (cond @@ -199,6 +233,13 @@ (str "the following definitions in " fname " may have sync/async issues: " (str/join ", " invalids))))))) +(deftest metadata-ignore-works + (let [c1 "(defcard \\a ^:ignore-async-check {:async true :effect (req nil)})" + c2 "(defcard \\b {:async true :effect (req nil)})"] + (is (not (invalid-chunk? c1)) "c1 is valid because we ignore the async check") + (is (invalid-chunk? c2) "c2 is invalid because we do not ignore the async check"))) + + (deftest async-test-defferred-fns-are-correct (let [c1 "(defcard \"c1\" (let [x (req (do-something state side eid))] {:async true :effect x}))" c2 "(defcard \"c2\" (let [x (req (do-something state side nil))] {:async true :effect x}))" @@ -226,23 +267,23 @@ (is (not c3) "If block C3 is picked up as being right (LHS and RHS both complete)"))) (deftest async-test-when-block-is-correct? - (let [c1 "(defcard \"c1\" {:async true :effect (req (when x (effect-completed state side eid)))})" - c2 "(defcard \"c2\" {:async true :effect (req (do (when x y) (effect-completed state side eid)))})"] + (let [c1 "(defcard \"c1\" {:async true :effect (req (when x (do-something state side eid)))})" + c2 "(defcard \"c2\" {:async true :effect (req (do (when x y) (do-something state side eid)))})"] (is (invalid-chunk? c1) "When block C1 is picked up as being wrong (conditional may not complete)") (is (not (invalid-chunk? c2)) "When block C2 is picked up as being right (conditional does not block completion)"))) (deftest async-test-case-block-is-correct? - (let [c1 "(defcard \"c1\" {:async true :effect (req (case x a (effect-completed state side eid) (system-msg state side \"whoops\")))})" - c2 "(defcard \"c2\" {:async true :effect (req (case x a (system-msg state side \"whoops\") (effect-completed state side eid)))})" - c3 "(defcard \"c3\" {:async true :effect (req (case x a (effect-completed state side eid) (effect-completed state side eid)))})"] + (let [c1 "(defcard \"c1\" {:async true :effect (req (case x a (do-something state side eid) (system-msg state side \"whoops\")))})" + c2 "(defcard \"c2\" {:async true :effect (req (case x a (system-msg state side \"whoops\") (do-something state side eid)))})" + c3 "(defcard \"c3\" {:async true :effect (req (case x a (do-thing state side eid) (do-something state side eid)))})"] (is (invalid-chunk? c1) "Case block C1 is picked up as being wrong (terminal does not complete)") (is (invalid-chunk? c2) "Case block C2 is picked up as being wrong (LHS does not complete)") (is (not (invalid-chunk? c3)) "Case block C3 is picked up as being right (LHS and terminal both complete)"))) (deftest async-test-cond+-is-correct? - (let [c1 "(defcard \"c1\" {:async true :effect (req (cond+ [a (damage state :runner)] [:else (effect-completed state side eid)]))})" - c2 "(defcard \"c2\" {:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (damage state side)]))})" - c3 "(defcard \"c3\" {:async true :effect (req (cond+ [a (effect-completed state :runner eid)] [:else (effect-completed state side eid)]))})"] + (let [c1 "(defcard \"c1\" {:async true :effect (req (cond+ [a (damage state :runner)] [:else (do-something state side eid)]))})" + c2 "(defcard \"c2\" {:async true :effect (req (cond+ [a (do-something state :runner eid)] [:else (damage state side)]))})" + c3 "(defcard \"c3\" {:async true :effect (req (cond+ [a (do-something state :runner eid)] [:else (do-something state side eid)]))})"] (is (invalid-chunk? c1) "Cond+ block C1 is picked up as being wrong (RHS does not complete)") (is (invalid-chunk? c2) "Cond+ block C2 is picked up as being wrong (LHS does not complete)") (is (not (invalid-chunk? c3)) "Cond+ block C3 is picked up as being right (LHS and RHS both complete)"))) From ac99e286d9b8ef9310c5d3d49b601e4895c4eae9 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 30 Jan 2025 20:54:01 +1300 Subject: [PATCH 4/8] async fixes --- src/clj/game/cards/assets.clj | 1 + src/clj/game/cards/hardware.clj | 1 + src/clj/game/cards/ice.clj | 1 + src/clj/game/cards/operations.clj | 1 + src/clj/game/cards/programs.clj | 1 - src/clj/game/cards/upgrades.clj | 8 +++++--- 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 4a478f3cf1..4a6fda84de 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -304,6 +304,7 @@ (defcard "B-1001" {:abilities [{:req (req (not this-server)) + :async true :cost [(->c :tag 1)] :msg "end the run" :label "End the run on another server" diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 8f8c1cb859..b018723ebd 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -1253,6 +1253,7 @@ (defcard "Lemuria Codecracker" {:abilities [{:action true + :async true :cost [(->c :click 1) (->c :credit 1)] :req (req (some #{:hq} (:successful-run runner-reg))) :choices {:card installed?} diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index e4b4bec4c3..907eb85b75 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -1241,6 +1241,7 @@ (defcard "Clairvoyant Monitor" {:subroutines [(do-psi {:label "Place 1 advancement token and end the run" + :async true :player :corp :prompt "Choose an installed card to place 1 advancement token on" :msg (msg "place 1 advancement token on " diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index cb117fdc31..4e59034911 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -1632,6 +1632,7 @@ (has-subtype? % "Connection") (installed? %))} :msg (msg "host itself on " (card-str state target) ". The Runner has an additional tag") + :async true :effect (effect (install-as-condition-counter eid card target))} :static-abilities [{:type :tags :value 1}] diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index ec6255a0b5..237ddcf03a 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -2081,7 +2081,6 @@ :once :per-turn :cost [(->c :x-credits)] :req (req (:runner-phase-12 @state)) - :async true :effect (effect (add-counter card :power (cost-value eid :x-credits))) :msg (msg "place " (quantify (cost-value eid :x-credits) "power counter") " on itself")} (break-sub [(->c :power 1)] 1) diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index 4ce81757aa..249b0504d1 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -397,6 +397,7 @@ {:events [{:event :pass-all-ice :psi {:req (req this-server) :not-equal {:msg "end the run" + :async true :effect (effect (end-run eid card))}}}]}) (defcard "Cayambe Grid" @@ -493,8 +494,7 @@ :effect (effect (add-counter card :power 1))}]}) (defcard "Corporate Troubleshooter" - {:abilities [{:async true - :label "Add strength to a rezzed piece of ice protecting this server" + {:abilities [{:label "Add strength to a rezzed piece of ice protecting this server" :cost [(->c :trash-can) (->c :x-credits)] :choices {:all true :req (req (and (ice? target) @@ -678,6 +678,7 @@ etr {:req (req this-server) :cost [(->c :power 1)] :msg "end the run" + :async true :effect (effect (end-run eid card))}] {:derezzed-events [(assoc corp-rez-toast :event :runner-turn-ends)] :events [(assoc maybe-gain-counter :event :corp-turn-begins) @@ -1707,6 +1708,7 @@ :choices {:req (req (same-server? card target))} :msg (msg "place " (if (is-boosted-fn? state side) 3 2) " advancement counters on " (card-str state target)) + :async true :effect (req (let [n (if (is-boosted-fn? state side) 3 2)] (add-prop state side eid target :advance-counter n {:placed true})))}] @@ -1752,7 +1754,7 @@ {:async true :msg "do 1 core damage instead of net damage" :effect (req (swap! state update :damage dissoc :damage-replace :defer-damage) - (wait-for (pay state :corp (make-eid state eid) card (->c :credit 2)) + (wait-for (pay state :corp card (->c :credit 2)) (system-msg state side (:msg async-result)) (wait-for (damage state side :brain 1 {:card card}) (swap! state assoc-in [:damage :damage-replace] true) From d313d33acc50f1634c9ca19e797488b29b91d965 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 30 Jan 2025 20:59:46 +1300 Subject: [PATCH 5/8] formatting --- test/clj/game/core/async_test.clj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index bb031bece7..312e937608 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -21,7 +21,6 @@ ;; ;; --nbk, Jan 2025 - (def card-base-str "src/clj/game/cards/") (def relevant-cards-files ["basic.clj" "agendas.clj" "assets.clj" "events.clj" "hardware.clj" "ice.clj" "identities.clj" "operations.clj" "programs.clj" @@ -68,7 +67,7 @@ ;; and TLDR, which contains a semicolon in the title sans-comments (map first (map #(str/split % #"(?> chunks (map invalid-chunk?) (filterv identity))] @@ -239,7 +236,6 @@ (is (not (invalid-chunk? c1)) "c1 is valid because we ignore the async check") (is (invalid-chunk? c2) "c2 is invalid because we do not ignore the async check"))) - (deftest async-test-defferred-fns-are-correct (let [c1 "(defcard \"c1\" (let [x (req (do-something state side eid))] {:async true :effect x}))" c2 "(defcard \"c2\" (let [x (req (do-something state side nil))] {:async true :effect x}))" From 4f78162ad9892c7c4bce7453f4911ddd2633dc9e Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Fri, 31 Jan 2025 12:46:05 +1300 Subject: [PATCH 6/8] cleaned up test more, covers core files too now --- test/clj/game/core/async_test.clj | 70 +++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index 312e937608..d678b9c04f 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -1,7 +1,6 @@ (ns game.core.async-test (:require [clojure.test :refer :all] - [clojure.java.io :as io] [instaparse.core :as insta] [clojure.string :as str])) @@ -22,23 +21,22 @@ ;; --nbk, Jan 2025 (def card-base-str "src/clj/game/cards/") -(def relevant-cards-files ["basic.clj" "agendas.clj" "assets.clj" "events.clj" "hardware.clj" - "ice.clj" "identities.clj" "operations.clj" "programs.clj" - "resources.clj" "upgrades.clj"]) +(def core-base-str "src/clj/game/core/") (def clojure-grammar (insta/parser "clojure = form+ - = (anon-fn | fn | list | vector | map | set | symbol | literal | metadata | comment) + = (anon-fn | fn | list | vector | map | set | symbol | literal | metadata | comment | unquote) fn = <'(' ws> form* = fn - list = fn + list = fn vector = <'[' ws> form* map = <'{' ws> (form form)* <'}'> set = <'#{' ws> form* <'}'> comment = <'#_' form> - = '@'? identifier - = #'[\\'&%a-zA-Z_+\\-*/<>=?!\\.][%a-zA-Z0-9_+\\-*/<>=?!\\.]*' + unquote = <'~' form> + = <('#\\''|'@')?> identifier + = #'[\\'&%a-zA-Z_+\\-*/<>=?!\\.][%a-zA-Z0-9_+\\-*/<>=?!\\.#]*' (* throw away the content of everything except for keywords and strings *) = number | string | truthy | keyword | character number = <#'-?[0-9]+'> @@ -46,7 +44,7 @@ truthy = <'true' | 'false' | 'nil'> keyword = <':'> identifier (* I doubt we will ever use any of these, but it can't hurt *) - character = <#'\\\\[a-zA-Z0-9]' + character = <#'\\\\[a-zA-Z0-9#]' | #'\\\\newline' | #'\\\\space' | #'\\\\tab' @@ -60,7 +58,7 @@ (defn- stitch-and-split-card-files [file] - (let [split-file (str/split file #"(?=\(def)") + (let [split-file (str/split file #"(?=\n\(def)") restitch-fn (fn [chunk] (let [lines (str/split-lines chunk) ;; special case specifically for the hydra subs, which have semicolons, @@ -70,11 +68,15 @@ (map restitch-fn split-file))) (defn- get-fn-name - "extracts a function name from a parsed segment of code" + "extracts a function name from a parsed segment of code + expects something like [:fn ide], where ide is either a string, or [:string ...], + and may also be wrapped in a [:clojure] tag, like [:clojure [:fn ide]]" [parsed] - (let [[t s :as ide] (nth (if (= :clojure (first parsed)) (second parsed) parsed) 2)] - (if (= :string t) s - ide))) + (let [[_ sig [t s :as ide] multi] (if (= :clojure (first parsed)) (second parsed) parsed)] + (cond + (= :string t) s + (= sig "defmethod") [ide multi] + :else ide))) (defn- assemble-keywords "convert chunks into keywords where appropriate" @@ -102,7 +104,7 @@ (def safe-fns "functions which probably contain an eid, but do not complete it" - #{"can-pay?" "cost-value"}) + #{"can-pay?" "cost-value" "recurring-fn"}) (defn- contains-eid? [chunk depth] @@ -180,12 +182,15 @@ ([[sig :as chunk] memory] (when (= :fn sig) (bank-fn! chunk memory)) (cond - (contains? #{:string :keyword :number :character :truthy :list :comment} sig) :fine + (contains? #{:string :keyword :number :character :truthy :list :comment :unquote} sig) :fine (string? chunk) :fine (contains? #{:fn :vector :set} sig) (every? #(is-valid-chunk? % memory) (rest chunk)) ;; maps -> require more complicated logic (= :map sig) (is-valid-chunk? (rest chunk) memory :map) ;; note, metadata can signify that a function does not need to be checked + ;; this is primarily for things which defer to other files, written like: + ;; {:async true :effect fn-from-another-file}. Resolving that properly is + ;; way beyond the scope of this test :) - nbk, 2025 (= :metadata sig) (if-let [next-chunk (read-metadata chunk)] (is-valid-chunk? next-chunk memory) :fine) :else nil)) @@ -216,13 +221,27 @@ (defn- invalid-chunk? [chunk] - (let [[sig body :as parsed] (clojure-grammar chunk)] - (if-not (is-valid-chunk? body) - (get-fn-name body) - nil))) + (let [parsed (clojure-grammar chunk)] + ;; just in case there are parsing errors, this should spit out something + ;; that's usable enough to figure out what we're missing in the grammar + ;; (or if some fancy new specs get added to clojure) + (if (= (str (type parsed)) "class instaparse.gll.Failure") + (do (println "unable to parse chunk: " chunk) + (println parsed) + "[parse-error]") + (let [[sig body] parsed] + (if-not (is-valid-chunk? body) + (get-fn-name body) + nil))))) + +;; note: this SHOULD avoid emacs autosave and backup files, but I'm not sure if it will +;; potentially pick up backup files from other editors. If that happens, I can just adjust +;; the regex later. -nbk, 2025 +(defn get-clojure-files [d] + (sort (filter #(re-matches #"^.*\.clj$" %) (seq (.list (clojure.java.io/file d)))))) (deftest cards-are-async-test - (doseq [fname (take 15 relevant-cards-files)] + (doseq [fname (get-clojure-files card-base-str)] (let [f (slurp (str card-base-str fname)) chunks (rest (stitch-and-split-card-files f))] (let [invalids (->> chunks (map invalid-chunk?) (filterv identity))] @@ -230,6 +249,15 @@ (str "the following definitions in " fname " may have sync/async issues: " (str/join ", " invalids))))))) +(deftest core-fns-are-async-test + (doseq [fname (get-clojure-files core-base-str)] + (let [f (slurp (str core-base-str fname)) + chunks (rest (stitch-and-split-card-files f))] + (let [invalids (->> chunks (map invalid-chunk?) (filterv identity))] + (is (empty? invalids) + (str "the following definitions in " fname + " may have sync/async issues: " (str/join ", " invalids))))))) + (deftest metadata-ignore-works (let [c1 "(defcard \\a ^:ignore-async-check {:async true :effect (req nil)})" c2 "(defcard \\b {:async true :effect (req nil)})"] From 990840677145a85f36ac199b1f0d6825a894c0f8 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Fri, 31 Jan 2025 12:46:27 +1300 Subject: [PATCH 7/8] async fixes --- src/clj/game/core/commands.clj | 2 ++ src/clj/game/core/costs.clj | 1 + src/clj/game/core/pick_counters.clj | 12 ++++++------ src/clj/game/core/prompts.clj | 18 +++++++++--------- src/clj/game/core/runs.clj | 2 ++ src/clj/game/core/sabotage.clj | 1 + src/clj/game/core/turns.clj | 1 + 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/clj/game/core/commands.clj b/src/clj/game/core/commands.clj index e93084c4dc..2047492446 100644 --- a/src/clj/game/core/commands.clj +++ b/src/clj/game/core/commands.clj @@ -324,6 +324,7 @@ (or (installed? target) (in-hand? target))))} :msg (msg "score " (card-str state target {:visible true}) ", ignoring all restrictions") + :async true :effect (effect (score eid target {:no-req true :ignore-turn true}))} (make-card {:title "the '/score' command"}) nil))) @@ -408,6 +409,7 @@ state side {:prompt "Choose a card to trash" :choices {:card #(f %)} + :async true :effect (effect (trash eid target {:unpreventable true}))} nil nil))) diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index 137bf50366..770a8e8261 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -1000,6 +1000,7 @@ :choices {:card #(and (agenda? %) (is-scored? state side %) (pos? (get-counters % :agenda)))} + :async true :effect (req (let [title (:title target) target (update! state side (update-in target [:counter :agenda] - (value cost)))] (wait-for (trigger-event-sync state side :agenda-counter-spent target) diff --git a/src/clj/game/core/pick_counters.clj b/src/clj/game/core/pick_counters.clj index 38e205e646..3c26e72637 100644 --- a/src/clj/game/core/pick_counters.clj +++ b/src/clj/game/core/pick_counters.clj @@ -111,8 +111,8 @@ pay-function (if (= :custom pay-credits-type) (-> target card-def :interactions :pay-credits :custom) (take-counters-of-type pay-credits-type)) - custom-ability {:async true - :effect pay-function} + custom-ability ^:ignore-async-check {:async true + :effect pay-function} neweid (make-eid state outereid) providing-card target] (wait-for (resolve-ability state side neweid custom-ability providing-card [card]) @@ -169,8 +169,8 @@ (if (or (not (pos? target-count)) ; there is a limit (<= target-count counter-count) ; paid everything (zero? (count provider-cards))) ; no more additional credit sources found - {:async true - :effect pay-rest} + {:async true + :effect pay-rest} {:async true :prompt (str "Choose a credit providing card (" counter-count (when (and target-count (pos? target-count)) @@ -185,8 +185,8 @@ pay-function (if (= :custom pay-credits-type) (-> target card-def :interactions :pay-credits :custom) (take-counters-of-type pay-credits-type)) - custom-ability {:async true - :effect pay-function} + custom-ability ^:ignore-async-check {:async true + :effect pay-function} neweid (make-eid state outereid) providing-card target] (wait-for (resolve-ability state side neweid custom-ability providing-card [card]) diff --git a/src/clj/game/core/prompts.clj b/src/clj/game/core/prompts.clj index 4f55a47072..9462603cb2 100644 --- a/src/clj/game/core/prompts.clj +++ b/src/clj/game/core/prompts.clj @@ -28,15 +28,15 @@ {:keys [waiting-prompt prompt-type show-discard cancel-effect end-effect targets]}] (let [prompt (if (string? message) message (message state side eid card targets)) choices (choice-parser choices) - newitem {:eid eid - :msg prompt - :choices choices - :effect f - :card card - :prompt-type (or prompt-type :other) - :show-discard show-discard - :cancel-effect cancel-effect - :end-effect end-effect}] + newitem ^:ignore-async-check {:eid eid + :msg prompt + :choices choices + :effect f + :card card + :prompt-type (or prompt-type :other) + :show-discard show-discard + :cancel-effect cancel-effect + :end-effect end-effect}] (when (or (#{:waiting :run} prompt-type) (:number choices) (:card-title choices) diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index c9114d00d8..c25c5cfd0a 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -593,6 +593,7 @@ state :runner {:prompt "Choose a breach replacement ability" :choices (if mandatory titles (conj titles (str "Breach " (zone->name (:server (:run @state)))))) + :async true :effect (req (let [chosen (some #(when (= target (get-in % [:card :title])) %) handlers) ability (:ability chosen) card (:card chosen)] @@ -630,6 +631,7 @@ state :runner eid {:prompt (str "You are prevented from breaching " (zone->name server) " this run.") :choices ["OK"] + :async true :effect (effect (system-msg :runner (str "is prevented from breaching " (zone->name server) " this run.")) (handle-end-run eid))} nil nil) diff --git a/src/clj/game/core/sabotage.clj b/src/clj/game/core/sabotage.clj index 4d1116416c..8478cb4046 100644 --- a/src/clj/game/core/sabotage.clj +++ b/src/clj/game/core/sabotage.clj @@ -42,6 +42,7 @@ (defn sabotage-ability [n] (let [choosing-ab (fn [forced-hq] + ^:ignore-async-check {:waiting-prompt true :player :corp :prompt (choosing-prompt-req n) diff --git a/src/clj/game/core/turns.clj b/src/clj/game/core/turns.clj index 39dc6d6244..737308ea23 100644 --- a/src/clj/game/core/turns.clj +++ b/src/clj/game/core/turns.clj @@ -117,6 +117,7 @@ :choices {:card in-hand? :max (- cur-hand-size (max (hand-size state side) 0)) :all true} + :async true :effect (req (system-msg state side (str "discards " (if (= :runner side) From 1dd7327746e13de880cd85c9b538296ce88402c8 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Fri, 31 Jan 2025 19:40:53 +1300 Subject: [PATCH 8/8] indent changes --- src/clj/game/core/pick_counters.clj | 102 ++++++++++++++-------------- src/clj/game/core/prompts.clj | 19 +++--- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/src/clj/game/core/pick_counters.clj b/src/clj/game/core/pick_counters.clj index 3c26e72637..3be66768b9 100644 --- a/src/clj/game/core/pick_counters.clj +++ b/src/clj/game/core/pick_counters.clj @@ -141,59 +141,59 @@ pay-rest (req (if (and (<= (- target-count counter-count) (get-in @state [side :credit])) (<= stealth-target stealth-count)) - (let [remainder (max 0 (- target-count counter-count)) - remainder-str (when (pos? remainder) - (str remainder " [Credits]")) - card-strs (when (pos? (count selected-cards)) - (str (enumerate-str (map #(let [{:keys [card number]} % - title (:title card)] - (str number " [Credits] from " title)) - (vals selected-cards))))) - message (str card-strs - (when (and card-strs remainder-str) - " and ") - remainder-str - (when (and card-strs remainder-str) - " from [their] credit pool"))] - (lose state side :credit remainder) - (let [cards (->> (vals selected-cards) - (map :card) - (remove #(-> (card-def %) :interactions :pay-credits :cost-reduction)))] - (wait-for (trigger-spend-credits-from-cards state side cards) - ; Now we trigger all of the :counter-added events we'd neglected previously - (pick-counter-triggers state side eid selected-cards selected-cards target-count message)))) - (continue-ability - state side - (pick-credit-providing-cards provider-func eid target-count stealth-target selected-cards) - card nil)))] + (let [remainder (max 0 (- target-count counter-count)) + remainder-str (when (pos? remainder) + (str remainder " [Credits]")) + card-strs (when (pos? (count selected-cards)) + (str (enumerate-str (map #(let [{:keys [card number]} % + title (:title card)] + (str number " [Credits] from " title)) + (vals selected-cards))))) + message (str card-strs + (when (and card-strs remainder-str) + " and ") + remainder-str + (when (and card-strs remainder-str) + " from [their] credit pool"))] + (lose state side :credit remainder) + (let [cards (->> (vals selected-cards) + (map :card) + (remove #(-> (card-def %) :interactions :pay-credits :cost-reduction)))] + (wait-for (trigger-spend-credits-from-cards state side cards) + ; Now we trigger all of the :counter-added events we'd neglected previously + (pick-counter-triggers state side eid selected-cards selected-cards target-count message)))) + (continue-ability + state side + (pick-credit-providing-cards provider-func eid target-count stealth-target selected-cards) + card nil)))] (if (or (not (pos? target-count)) ; there is a limit (<= target-count counter-count) ; paid everything (zero? (count provider-cards))) ; no more additional credit sources found {:async true :effect pay-rest} - {:async true - :prompt (str "Choose a credit providing card (" - counter-count (when (and target-count (pos? target-count)) - (str " of " target-count)) - " [Credits]" - (if (pos? stealth-target) - (str ", " (min stealth-count stealth-target) " of " stealth-target " stealth") - "") - ")") - :choices {:card #(in-coll? (map :cid provider-cards) (:cid %))} - :effect (req (let [pay-credits-type (-> target card-def :interactions :pay-credits :type) - pay-function (if (= :custom pay-credits-type) - (-> target card-def :interactions :pay-credits :custom) - (take-counters-of-type pay-credits-type)) - custom-ability ^:ignore-async-check {:async true - :effect pay-function} - neweid (make-eid state outereid) - providing-card target] - (wait-for (resolve-ability state side neweid custom-ability providing-card [card]) - (continue-ability state side - (pick-credit-providing-cards - provider-func eid target-count stealth-target - (update selected-cards (:cid providing-card) - #(assoc % :card providing-card :number (+ (:number % 0) async-result)))) - card targets)))) - :cancel-effect pay-rest})))) + {:async true + :prompt (str "Choose a credit providing card (" + counter-count (when (and target-count (pos? target-count)) + (str " of " target-count)) + " [Credits]" + (if (pos? stealth-target) + (str ", " (min stealth-count stealth-target) " of " stealth-target " stealth") + "") + ")") + :choices {:card #(in-coll? (map :cid provider-cards) (:cid %))} + :effect (req (let [pay-credits-type (-> target card-def :interactions :pay-credits :type) + pay-function (if (= :custom pay-credits-type) + (-> target card-def :interactions :pay-credits :custom) + (take-counters-of-type pay-credits-type)) + custom-ability ^:ignore-async-check {:async true + :effect pay-function} + neweid (make-eid state outereid) + providing-card target] + (wait-for (resolve-ability state side neweid custom-ability providing-card [card]) + (continue-ability state side + (pick-credit-providing-cards + provider-func eid target-count stealth-target + (update selected-cards (:cid providing-card) + #(assoc % :card providing-card :number (+ (:number % 0) async-result)))) + card targets)))) + :cancel-effect pay-rest})))) diff --git a/src/clj/game/core/prompts.clj b/src/clj/game/core/prompts.clj index 9462603cb2..64b5614989 100644 --- a/src/clj/game/core/prompts.clj +++ b/src/clj/game/core/prompts.clj @@ -28,15 +28,16 @@ {:keys [waiting-prompt prompt-type show-discard cancel-effect end-effect targets]}] (let [prompt (if (string? message) message (message state side eid card targets)) choices (choice-parser choices) - newitem ^:ignore-async-check {:eid eid - :msg prompt - :choices choices - :effect f - :card card - :prompt-type (or prompt-type :other) - :show-discard show-discard - :cancel-effect cancel-effect - :end-effect end-effect}] + newitem ^:ignore-async-check + {:eid eid + :msg prompt + :choices choices + :effect f + :card card + :prompt-type (or prompt-type :other) + :show-discard show-discard + :cancel-effect cancel-effect + :end-effect end-effect}] (when (or (#{:waiting :run} prompt-type) (:number choices) (:card-title choices)