diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 2926aacdf9..1f26d3318b 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -8,7 +8,7 @@ update-all-agenda-points]] [game.core.bad-publicity :refer [gain-bad-publicity lose-bad-publicity]] [game.core.board :refer [all-active-installed all-installed all-installed-corp - all-installed-runner-type get-remote-names installable-servers server->zone]] + all-installed-runner-type get-remote-names installable-servers server->zone server-list]] [game.core.card :refer [agenda? asset? can-be-advanced? corp-installable-type? corp? facedown? faceup? get-advancement-requirement get-agenda-points get-card get-counters get-title get-zone has-subtype? ice? in-discard? in-hand? @@ -22,8 +22,8 @@ [game.core.drawing :refer [draw draw-up-to]] [game.core.effects :refer [register-lingering-effect]] [game.core.eid :refer [effect-completed make-eid]] - [game.core.engine :refer [checkpoint pay queue-event register-events resolve-ability - unregister-events]] + [game.core.engine :refer [checkpoint pay queue-event register-events register-default-events + resolve-ability unregister-events]] [game.core.events :refer [first-event? first-run-event? no-event? run-events run-event-count turn-events]] [game.core.finding :refer [find-latest]] [game.core.flags :refer [in-runner-scored? is-scored? register-run-flag! @@ -43,7 +43,7 @@ [game.core.prompts :refer [cancellable clear-wait-prompt show-wait-prompt]] [game.core.props :refer [add-counter add-prop]] [game.core.purging :refer [purge]] - [game.core.revealing :refer [reveal]] + [game.core.revealing :refer [reveal reveal-loud]] [game.core.rezzing :refer [derez rez rez-multiple-cards]] [game.core.runs :refer [clear-encounter end-run get-current-encounter force-ice-encounter redirect-run start-next-phase]] [game.core.say :refer [play-sfx system-msg]] @@ -51,7 +51,7 @@ [game.core.set-aside :refer [set-aside-for-me]] [game.core.shuffling :refer [shuffle! shuffle-into-deck shuffle-my-deck! shuffle-into-rd-effect]] - [game.core.tags :refer [gain-tags]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.update :refer [update!]] @@ -349,50 +349,95 @@ :effect (effect (damage eid :meat 2 {:card card}))}}) (defcard "Bacterial Programming" - (letfn [(hq-step [remaining to-trash to-hq] - {:async true - :prompt "Choose a card to move to HQ" - :choices (conj (vec remaining) "Done") - :effect (req (if (= "Done" target) - (wait-for (trash-cards state :corp to-trash {:unpreventable true :cause-card card}) - (doseq [h to-hq] - (move state :corp h :hand)) - (do - (system-msg state :corp - (str "uses " (:title card) - " to trash " (quantify (count to-trash) "card") - ", add " (quantify (count to-hq) "card") - " to HQ, and arrange the top " (quantify (- 7 (count to-trash) (count to-hq)) "card") " of R&D")) - (if (seq remaining) - (continue-ability state :corp (reorder-choice :corp (vec remaining)) card nil) - (effect-completed state :corp eid)))) - (continue-ability state :corp (hq-step - (set/difference (set remaining) (set [target])) - to-trash - (conj to-hq target)) card nil)))}) - (trash-step [remaining to-trash] - {:async true - :prompt "Choose a card to discard" - :choices (conj (vec remaining) "Done") - :effect (req (if (= "Done" target) - (continue-ability state :corp (hq-step remaining to-trash '()) card nil) - (continue-ability state :corp (trash-step - (set/difference (set remaining) (set [target])) - (conj to-trash target)) card nil)))})] + (letfn [(remove-card [remaining target] + (filterv #(not (same-card? % target)) remaining)) + (enumerate-text [phrases] + (let [phrases (filterv identity phrases)] + (cond + (zero? (count phrases)) + "" + (= 1 (count phrases)) + (first phrases) + (= 2 (count phrases)) + (str (first phrases) " and " (second phrases)) + :else + (str (first phrases) ", " (enumerate-text (rest phrases)))))) + (interact [cards remaining to-trash to-add to-top stage] + (cond + (not (seq remaining)) + (choose-one-helper + {:prompt (str (when (seq to-trash) + (str (enumerate-cards to-trash) " will be trashed. ")) + (when (seq to-add) + (str (enumerate-cards to-add) " will be added to HQ. ")) + (when (seq to-top) + (str "the top of R&D will be (top->bottom): " (enumerate-cards (reverse to-top)))))} + [{:option "OK" + :ability {:msg (msg (enumerate-text + [(when (seq to-trash) + (str "trash " (quantify (count to-trash) "card") " from R&D")) + (when (seq to-add) + (str "add " (quantify (count to-add) "card") " to HQ")) + (when (seq to-top) + (str "rearrange the top " (quantify (count to-top) "card") " of R&D"))])) + :async true + :effect (req (doseq [c to-add] + (move state side c :hand)) + (doseq [c to-trash] + (move state side c :deck {:front true})) + (wait-for + (trash-cards state side (take (count to-trash) (get-in @state [:corp :deck])) {:suppress-checkpoint true}) + (doseq [c to-top] + (move state side c :deck {:front true})) + (checkpoint state side eid)))}} + {:option "I want to start over" + :ability (interact cards cards [] [] [] :trash)}]) + (= stage :trash) + {:prompt "Choose a card to trash" + :choices (conj remaining "Done") + :async true + :effect (req (continue-ability + state side + (if (= target "Done") + (interact cards remaining to-trash to-add to-top :add) + (interact cards (remove-card remaining target) (conj to-trash target) to-add to-top stage)) + card nil))} + (= stage :add) + {:prompt "Choose a card to add to HQ" + :choices (conj remaining "Done") + :async true + :effect (req (continue-ability + state side + (if (= target "Done") + (interact cards remaining to-trash to-add to-top :order) + (interact cards (remove-card remaining target) to-trash (conj to-add target) to-top stage)) + card nil))} + ;; note - if there is one card remaining, we could just add it to the top, but I think that actually + ;; causes (player) memory issues that clicking on the card does not, so I have chosen to do it this way + ;; -nbkelly, 2026.02 + :else + {:prompt "Add a card to the top of R&D" + :choices remaining + :async true + :effect (req (continue-ability state side (interact cards (remove-card remaining target) to-trash to-add (conj to-top target) stage) card nil))}))] (let [arrange-rd {:interactive (req true) - :optional - {:waiting-prompt true - :prompt "Look at the top 7 cards of R&D?" - :yes-ability - {:async true - :msg "look at the top 7 cards of R&D" - :effect (req (let [c (take 7 (:deck corp))] - (when (and - (:access @state) - (:run @state)) - (swap! state assoc-in [:run :shuffled-during-access :rd] true)) - (continue-ability state :corp (trash-step c '()) card nil)))}}}] + :change-in-game-state {:silent true :req (req (seq (:deck corp)))} + :optional {:waiting-prompt true + :prompt "Look at the top 7 cards of R&D?" + :yes-ability {:async true + :msg "look at the top 7 cards of R&D" + :prompt (msg "The top cards of R&D are (top->bottom): " (enumerate-cards (take 7 (:deck corp)))) + :choices ["OK"] + :effect (req (let [set-aside-cards (set-aside-for-me state side eid (take 7 (:deck corp)))] + (when (and + (:access @state) + (:run @state)) + (swap! state assoc-in [:run :shuffled-during-access :rd] true)) + (continue-ability + state side + (interact set-aside-cards set-aside-cards [] [] [] :trash) + card nil)))}}}] {:on-score arrange-rd :stolen arrange-rd}))) @@ -1281,6 +1326,38 @@ :duration :end-of-run}) (effect-completed state side eid)))}}]}) +(defcard "Let Them Dream" + (letfn [(move-to [c from] + (choose-one-helper + (let [and-then (fn [s] (str (if (= from :rd) ", shuffle R&D, and then " " and ") s))] + {:prompt (str "Move " (:title c) " where?")} + [{:option "HQ" + :ability {:async true + :effect (req (when (= from :rd) (shuffle! state side :deck)) + (move state side c :hand) + (reveal-loud state side eid card {:and-then (and-then "add it to HQ")} c))}} + {:option "Bottom of R&D" + :ability {:async true + :effect (req (when (= from :rd) (shuffle! state side :deck)) + (move state side c :deck) + (reveal-loud state side eid card {:and-then (and-then "add it to the bottom of R&D")} c))}}]))) + (find-ab [zone] + {:prompt "Choose an agenda" + :show-discard (= zone :archives) + :choices (if (= zone :rd) + (req (cancellable (filter agenda? (:deck corp)) :sorted)) + {:card #(and (agenda? %) (if (= zone :hq) (in-hand? %) (in-discard? %)))}) + :effect (req (continue-ability state side (move-to target zone) card nil)) + :async true + :cancel-effect shuffle-my-deck!})] + {:on-score (choose-one-helper + {:optional true + :prompt "Search for an Agenda from where?"} + [{:option "HQ" :ability (find-ab :hq)} + {:option "R&D" :ability (find-ab :rd)} + {:option "Archives" :ability (find-ab :archives)}]) + :agendapoints-runner (req 1)})) + (defcard "License Acquisition" {:on-score {:interactive (req true) :prompt "Choose an asset or upgrade to install from Archives or HQ" @@ -1353,6 +1430,50 @@ :effect (req (wait-for (trash-cards state side targets {:unpreventable true :cause-card card}) (shuffle-into-rd-effect state side eid card 3)))}}) +(defcard "Lotus Haze" + {:on-score (agenda-counters 3) + :abilities [{:cost [(->c :agenda 1)] + :prompt "Choose an upgrade to move" + :choices {:card (every-pred upgrade? rezzed?)} + :label "Move a rezzed upgrade to the root of another server." + :waiting-prompt true + :async true + ;; note: + ;; It's not legal to even attempt to stack regions + ;; It's not legal to even attempt to move cards to zones which they cannot legally occupy + ;; This looks messy, but it's accurate, and will be enough to stop people getting game losses + ;; TODO: extend this logic to swaps (ie daruma) + ;; --nbkelly, 2026.02 + :effect (req (let [to-move target + zone (second (get-zone to-move)) + not-same-zone (fn [zones] (filter #(not= % (zone->name zone)) zones)) + legal-zones-fn (if-let [f (-> to-move card-def :legal-zones)] + (fn [zones] (f state side eid card zones)) + (fn [zones] (vec zones))) + region? #(has-subtype? % "Region") + region-restriction (fn [zones] + (if (region? target) + (filter #(let [z (last (server->zone state %)) + content (get-in @state [:corp :servers z :content])] + (not (some region? content))) + zones) + zones)) + legal-moves (-> (server-list state) not-same-zone legal-zones-fn region-restriction)] + (continue-ability + state side + (if (seq legal-moves) + {:prompt "Choose a server" + :choices (req legal-moves) + :msg (msg "move " (:title to-move) " to " target) + :effect (req (let [c (move state side to-move + (conj (server->zone state target) :content))] + (unregister-events state side to-move) + (register-default-events state side c)))} + {:prompt (str "You have no legal moves for " (:title to-move)) + :msg (msg "reveal that they have no legal moves for " (:title to-move)) + :choices ["OK"]}) + card nil)))}]}) + (defcard "Luminal Transubstantiation" {:on-score {:silent true @@ -1409,6 +1530,12 @@ :req (req (= (:title target) "Medical Breakthrough")) :value -1}]}) +(defcard "Méliès City Luxury Line" + {:steal-cost-bonus (req [(->c :click 1)]) + :on-score {:msg "gain [Click]" + :silent (req true) + :effect (req (gain-clicks state :corp 1))}}) + (defcard "Megaprix Qualifier" {:on-score {:silent true :req (req (< 1 (count (filter #(= (:title %) "Megaprix Qualifier") @@ -2062,6 +2189,27 @@ :trace {:base 2 :successful (give-tags 1)}}]}) +(defcard "Sacrifice Zone Expansion" + {:install-state :face-up + :events [{:event :advance + :condition :faceup + :req (req (and (same-card? card (:card context)) + (first-event? state side :advance #(same-card? card (:card (first %)))))) + :msg (msg "gain 3 [Credits]") + :async true + :effect (effect (gain-credits eid 3))} + {:event :successful-run + :condition :faceup + :optional {:prompt "Do 1 meat damage?" + :once :per-turn + :req (req (and (installed? card) + (not= (target-server context) (second (get-zone card))) + (can-pay? state side eid card nil [(->c :advancement 1)]))) + :yes-ability {:cost [(->c :advancement 1)] + :msg "do 1 meat damage" + :effect (req (damage state side eid :meat 1)) + :async true}}}]}) + (defcard "Salvo Testing" {:events [{:event :agenda-scored :interactive (req true) @@ -2545,3 +2693,22 @@ (not (has-subtype? target "Virtual")) (not (:facedown (second targets))))) :value 1}]}) + +(defcard "Witch Hunt" + (let [bp {:msg "take 1 bad publicity" + :async true + :effect (effect (gain-bad-publicity :corp eid 1))}] + {:stolen bp + :on-score bp + :events [{:unregister-once-resolved true + :event :corp-action-phase-ends + :duration :end-of-turn + :req (effect (first-event? :agenda-scored #(same-card? card (:card (first %))))) + :msg (msg (if tagged + "Remove all tags, and then give the Runner 3 tags" + "give the Runner 3 tags")) + :async true + :effect (req (if tagged + (wait-for (lose-tags state side :all {:suppress-checkpoint true}) + (gain-tags state side eid 3)) + (gain-tags state side eid 3)))}]})) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 29b73f63e0..01791aae96 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -54,7 +54,7 @@ [game.core.set-aside :refer [swap-set-aside-cards]] [game.core.shuffling :refer [shuffle! shuffle-into-deck shuffle-my-deck! shuffle-into-rd-effect]] - [game.core.tags :refer [gain-tags]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.threat :refer [threat-level]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] @@ -1071,6 +1071,20 @@ :req (req (installed? target)) :value 1}]}) +(defcard "Esca" + {:flags {:rd-reveal (req true)} + :poison true + :on-access {:msg "force the Runner to lose 1 [Credits]" + :async true + :effect (req (wait-for (lose-credits state :runner 1) + (continue-ability + state side + {:req (req tagged) + :msg "do 1 net damage" + :async true + :effect (req (damage state side eid :net 1))} + card nil)))}}) + (defcard "Estelle Moon" {:events [{:event :corp-install :req (req (and (or (asset? (:card context)) @@ -1843,6 +1857,41 @@ :effect (req (access-bonus state :runner target -1))} card targets))}]}) +(defcard "Luana Campos" + {:uninstall (req (continue-ability + state side + {:req (req (and (rezzed? (:old-card context)) + (pos? (get-counters (:old-card context) :bad-publicity)))) + :msg (msg "take " (get-counters (:old-card context) :bad-publicity) + " bad publicity") + :async true + :effect (req + (gain-bad-publicity + state side eid + (get-counters (:old-card context) :bad-publicity)))} + card targets)) + :events [{:event :corp-turn-begins + :interactive (req true) + :change-in-game-state {:req (req (pos? (count-bad-pub state))) :silent true} + :optional {:interactive (req true) + :prompt "Host a bad publicity counter to gain 3 [Credits] and draw a card?" + :yes-ability {:msg (msg "gain 3 [Credits] and draw 1 card") + :cost [(->c :host-bad-pub 1)] + :async true + :effect (req (wait-for + (gain-credits state side 3 {:suppress-checkpoint true}) + (draw state side eid 1)))}}}]}) + +(defcard "Magistrate Revontulet" + {:static-abilities [{:type :steal-additional-cost + :req (req (agenda? target)) + :value (req [(->c :credit 3)])}] + :events [{:event :agenda-scored + :async true + :interactive (req true) + :msg "force the Runner to lose 3 [Credits]" + :effect (req (lose-credits state :runner eid 3))}]}) + (defcard "Malia Z0L0K4" (let [unmark (req (when-let [malia-target (get-in card [:special :malia-target])] @@ -2240,6 +2289,25 @@ (do (as-agenda state :runner card -1) (effect-completed state side eid))))}}) +(defcard "Nihilo Agent" + {:data {:counter {:power 3}} + :events [(trash-on-empty :power) + {:event :corp-turn-ends + :msg "take 1 bad publicity and give the Runner 1 tag" + :async true + :effect (req (wait-for + (gain-bad-publicity state :corp 1 {:suppress-checkpoint true}) + (wait-for + (add-counter state side card :power -1 {:suppress-checkpoint true}) + (gain-tags state side eid 1))))} + {:event :corp-turn-begins + :change-in-game-state {:silent true + :req (req (or tagged (pos? (count-bad-pub state))))} + :msg "remove 1 bad publicity and 1 tag" + :async true + :effect (req (wait-for (lose-bad-publicity state :corp 1 {:suppress-checkpoint true}) + (lose-tags state side eid 1)))}]}) + (defcard "Open Forum" {:events [{:event :corp-mandatory-draw :interactive (req true) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index deb4352b88..4a783affe4 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -103,6 +103,34 @@ :this-card-run true :ability (drain-credits :runner :corp 5 2 2)})]}) +(defcard "Aircheck" + {:makes-run true + :data {:counter {:credit 4}} + :interactions {:pay-credits {:req (req run) :type :credit}} + :static-abilities [{:type :cannot-pay-credits-from-pool + :req (req (= :runner side)) + :value true} + {:type :cannot-lose-credits + :req (req (= :runner side)) + :value true}] + :on-play (run-server-from-choices-ability + ["HQ" "R&D"] + {:events [{:event :run-ends + :unregister-once-resolved true + :req (req (and (:successful context) + this-card-run + (or (= [:hq] (:server context)) + (= [:rd] (:server context))))) + :prompt "Choose a remote server to run" + :choices (req (cancellable + (->> runnable-servers + (map unknown->kw) + (filter is-remote?) + (map remote->name)))) + :msg (msg "make a run on " target) + :async true + :effect (effect (make-run eid target card))}]})}) + (defcard "Always Have a Backup Plan" {:makes-run true :on-play {:prompt "Choose a server" @@ -264,6 +292,34 @@ (move state :corp c :deck)) (shuffle! state :corp :deck))}})]}) +(defcard "Beta Build" + {:makes-run true + :on-play {:async true + :effect (req (wait-for + (resolve-ability + state side + {:prompt "Install a non-virus program" + :choices (req (cancellable (filter #(and (program? %) + (runner-can-install? state side eid % {:no-toast true})) + (:deck runner)))) + :async true + :effect (req (wait-for + (runner-install state side target {:ignore-all-cost :true :msg-keys {:display-origin true :source-card card}}) + (complete-with-result state side eid async-result)))} + card nil) + (let [installed-card async-result] + (resolve-ability state side eid + (run-any-server-ability {:events [{:event :run-ends + :unregister-once-resolved true + :duration :end-of-run + :interactive (req true) + :automatic :last + :change-in-game-state {:silent true + :req (req (get-card state installed-card))} + :msg (msg "add " (:title installed-card) " to the top of the stack") + :effect (req (move state side installed-card :deck {:front true}))}]}) + card nil))))}}) + (defcard "Black Hat" {:on-play {:trace @@ -561,6 +617,39 @@ (cbi-choice from '() (count from) from))) card nil))}})]})) +(defcard "Chain Reaction" + (let [corp-choice {:player :corp + :prompt "Choose a Runner card to trash" + :async true + :req (req (seq (all-installed state :runner))) + :choices {:card (every-pred runner? installed?)} + :waiting-prompt true + :display-side :corp + :msg (msg "trash " (:title target)) + :effect (req (trash state :corp eid target))} + cards-to-trash (fn [state] (min 2 (count (all-installed state :corp)))) + runner-choice {:prompt (msg "choose " (quantify (cards-to-trash state) "card") " to trash") + :async true + :choices {:card (every-pred corp? installed?) + :max (req (cards-to-trash state)) + :all true} + :waiting-prompt true + :msg (msg "trash " (enumerate-str (map #(card-str state %) targets))) + :effect (req (wait-for (trash-cards state side targets) + (continue-ability + state :corp + corp-choice + card nil)))}] + {:on-play {:async true + :change-in-game-state {:req (req (or (seq (all-installed state :corp)) + (seq (all-installed state :runner))))} + :req (req (and (some #{:hq} (:successful-run runner-reg)) + (some #{:rd} (:successful-run runner-reg)) + (some #{:archives} (:successful-run runner-reg)))) + :effect (req (if (seq (all-installed state :corp)) + (continue-ability state side runner-choice card nil) + (continue-ability state :corp corp-choice card nil)))}})) + (defcard "Charm Offensive" (letfn [(trash-x-opt [t] {:option (str "Trash a rezzed copy of " t) @@ -2180,6 +2269,45 @@ (defcard "Knifed" (cutlery "Barrier")) +(defcard "Kompromat" + (letfn [(iced-servers [state side eid card] + (filter #(-> (get-in @state (cons :corp (server->zone state %))) :ices count pos?) + (zones->sorted-names (get-runnable-zones state side eid card nil))))] + {:makes-run true + :on-play {:async true + :rfg-instead-of-trashing true + :change-in-game-state {:req (req (seq (iced-servers state side eid card)))} + :prompt "Choose an iced server" + :choices (req (iced-servers state side eid card)) + :effect (req (make-run state side eid target card))} + :events [{:event :run-ends + :req (req (and this-card-run (:successful context))) + :async true + :interactive (req true) + :effect (req (let [valid-ice (filter #(and (ice? %) + (rezzed? %) + (= (first (:server context)) (second (get-zone %)))) + (all-installed state :corp))] + (continue-ability + state side + (if (seq valid-ice) + {:prompt "Derez an ice? (if you click done, you take a bad publicity)" + :player :corp + :waiting-prompt true + :choices {:req (req (some #(same-card? % target) valid-ice))} + :cancel {:display-side :runner + :msg "give the Corp 1 bad publicity" + :async true + :effect (req (gain-bad-publicity state :runner eid 1))} + :msg (msg "derez " (card-str state target)) + :display-side :corp + :async true + :effect (req (derez state side eid target {:no-msg true}))} + {:msg "give the Corp 1 bad publicity" + :async true + :effect (req (gain-bad-publicity state :runner eid 1))}) + card nil)))}]})) + (defcard "Kraken" {:on-play {:req (req (:stole-agenda runner-reg)) @@ -3459,6 +3587,13 @@ (:label (:ability context))))) :value (->c :credit 1)}]}) +(defcard "Sell Out" + {:on-play {:additional-cost [(->c :resource 1)] + :async true + :msg "gain 4 [Credits] and draw 2 cards" + :effect (req (wait-for (gain-credits state side 4 {:suppress-checkpoint true}) + (draw state side eid 2)))}}) + (defcard "Shred" {:on-play (run-any-server-ability) :makes-run true @@ -3761,6 +3896,26 @@ (assoc ability :event :corp-turn-ends) (assoc ability :event :runner-turn-ends)]})) +(defcard "Tailgate" + {:makes-run true + :on-play (run-server-ability + :hq + {:play-cost-bonus (req (- (count (get-in @state [:corp :servers :hq :ices]))))}) + :events [{:event :successful-run + :silent (req true) + :req (req (and (= :hq (target-server context)) this-card-run)) + :effect (effect (register-events + card [(breach-access-bonus :hq 2 {:duration :end-of-run})]))}]}) + +(defcard "Take a Dive" + {:on-play (run-server-from-choices-ability ["HQ" "R&D"] {:rfg-instead-of-trashing true}) + :events [{:event :successful-run + :req (req (letfn [(valid-ctx? [[ctx]] (pos? (or (:subroutines-fired ctx) 0)))] + (valid-ctx? [context]))) + :msg "force the Corp to take 1 Bad Publicity" + :async true + :effect (req (gain-bad-publicity state :corp eid 1 {:card card}))}]}) + (defcard "Test Run" {:on-play {:prompt (req (if (not (zone-locked? state :runner :discard)) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index d24006844b..c342be1c30 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -52,7 +52,7 @@ get-current-encounter jack-out make-run successful-run-replace-breach total-cards-accessed]] [game.core.say :refer [play-sfx system-msg]] - [game.core.servers :refer [target-server is-central?]] + [game.core.servers :refer [target-server is-central? zone->name]] [game.core.shuffling :refer [shuffle!]] [game.core.tags :refer [gain-tags lose-tags]] [game.core.threat :refer [threat-level]] @@ -444,6 +444,14 @@ :deck) (shuffle! :deck))}}}])))}]})) +(defcard "Borrowed Goods" + {:on-install {:change-in-game-state {:req (req (not tagged)) :silent true} + :msg "take 1 tag" + :interactive (req true) + :async true + :effect (req (gain-tags state side eid 1))} + :static-abilities [(mu+ 1)]}) + (defcard "Box-E" {:static-abilities [(mu+ 2) (runner-hand-size+ 2)]}) @@ -1604,6 +1612,22 @@ (defcard "MemStrips" {:static-abilities [(virus-mu+ 3)]}) +(defcard "Methuselah" + {:interactions {:pay-credits {:req (req run) + :type :credit}} + :events [{:event :run + :change-in-game-state {:req (req (seq (:hand runner))) :silent true} + :skippable true + :interactive (req true) + :prompt "Trash a hardware from the Grip?" + :choices {:card (every-pred hardware? in-hand?)} + :async true + :waiting-prompt true + :msg (msg "trash " (:title target) " and place 2 [Credits] on itself") + :effect (req (wait-for (trash state side target {:unpreventable true}) + (add-counter state side eid card :credit 2)))}] + :static-abilities [(mu+ 1)]}) + (defcard "Mind's Eye" {:implementation "Power counters added automatically" :static-abilities [(mu+ 1)] @@ -2207,6 +2231,23 @@ :effect (effect (continue-ability ability card nil))}] :abilities [ability]})) +(defcard "Rotary" + {:static-abilities [(mu+ 1)] + :events [{:event :breach-server + :automatic :pre-breach + :optional {:req (req (or (= target :rd) (= target :hq))) + :prompt "Tag 1 tag to see an additional card?" + :yes-ability {:cost [(->c :gain-tag 1)] + :msg (msg "access 1 additional card from " (zone->name target)) + :effect (effect (access-bonus target 1))}}}] + :corp-abilities [{:action true + :label "Trash Rotary" + :async true + :cost [(->c :click 1) (->c :credit 2)] + :req (req (and tagged (= :corp side))) + :effect (effect (system-msg :corp "spends [Click] and 2 [Credits] to trash Rotary") + (trash :corp eid card {:cause-card card}))}]}) + (defcard "Rubicon Switch" {:abilities [{:action true :cost [(->c :click 1)(->c :x-credits)] @@ -2535,6 +2576,20 @@ (has-subtype? target "Icebreaker"))) :type :recurring}}}) +(defcard "The Tungsten Tailor" + {:static-abilities [{:type :ice-strength + :req (req (ice? target)) + :value -1}] + :events [{:event :subroutines-broken + :async true + :once-per-instance true + :automatic :gain-credits + :req (req (letfn [(valid-ctx? [[ctx]] (:was-zero-or-less-strength? ctx))] + (and (valid-ctx? targets) + (first-event? state side :subroutines-broken valid-ctx?)))) + :msg "gain 1 [Credits]" + :effect (req (gain-credits state side eid 1))}]}) + (defcard "The Wizard's Chest" (letfn [(install-choice [state side eid card rev-str first-card second-card] (continue-ability @@ -2648,6 +2703,15 @@ (effect-completed state nil eid) (access-card state side eid (nth (:deck corp) (dec (str->int target))) "an unseen card")))}})]}) +(defcard "Touchstone" + {:events [{:event :play-event + :req (req (first-event? state side :play-event)) + :async true + :silent (req true) + :effect (req (add-counter state side eid card :credit 1))}] + :interactions {:pay-credits {:req (req run) + :type :credit}}}) + (defcard "Turntable" {:static-abilities [(mu+ 1)] :events [{:event :agenda-stolen diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index 7721b996b3..4accd6a5a1 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -17,7 +17,7 @@ [game.core.costs :refer [total-available-credits]] [game.core.damage :refer [damage]] [game.core.def-helpers :refer [combine-abilities corp-recur defcard - do-brain-damage do-net-damage draw-abi give-tags make-icon offer-jack-out + do-brain-damage do-net-damage draw-abi give-tags make-icon move-card-to-top-or-bottom offer-jack-out reorder-choice get-x-fn with-revealed-hand]] [game.core.drawing :refer [draw maybe-draw draw-up-to]] [game.core.effects :refer [any-effects get-effects is-disabled? is-disabled-reg? register-lingering-effect unregister-effects-for-card unregister-effect-by-uuid unregister-static-abilities update-disabled-cards]] @@ -51,7 +51,7 @@ [game.core.revealing :refer [reveal reveal-loud]] [game.core.rezzing :refer [can-pay-to-rez? derez get-rez-cost rez]] [game.core.runs :refer [bypass-ice encounter-ends end-run - force-ice-encounter get-current-encounter prevent-access + force-ice-encounter get-current-encounter jack-out prevent-access redirect-run set-next-phase]] [game.core.say :refer [play-sfx system-msg]] [game.core.servers :refer [central->name protecting-same-server? @@ -743,6 +743,22 @@ cannot-steal-or-trash-sub] :runner-abilities [(bioroid-break 1 1)]}) +(defcard "Ansel 2.0" + {:runner-abilities [(bioroid-break 2 2)] + :subroutines [trash-installed-sub + {:label "Remove 1 card in the Heap from the game" + :change-in-game-state {:silent true + :req (req (and (seq (:discard runner)) + (not (zone-locked? state :runner :discard))))} + :prompt "Choose a card in the heap to remove from the game" + :show-opponent-discard true + :waiting-prompt true + :choices {:card (every-pred runner? in-discard?)} + :msg (msg "remove " (:title target) " from the game") + :effect (req (move state :runner target :rfg))} + (install-from-hq-or-archives-sub) + end-the-run]}) + (defcard "Anvil" (letfn [(encounter-ab [] {:optional {:prompt "Trash another card?" @@ -1769,12 +1785,56 @@ sub sub]})) +(defcard "Event Horizon" + {:subroutines [(choose-one-helper + {:label "Trash 1 program unless runner pays 3 [Credits]" + :player :runner} + [(cost-option [(->c :credit 3)] :runner) + {:option "The Corp trashes a Program" + :ability {:async true + :effect (req (continue-ability state :corp trash-program-sub card nil))}}]) + (end-the-run-unless-runner-pays (->c :credit 3))] + :abilities [{:label "End the run" + :msg "end the run" + :async true + :req (req this-server run) + :cost [(->c :trash-can)] + :effect (req (end-run state side eid card))}]}) + (defcard "Excalibur" {:subroutines [prevent-runs-this-turn]}) (defcard "Executive Functioning" {:subroutines [(trace-ability 4 (do-brain-damage 1))]}) +(defcard "ezaM" + {:subroutines [{:label "Look at the top card of R&D" + :change-in-game-state {:silent true :req (req (seq (:deck corp)))} + :waiting-prompt true + :prompt (msg "The top card of R&D is " (get-in @state [:corp :deck 0 :title])) + :choices (req [(when (not= (count (:deck corp)) 1) + "Place it on the bottom of R&D") + "Done"]) + :msg (msg "look at the top card of R&D" (when-not (= target "Done") " and add it to the bottom of R&D")) + :effect (req (when-not (= target "Done") (move state side (first (:deck corp)) :deck)))} + {:label "Each piece of ice gets +1 strength for the remainder of this run." + :msg "give +1 strength to all ice for the remainder of the run" + :effect (effect (register-lingering-effect + card + (let [c-ice card] + {:type :ice-strength + :duration :end-of-run + :value 1})) + (update-all-ice))}] + :abilities [{:cost [(->c :click 1)] + :action true + :label "Swap this ice with another installed ice." + :choices {:req (req (and (ice? target) + (installed? target) + (not (same-card? card target))))} + :msg (msg "swap itself with " (card-str state target)) + :effect (req (swap-ice state side card target))}]}) + (defcard "F2P" {:subroutines [add-runner-card-to-grip (give-tags 1)] @@ -1920,6 +1980,15 @@ (purge state side eid))} :subroutines [end-the-run]}) +(defcard "Flywheel" + (let [sub {:label "Gain 1 [Credit]. You may draw 1 card" + :async true + :msg "gain 1 [Credit]" + :effect (req (wait-for + (gain-credits state side 1) + (maybe-draw state side eid card 1)))}] + {:subroutines [sub sub]})) + (defcard "Formicary" {:derezzed-events [{:event :approach-server @@ -2020,6 +2089,14 @@ {:on-rez take-bad-pub :subroutines [trash-program-sub]}) +(defcard "Grubber" + {:on-rez {:change-in-game-state {:silent true :req (req (protecting-a-central? card))} + :async true + :msg "take 1 bad publicity" + :effect (req (gain-bad-publicity state side eid 1))} + :subroutines [(end-the-run-unless-runner-pays (->c :credit 3)) + (end-the-run-unless-runner-pays (->c :credit 3))]}) + (defcard "Guard" {:static-abilities [{:type :bypass-ice :req (req (same-card? card target)) @@ -2672,6 +2749,27 @@ :effect (effect (continue-ability on-rez-ability card nil))} :no-ability {:effect (effect (system-msg :corp (str "declines to use " (:title card))))}}}})) +(defcard "Knowledge Seeker" + {:events [{:event :end-of-encounter + :req (req (and (= (:ice context) card) + (>= (get-counters card :virus) 3))) + :interactive (req true) + :async true + :msg "purge virus counters and derez itself" + :effect (req (wait-for (derez state side card) + (play-sfx state side "virus-purge") + (purge state side eid)))}] + :subroutines [{:label "Place 1 virus counter on this card" + :msg "place 1 virus counter on itself" + :effect (req (add-counter state side eid card :virus 1)) + :async true} + {:label "Rearrange the top 4 cards of R&D" + :async true + :waiting-prompt true + :change-in-game-state {:silent true :req (req (seq (:deck corp)))} + :effect (req (resolve-ability state side eid (reorder-choice :corp (take 4 (:deck corp))) card targets))} + end-the-run]}) + (defcard "Komainu" {:on-encounter {:interactive (req true) :effect (req (let [sub-count (count (:hand runner))] @@ -2728,6 +2826,51 @@ (defcard "Lancelot" (grail-ice trash-program-sub)) +(defcard "Lethe" + {:events [(merge + (give-tags 1) + {:event :bypassed-ice + :req (req (same-card? card target))}) + (merge + (give-tags 1) + {:event :subroutines-broken + :req (req (and (same-card? (:ice context) card) + (:all-subs-broken context)))})] + :subroutines [{:change-in-game-state {:silent true :req (req (seq (:discard corp)))} + :label "add card from Archives to R&D" + :prompt "Choose a card to add to the top or bottom of R&D" + :show-discard true + :choices {:card #(and (corp? %) + (in-discard? %))} + :async true + :effect (req (continue-ability + state side + (move-card-to-top-or-bottom target) + card nil))} + add-runner-card-to-grip]}) + +(defcard "Lionsmane" + {:subroutines (let [two-net-option {:option "Corp does 2 net damage" + :ability {:msg "do 2 net damage" + :display-side :corp + :async true + :effect (req (damage state :corp eid :net 2))}}] + [(do-net-damage 2) + (choose-one-helper + {:label "Do 2 net damage unless the Runner pays 3 [Credits]" + :player :runner} + [(cost-option [(->c :credit 3)] :runner) + two-net-option]) + (choose-one-helper + {:label "Do 2 net damage unless the Runner jacks out" + :player :runner} + [{:option "Jack out" + :ability {:msg "jack out" + :display-side :runner + :async true + :effect (req (jack-out state :runner eid))}} + two-net-option])])}) + (defcard "Little Engine" {:subroutines [end-the-run end-the-run @@ -3480,6 +3623,10 @@ :effect (effect (trash :corp eid card {:cause-card card :cause :effect}))}] :subroutines [end-the-run]}) +(defcard "Paywall" + {:on-encounter (runner-loses-credits 1) + :subroutines [(end-the-run-unless-runner-pays (->c :credit 1))]}) + (defcard "Peeping Tom" (let [sub (end-the-run-unless-runner "takes 1 tag" @@ -3625,6 +3772,14 @@ {:static-abilities [(ice-strength-bonus (req (count-tags state)))] :subroutines [(trace-ability 4 end-the-run)]}) +(defcard "Reverb" + {:rez-cost-bonus (req (- (count (filter #(and (ice? %) + (not (same-card? card %)) + (not (rezzed? %))) + (all-installed state :corp))))) + :subroutines [end-the-run + end-the-run]}) + (defcard "Rime" {:implementation "Can be rezzed anytime already" :on-rez {:effect (effect (update-all-ice))} @@ -3887,6 +4042,21 @@ :msg "make the Runner breach R&D" :effect (effect (breach-server :runner eid [:rd] {:no-root true}))}}}]}) +(defcard "Sleipnir" + {:subroutines [(maybe-draw-sub 1) + {:prompt "Shuffle up 1 card from HQ or Archives into R&D?" + :label "You may shuffle 1 card from HQ or Archives into R&D" + :show-discard true + :choices {:card #(and (corp? %) + (or (in-hand? %) + (in-discard? %)))} + :async true + :msg (msg "shuffle " (card-str state target) " into R&D") + :effect (req (move state :corp target :deck) + (shuffle! state :corp :deck) + (effect-completed state :corp eid))} + end-the-run]}) + (defcard "Slot Machine" (letfn [(top-3 [state] (take 3 (get-in @state [:runner :deck]))) (top-3-names [cards] (map #(str (:title %) " (" (:type %) ")") cards)) @@ -4250,6 +4420,44 @@ :effect (effect (derez eid card))}}} :subroutines [end-the-run]}) +(defcard "Tocsin" + (letfn [(next-t [t] (when (= t "Barrier") "Sentry")) + (search-for-type [t chosen] + (if t + {:prompt (str "Pick a " t " to add to HQ") + :choices (req (cancellable (filter #(has-subtype? % t) (:deck corp)) :sorted)) + :async true + :effect (req (continue-ability state side (search-for-type (next-t t) (conj chosen target)) card nil)) + :cancel {:async true + :effect (req (continue-ability + state side + (search-for-type (next-t t) chosen) + card nil))}} + (choose-one-helper + {:prompt (if (seq chosen) + (str "You will tutor " (enumerate-cards chosen)) + "You will shuffle R&D")} + [{:option "OK" + :ability (if (seq chosen) + {:async true + :effect (req (wait-for + (reveal-loud state side card {:and-then ", and add [them] to HQ"} (vec chosen)) + (doseq [c chosen] (move state :corp c :hand)) + (shuffle! state :corp :deck) + (effect-completed state side eid)))} + {:msg "shuffle R&D" + :effect (req (shuffle! state side :deck))})} + {:option "I want to start over" + :ability (search-for-type "Barrier" #{})}])))] + {:subroutines [(runner-loses-credits 2) + end-the-run + end-the-run] + :expend {:change-in-game-state {:req (req (seq (:deck corp)))} + :cost [(->c :credit 1)] + :msg "search R&D for up to 1 barrier and up to 1 sentry" + :async true + :effect (req (continue-ability state side (search-for-type "Barrier" #{}) card nil))}})) + (defcard "Tollbooth" {:on-encounter {:async true :effect (req (wait-for (pay state :runner (make-eid state eid) card [(->c :credit 3)]) @@ -4470,6 +4678,42 @@ (runner-loses-credits 2) (trace-ability 2 (give-tags 1))]}) +(defcard "Vertigo" + ;; "When the Runner passes this ice, if they have no remaining {click}, they + ;; cannot steal or trash cards for the remainder of this run. + ;; {sub} The Runner loses {click}." + {:events [{:event :pass-ice + :req (req (same-card? (:ice context) card)) + :change-in-game-state {:silent true :req (req (zero? (:click runner)))} + :msg "prevent the Runner from stealing or trashing Corp cards for the remainder of the run" + :effect (effect (register-run-flag! + card :can-steal + (fn [state _side _card] + ((constantly false) + (toast state :runner "Cannot steal due to Vertigo." "warning")))) + (register-run-flag! + card :can-trash + (fn [state _side card] + ((constantly (not (corp? card))) + (toast state :runner "Cannot trash due to Vertigo." "warning")))))}] + :subroutines [runner-loses-click]}) + +(defcard "Vicsek" + {:subroutines [{:label "Do X damage and give the Runner X tags." + :async true + :change-in-game-state {:silent true :req (req tagged)} + :effect (req (let [x (count-tags state)] + (wait-for (gain-tags state :side x {:suppress-checkpoint true}) + (damage state side eid :net x))))} + {:label "Give the Runner 1 tag. Trash this ice." + :async true + :msg (msg "give the runner 1 tag") + :effect (req (wait-for + (gain-tags state side 1) + (wait-for + (trash state side card {:cause :subroutine}) + (encounter-ends state side eid))))}]}) + (defcard "Vikram 1.0" {:implementation "Program prevention is not implemented" :subroutines [{:msg "prevent the Runner from using programs for the remainder of this run"} diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index a32bb9c820..3343e9bab3 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -52,7 +52,7 @@ [game.core.say :refer [system-msg]] [game.core.servers :refer [central->name is-central? is-remote? name-zone target-server zone->name]] - [game.core.shuffling :refer [shuffle! shuffle-into-deck shuffle-cards-into-deck! fail-to-find!]] + [game.core.shuffling :refer [fail-to-find! shuffle! shuffle-into-deck shuffle-cards-into-deck! shuffle-my-deck!]] [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] @@ -755,6 +755,20 @@ :req (req (:flipped card)) :effect flip-effect}]})) +(defcard "Editorial Division: Ad Nihilum" + {:events [{:event :corp-gain-bad-publicity + :optional {:req (req (let [valid-ctx? (fn [[ctx]] (pos? (:amount ctx)))] + (and (valid-ctx? targets) + (first-event? state side :corp-gain-bad-publicity valid-ctx?)))) + :prompt "Search for a card?" + :waiting-prompt true + :yes-ability {:prompt "Choose a card" + :msg (msg "add " (:title target) " to HQ from R&D") ;; TODO - once the illicit->liability change is through, we can adjust this (or just leave it) + :choices (req (cancellable (filter #(has-any-subtype? % ["Illicit" "Black Ops" "Gray Ops" "Liability"]) (filter (complement agenda?) (:deck corp))) :sorted)) + :cancel shuffle-my-deck! + :effect (effect (shuffle! :deck) + (move target :hand))}}}]}) + (defcard "Edward Kim: Humanity's Hammer" {:events [{:event :access :req (req (and (operation? target) @@ -1036,6 +1050,18 @@ :prompt-type :bogus})) card nil))}]}) +(defcard "Hiram \"0mission\" Svensson: Shadow of the Past" + (let [scry {:change-in-game-state {:silent (req true) + :req (req (seq (:deck corp)))} + :msg "look at the top card of R&D" + :interactive (req true) + :skippable true + :waiting-prompt true + :prompt (msg "The top card of R&D is " (:title (first (:deck corp)))) + :choices ["Noted"]}] + {:events [(assoc scry :event :runner-install :req (req (hardware? (:card context)))) + (assoc scry :event :runner-trash :req (req (some hardware? (map :card targets))))]})) + (defcard "Hoshiko Shiro: Untold Protagonist" (let [flip-effect (req (update! state side (if (:flipped card) (assoc card @@ -1480,6 +1506,66 @@ :events [(assoc ability :event :runner-turn-begins)] :abilities [ability]})) +(defcard "Méliès U: Only the Brightest" + {:events [;; At game start, you're on the front face + {:event :pre-first-turn + :req (req (= side :corp)) + :effect (effect (update! + (assoc card + :face :front + :melies-target (first (shuffle ["HQ" "R&D" "Archives"])))) + (system-msg "reveals that the three hidden faces of Méliès U: Only the Brightest are: Tenure Floors: Méliès U, Subsurface Labs: Méliès U, and Disposal Grounds: Méliès U"))} + ;; When your turn ends, you secretly choose a server + {:event :corp-turn-ends + :prompt "Choose a server" + :interactive (req true) + :waiting-prompt true + :choices ["HQ" "R&D" "Archives"] + :msg (msg "secretly choose a server") + :effect (req (update! state side (assoc card :melies-target target)))} + ;; When the runner discard phase ends while you're on the front, you gain 1c + {:event :runner-turn-ends + :req (req (= (:face card) :front)) + :msg "gain 1 [Credit]" + :async true + :effect (req (gain-credits state side eid 1))} + ;; when our turn begins and we are not on the front face, we flip + {:event :corp-turn-begins + :silent (req true) + :effect (effect (update! (assoc card :face :front)))} + ;; When the runner makes a successful run on a central + ;; while we're on a front face, we flip and maybe do something + {:event :successful-run + :req (req (and (= (:face card) :front) (is-central? (:server context)))) + :msg (msg "flip to " + (case (:melies-target card) + "HQ" "Tenure Floors: Méliès U" + "R&D" "Subsurface Labs: Méliès U" + "Archives" "Disposal Grounds: Méliès U" + "this shouldn't occur")) + :async true + :effect (req (let [[target-zone face] (case (:melies-target card) + "HQ" [:hq :tenure] + "R&D" [:rd :subsurface] + "Archives" [:archives :disposal] + [:hq :tenure])] + (update! state side (assoc card :face face)) + (if (and (-> context :server first (= target-zone)) + (seq (:deck corp))) + (continue-ability + state side + {:optional + {:prompt (msg "The top card of R&D is " (:title (first (:deck corp))) ". Trash it?") + :waiting-prompt true + :req (req (seq (:hand runner))) + :yes-ability {:cost [(->c :trash-from-deck 1)] + :once :per-turn + :msg "add 1 card from Archives to HQ" + :async true + :effect (effect (continue-ability (corp-recur) card nil))}}} + card nil) + (effect-completed state side eid))))}]}) + (defcard "Mercury: Chrome Libertador" {:events [{:event :breach-server :automatic :pre-breach @@ -2744,6 +2830,19 @@ ;; This doesn't use `gain-bad-publicity` to avoid the event :effect (effect (gain :corp :bad-publicity 1))}]}) +(defcard "Virtual Intelligence, P.I.: \"You Can Call Me Vic\"" + {:abilities [{:cost [(->c :click 1) (->c :credit 1)] + :action true + :once :per-turn + :label "Draw 1 card and remove 1 tag." + :msg (msg (if tagged "draw 1 card and remove 1 tag" "draw 1 card")) + :async true + :change-in-game-state {:req (req (or tagged (seq (:deck runner))))} + :effect (req (if tagged + (wait-for (draw state side 1 {:suppress-checkpoint true}) + (lose-tags state side eid 1)) + (draw state side eid 1)))}]}) + (defcard "Weyland Consortium: Because We Built It" {:recurring 1 :interactions {:pay-credits {:req (req (let [ab-target (:card (get-ability-targets eid))] diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index a232763872..d3c7a74727 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -21,7 +21,7 @@ [game.core.drawing :refer [draw]] [game.core.effects :refer [register-lingering-effect]] [game.core.eid :refer [effect-completed make-eid make-result]] - [game.core.engine :refer [do-nothing pay register-events resolve-ability should-trigger? unregister-events]] + [game.core.engine :refer [checkpoint do-nothing pay register-events resolve-ability should-trigger? unregister-events]] [game.core.events :refer [event-count first-event? last-turn? no-event? not-last-turn? turn-events ]] [game.core.flags :refer [can-score? clear-persistent-flag! in-corp-scored? in-runner-scored? is-scored? @@ -29,7 +29,7 @@ [game.core.gaining :refer [gain-clicks gain-credits lose-clicks lose-credits]] [game.core.hand-size :refer [runner-hand-size+]] - [game.core.ice :refer [update-all-ice]] + [game.core.ice :refer [resolve-subroutine! unbroken-subroutines-choice update-all-ice]] [game.core.identities :refer [disable-identity enable-identity]] [game.core.initializing :refer [ability-init card-init]] [game.core.installing :refer [corp-install corp-install-msg install-as-condition-counter]] @@ -47,6 +47,7 @@ [game.core.rezzing :refer [can-pay-to-rez? derez rez rez-multiple-cards]] [game.core.runs :refer [end-run make-run]] [game.core.say :refer [system-msg]] + [game.core.set-aside :refer [set-aside-for-me]] [game.core.servers :refer [is-remote? remote->name zone->name]] [game.core.shuffling :refer [shuffle! shuffle-into-deck shuffle-my-deck! shuffle-into-rd-effect]] @@ -617,6 +618,85 @@ :msg "give the Runner 2 tags" :effect (effect (gain-tags :runner eid 2))}]}) +(defcard "Caveat Emptor" + {:on-play (choose-one-helper + [{:option "Gain 6 [Credits]. Runner has -1 [Click] next turn" + :ability {:msg "Gain 6 [Credits] and give the Runner -1 alotted [Click] next turn" + :async true + :effect (req (swap! state update-in [:runner :extra-click-temp] (fnil dec 0)) + (gain-credits state side eid 6))}} + {:option "Gain 10 [Credits]. Runner has +1 [Click] next turn" + :ability {:msg "Gain 10 [Credits] and give the Runner +1 alotted [Click] next turn" + :async true + :effect (req (swap! state update-in [:runner :extra-click-temp] (fnil inc 0)) + (gain-credits state side eid 10))}}])}) + +(defcard "Cultivate" + (letfn [(remove-card [remaining target] + (filterv #(not (same-card? % target)) remaining)) + (interact [cards remaining to-trash to-add to-top] + (cond + (not (seq remaining)) + (choose-one-helper + {:prompt (str (:title to-trash) " will be trashed, " + (:title to-add) " will be added to HQ" + (when (seq to-top) + (str ", and the top of R&D will be (top->bottom): " (enumerate-cards (reverse to-top)))))} + [{:option "OK" + :ability {:msg (msg "trash a card from among the top " (count cards) " cards of R&D" + (if (seq to-top) ", " " and ") + "add another one of those cards to HQ" + (when (seq to-top) ", and re-arrange the remainder")) + :async true + :effect (req (move state side to-add :hand) + (move state side to-trash :deck {:front true}) + ;; note - card is trashed from R&D (relevant for nuvem) + (wait-for + (trash state side (get-in @state [:corp :deck 0]) {:suppress-checkpoint true}) + (doseq [c to-top] + (move state side c :deck {:front true})) + (checkpoint state side eid)))}} + {:option "I want to start over" + :ability (interact cards cards nil nil [])}]) + (not to-trash) + {:prompt "Choose a card to trash" + :choices remaining + :async true + :effect (req (continue-ability state side (interact cards (remove-card remaining target) target nil []) card nil))} + (not to-add) + {:prompt "Choose a card to add to HQ" + :choices remaining + :async true + :effect (req (continue-ability state side (interact cards (remove-card remaining target) to-trash target []) card nil))} + ;; note - if there is one card remaining, we could just add it to the top, but I think that actually + ;; causes (player) memory issues that clicking on the card does not, so I have chosen to do it this way + ;; -nbkelly, 2026.02 + :else + {:prompt "Add a card to the top of R&D" + :choices remaining + :async true + :effect (req (continue-ability state side (interact cards (remove-card remaining target) to-trash to-add (conj to-top target)) card nil))}))] + {:on-play {:msg (msg (if (= 1 (count (:deck corp))) + "trash the top card of R&D" + (str "look at the top " (count (:deck corp)) " cards of R&D"))) + :change-in-game-state {:req (req (seq (:deck corp)))} + :async true + :effect (req (if (= 1 (count (:deck corp))) + (trash state side eid (first (:deck corp))) + (let [set-aside-cards (set-aside-for-me state side eid (take 5 (:deck corp))) + set-aside-eid eid] + (continue-ability + state side + {:prompt (str "The top cards of R&D are (top->bottom): " (enumerate-cards set-aside-cards)) + :waiting-prompt true + :choices ["OK"] + :async true + :effect (req (continue-ability + state side + (interact set-aside-cards set-aside-cards nil nil []) + card nil))} + card nil))))}})) + (defcard "Celebrity Gift" {:on-play {:choices {:max 5 @@ -1906,6 +1986,11 @@ :effect (req (wait-for (trash-cards state side targets {:cause-card card}) (gain-tags state :corp eid (count targets))))}}) +(defcard "Myōshu" + {:on-play {:req (req (not (no-event? state side :agenda-scored #(->> % first :scored-card :installed (not= :this-turn))))) + :msg "add itself to [their] score area as an Agenda worth 2 points" + :effect (req (as-agenda state side card 2))}}) + (defcard "Nanomanagement" {:on-play (gain-n-clicks 2)}) @@ -2319,6 +2404,53 @@ :msg (msg "do " (:stole-agenda runner-reg-last 0) " meat damage") :effect (effect (damage eid :meat (:stole-agenda runner-reg-last 0) {:card card}))}}}}) +(defcard "realloc()" + {:on-play {:change-in-game-state {:req (req (seq (filter (every-pred rezzed? ice?) + (all-installed state :corp))))} + :waiting-prompt true + :prompt "Choose any number of ice to derez" + :choices {:req (req (and (rezzed? target) + (ice? target) + (installed? target))) + :all true + :max (req (min (count (filter (every-pred rezzed? ice?) + (all-installed state :corp))) + 2))} + :async true + :msg (msg "derez " (enumerate-cards targets) " and gain " + (reduce + 0 (mapv :cost targets)) " [Credits]") + :effect (req (let [c (reduce + 0 (mapv :cost targets))] + (wait-for (derez state side targets) + (gain-credits state side eid c))))}}) + +(defcard "Reanimation Protocol" + {:on-play + {:prompt "Choose an Ice to install and rez (paying a total of 10 less)" + :show-discard true + :choices {:card (every-pred corp? ice? in-discard?)} + :async true + :waiting-prompt true + :effect (req (wait-for + (corp-install state side target nil {:ignore-all-cost true + :msg-keys {:install-source card + :display-origin true} + :install-state :rezzed + :combined-credit-discount 10}) + (if-let [installed-card async-result] + (cond + (and (rezzed? installed-card) + (has-any-subtype? installed-card ["Liability" "Illicit"])) + (effect-completed state side eid) + (rezzed? installed-card) + (continue-ability + state side + {:msg "take 1 bad publicity" + :async true + :effect (req (gain-bad-publicity state side eid 1))} + card nil) + :else (effect-completed state side eid)) + (effect-completed state side eid))))}}) + (defcard "Reclamation Order" {:on-play {:prompt "Choose a card from Archives" @@ -2501,6 +2633,18 @@ (defcard "Restructure" {:on-play (gain-credits-ability 15)}) +(defcard "Retirement Plan" + {:on-play {:prompt "Install an Asset, Ice or Agenda from Archives" + :change-in-game-state {:req (req (some #(or (asset? %) (ice? %) (agenda? %) (not (:seen %))) (:discard corp)))} + :show-discard true + :not-distinct true + :choices {:card #(and (or (ice? %) (asset? %) (agenda? %)) + (corp? %) + (in-discard? %))} + :async true + :effect (effect (corp-install eid target nil {:msg-keys {:install-source card + :display-origin true}}))}}) + (defcard "Retribution" {:on-play (trash-type "program of piece of hardware" #(or (program? %) (hardware? %)) :loud 1 nil {:req (req tagged)})}) @@ -2624,6 +2768,27 @@ (reveal state side cards) (trash-cards state side eid cards {:unpreventable true :cause-card card}))))}}) +(defcard "Scapegoat" + {:on-play (choose-one-helper + {:player :runner} + [{:option "Corp removes 2 bad publicity" + :ability {:async true + :display-side :corp + :msg "remove 2 bad publicity" + :effect (req (lose-bad-publicity state :corp eid 2))}} + {:option "Corp shuffles 1 Runner card into the Stack" + :ability {:change-in-game-state {:req (req (seq (all-installed state :runner)))} + :player :corp + :prompt "Shuffle an installed Runner card into the stack" + :choices {:max 1 + :card (every-pred runner? installed?) + :all true} + :display-side :corp + :msg (msg "shuffle " (enumerate-str (map :title targets)) " into the Stack") + :effect (req (doseq [t targets] + (move state :runner t :deck)) + (shuffle! state :runner :deck))}}])}) + (defcard "Scapenet" {:on-play {:trace @@ -3302,6 +3467,27 @@ :async true :effect (req (gain-bad-publicity state side eid 1))}}}) +(defcard "Unleash" + {:on-play {:additional-cost [(->c :tag 1)] + :change-in-game-state {:req (req (some (every-pred ice? (complement rezzed?)) (all-installed state :corp)))} + :choices {:card (every-pred ice? installed? (complement rezzed?))} + :async true + :effect (req (wait-for + (rez state side target {:ignore-cost :all-costs}) + (let [rezzed-card (get-card state target)] + (if (and rezzed-card (rezzed? rezzed-card) (seq (:subroutines rezzed-card))) + (continue-ability + state side + {:prompt "Choose a subroutine to resolve" + :choices (req (unbroken-subroutines-choice rezzed-card)) + :msg (msg "resolve the subroutine (\"[subroutine] " + target "\") from " (:title rezzed-card)) + :async true + :effect (req (let [sub (first (filter #(= target (make-label (:sub-effect %))) (:subroutines rezzed-card)))] + (resolve-subroutine! state side eid rezzed-card (assoc sub :external-trigger true))))} + card nil) + (effect-completed state side eid)))))}}) + (defcard "Violet Level Clearance" {:on-play (clearance 8 4)}) @@ -3310,6 +3496,12 @@ {:psi {:req (req (seq (:scored runner))) :not-equal (trash-type "resource" resource? :loud)}}}) +(defcard "Vulture Fund" + {:on-play {:msg "gain 14 [Credits] and take 1 bad publicity" + :async true + :effect (req (wait-for (gain-credits state side 14 {:suppress-checkpoint true}) + (gain-bad-publicity state side eid 1)))}}) + (defcard "Wake Up Call" {:on-play {:rfg-instead-of-trashing true diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index a90a06ac78..0f18e05e57 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -55,6 +55,7 @@ [game.core.say :refer [play-sfx system-msg]] [game.core.servers :refer [central->name is-central? is-remote? protecting-same-server? remote->name target-server unknown->kw zone->name]] + [game.core.set-aside :refer [get-set-aside set-aside-for-me]] [game.core.shuffling :refer [shuffle! shuffle-my-deck!]] [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] @@ -559,6 +560,27 @@ :hosted-gained gain-abis :hosted-lost gain-abis})) +(defcard "Baker" + (letfn [(switch-server [key serv] + {:option (str "Switch to " serv) + :cost [(->c :credit 1 {:stealth :all-stealth})] + :ability {:msg (str "change the attacked server to " serv) + :effect (req (swap! state assoc-in [:run :server] [key]))}})] + {:abilities [(run-server-ability + :archives + {:action true + :cost [(->c :click 1)] + :once :per-turn + :events [(choose-one-helper + {:event :pre-approach-server + :req (req (= :archives (-> run :server first))) + :duration :end-of-run + :unregister-once-resolved true + :interactive (req true) + :optional true} + [(switch-server :hq "HQ") + (switch-server :rd "R&D")])]})]})) + (defcard "Bankroll" {:special {:auto-place-credit :always} :events [{:event :successful-run @@ -2834,6 +2856,48 @@ :duration :while-active :value (req (get-counters card :power))}]})) +(defcard "Read-Write Share" + (let [ab {:interactive (req true) + :prompt "Host a card from your grip to draw a card?" + :choices {:req (req (and (runner? target) + (in-hand? target)))} + :skippable true + :msg "host a card facedown from the Grip and draw a card" + :async true + :effect (req (host state side (get-card state card) target {:facedown true}) + (wait-for (draw state side 1) + (if (>= (count (:hosted (get-card state card))) 5) + (continue-ability + state side + {:msg "trash itself" + :async true + :effect (req (trash state side eid card))} + card nil) + (effect-completed state side eid))))}] + {:on-install ab + :events [(assoc ab :event :runner-turn-begins)] + :abilities [{:fake-cost [(->c :trash-can)] + :label "Shuffle all hosted cards into the stack" + :interactive (req true) + :async true + :effect (req (if (seq (:hosted (get-card state card))) + (let [set-aside-cards (set-aside-for-me state side eid (:hosted (get-card state card))) + set-aside-cards (get-set-aside state side eid)] + (continue-ability + state side + {:cost [(->c :trash-can)] + :msg (str "shuffle " (quantify (count set-aside-cards) "hosted card") " into the Stack") + :effect (req (doseq [c set-aside-cards] + (move state side c :deck)) + (shuffle! state side :deck))} + (get-card state card) nil)) + (continue-ability + state side + {:cost [(->c :trash-can)] + :msg "shuffle the Stack" + :effect (req (shuffle! state side :deck))} + card nil)))}]})) + (defcard "Reaver" {:events [{:event :runner-trash :async true @@ -3068,6 +3132,28 @@ (defcard "Shiv" (break-and-enter "Sentry")) +(defcard "Sipa" + {:events [{:event :pass-ice + :req (req (letfn [(valid-ctx? [ctx] + (and (:all-subs-broken ctx) + (:outermost ctx) + (:ice ctx)))] + (and (valid-ctx? context) + (first-event? state side :pass-ice #(some valid-ctx? %))))) + :interactive (req true) + :async true + :effect (effect + (continue-ability + (when-let [ice (get-card state (:ice context))] + {:prompt (str "Swap " (:title ice) " with another ice?") + :choices {:card #(and (installed? %) + (ice? %) + (not (same-card? % ice)))} + :msg (msg "swap the positions of " (card-str state ice) + " and " (card-str state target)) + :effect (effect (swap-ice ice (get-card state target)))}) + card nil))}]}) + (defcard "Slap Vandal" trojan {:abilities [(break-sub 1 1 "All" {:req (req (same-card? current-ice (:host card))) @@ -3189,6 +3275,16 @@ :once :per-turn :events [ability]})]})) +(defcard "Stowaway" + trojan + {:events [{:event :successful-run + :req (req (= (second (get-zone (get-card state (:host card)))) + (target-server context))) + :async true + :msg "gain 2 [Credits]" + :automatic :gain-credits + :effect (req (gain-credits state side eid 2))}]}) + (defcard "Study Guide" (auto-icebreaker {:abilities [(break-sub 1 1 "Code Gate") {:cost [(->c :credit 2)] diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 7726a6cfa2..924db34614 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -13,7 +13,7 @@ event? facedown? get-agenda-points get-card get-counters get-title get-zone hardware? has-subtype? has-any-subtype? ice? identity? in-discard? in-hand? in-set-aside? in-scored? installed? is-type? program? resource? rezzed? - runner? upgrade? virus-program?]] + runner? unique? upgrade? virus-program?]] [game.core.card-defs :refer [card-def]] [game.core.charge :refer [can-charge charge-ability]] [game.core.checkpoint :refer [fake-checkpoint]] @@ -22,7 +22,7 @@ trash-cost]] [game.core.costs :refer [total-available-credits]] [game.core.damage :refer [damage]] - [game.core.def-helpers :refer [all-cards-in-hand* in-hand*? breach-access-bonus defcard draw-abi offer-jack-out + [game.core.def-helpers :refer [all-cards-in-hand* in-hand*? breach-access-bonus defcard draw-abi draw-loud offer-jack-out reorder-choice spend-credits take-credits take-n-credits-ability take-all-credits-ability trash-on-empty do-net-damage play-tiered-sfx run-any-server-ability run-server-ability make-icon]] @@ -66,7 +66,7 @@ [game.core.revealing :refer [reveal reveal-loud]] [game.core.rezzing :refer [derez rez]] [game.core.runs :refer [active-encounter? bypass-ice can-run-server? get-runnable-zones - gain-run-credits get-current-encounter + get-current-encounter update-current-encounter make-run set-next-phase successful-run-replace-breach total-cards-accessed]] @@ -1689,6 +1689,16 @@ (pay state :runner eid card (->c :credit 4))))} card nil)))}}]}) +(defcard "Hackerspace" + {:static-abilities [{:type :can-host + :req (req (and (resource? target) + (has-any-subtype? target ["Connection" "Companion"]) + (unique? target))) + :cost-bonus -1} + (runner-hand-size+ (req (if (and (some #(has-subtype? % "Connection") (:hosted card)) + (some #(has-subtype? % "Companion") (:hosted card))) + 2 0)))]}) + (defcard "Hades Shard" (shard-constructor "Hades Shard" :archives "breach Archives" (effect (breach-server eid [:archives] {:no-root true})))) @@ -2478,6 +2488,13 @@ :msg "trash itself" :effect (effect (trash eid card {:cause :runner-ability :cause-card card}))}]})) +(defcard "Nurse Hạnh" + {:events [{:event :archives-flipped + :req (req (>= (:count context) 2)) + :msg "draw 2 cards" + :async true + :effect (req (draw state side eid 2))}]}) + (defcard "No Free Lunch" {:abilities [{:label "Gain 3 [Credits]" :msg "gain 3 [Credits]" @@ -3262,6 +3279,25 @@ (swap! state assoc-in [:runner :register :double-ignore-additional] true))}] :leave-play (req (swap! state update-in [:runner :register] dissoc :double-ignore-additional))}) +(defcard "Stick and Poke" + {:events [{:event :encounter-ice + :req (req (first-event? state side :encounter-ice)) + :interactive (req true) + :effect (req (register-lingering-effect + state side card + (let [ice (:ice context)] + {:duration :end-of-encounter + :type :additional-subroutines + :req (req (and (rezzed? target) (same-card? target ice))) + :value {:position :front + :subroutines + [{:label "[Stick] Do 1 net damage. The Runner draws 1 card." + :msg "Do 1 net damage" + :async true + :effect (req (wait-for + (damage state side :net 1) + (draw-loud state :runner eid card 1)))}]}})))}]}) + (defcard "Stim Dealer" {:events [{:event :runner-turn-begins :async true @@ -4096,6 +4132,22 @@ {:events [(assoc ability :event :runner-turn-begins)] :abilities [ability]})) +(defcard "Word on the Street" + {:events [{:event :pre-agenda-scored + :req (req (= :this-turn (:installed (:scored-card context)))) + :msg "add itself to the score area as an agenda worth -1 agenda points" + :display-side :corp + :effect (req (let [fake-gendie (as-agenda state :corp card -1)] + (update! state :corp (assoc-in fake-gendie [:flags :cannot-forfeit] true))))} + {:event :agenda-scored + :msg "trash itself, gain 4 [Credits] and draw a card" + :effect (req (wait-for + (trash state side card {:suppress-checkpoint true}) + (wait-for + (gain-credits state side 4 {:suppress-checkpoint true}) + (draw state side eid 1)))) + :async true}]}) + (defcard "Wyldside" (let [ab {:msg "draw 2 cards and lose [Click]" :once :per-turn diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index e22936d7c2..a839924014 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -21,8 +21,8 @@ [game.core.effects :refer [register-lingering-effect]] [game.core.eid :refer [effect-completed get-ability-targets is-basic-advance-action? make-eid]] [game.core.engine :refer [dissoc-req pay register-default-events - register-events resolve-ability unregister-events]] - [game.core.events :refer [first-event? first-run-event? no-event? turn-events]] + not-used-once? register-events resolve-ability unregister-events]] + [game.core.events :refer [first-event? first-run-event? no-event? turn-events run-event-count run-events]] [game.core.finding :refer [find-cid find-latest]] [game.core.flags :refer [clear-persistent-flag! is-scored? register-persistent-flag! register-run-flag!]] @@ -725,6 +725,43 @@ :base 3 :successful (give-tags 2)}}}) +(defcard "Flagship" + (let [other-cards-accessed (fn [state card] (map :cid (filter #(not= (:cid %) (:cid card)) (apply concat (run-events state :runner :access))))) + prevent-random {:type :disable-random-accesses + :value true + :req (req (and run this-server (seq (other-cards-accessed state card))))} + prevent-installed {:type :disable-access-candidacy + :value true + :req (req (and run this-server + (not (same-card? card target)) + (seq (other-cards-accessed state card))))}] + {:static-abilities [{:type :block-successful-run + :req (req this-server) + :value true} + prevent-random + prevent-installed] + :legal-zones (req (filter #{"R&D" "HQ"} targets)) + :on-trash {:req (req (and run (= :runner side))) + :effect (req + (let [c (:card context)] + (register-lingering-effect + state side (:card context) + {:type :disable-random-accesses + :value true + :duration :end-of-run + :req (req + (and run + (= (:server run) [(second (get-zone c))]) + (seq (other-cards-accessed state c))))}) + (register-lingering-effect + state side (:card context) + {:type :disable-access-candidacy + :value true + :duration :end-of-run + :req (req (and run + (= (:server run) [(second (get-zone c))]) + (seq (other-cards-accessed state c))))})))}})) + (defcard "Fractal Threat Matrix" {:events [{:event :subroutines-broken :req (req (and (:all-subs-broken context) @@ -888,6 +925,19 @@ :event :successful-run :req (req this-server))]}) +(defcard "Hype Machine" + {:rez-cost-bonus (req (if-not (and (no-event? state side :agenda-scored) + (no-event? state side :agenda-stolen)) + -6 + 0)) + :abilities [{:label "Place 1 advancement token on a card in this server" + :async true + :prompt "Choose a card in this server" + :choices {:req (req (in-same-server? card target))} + :msg (msg "place an advancement token on " (card-str state target)) + :cost [(->c :trash-can)] + :effect (effect (add-prop eid target :advance-counter 1 {:placed true}))}]}) + (defcard "Increased Drop Rates" {:flags {:rd-reveal (req true)} :poison true @@ -1053,7 +1103,7 @@ (in-same-server? card target)))} :async true :effect (effect (add-prop eid target :advance-counter 1 {:placed true}))}] - {:install-req (req (remove #{"HQ" "R&D" "Archives"} targets)) + {:legal-zones (req (remove #{"HQ" "R&D" "Archives"} targets)) :derezzed-events [corp-rez-toast] :flags {:corp-phase-12 (req true)} :events [(assoc ability :event :corp-turn-begins)] @@ -1574,6 +1624,43 @@ :keep-menu-open :while-credits-left :req (req (and run (= (target-server run) :hq)))})]}) +(defcard "Perfect Recall" + (let [ab {:req (req run) + :choices {:req (req (and (corp? target) + (in-hand? target)))} + :label "Reveal a card and prevent it being trashed or stolen this run" + :msg (msg "reveal " (:title target) "from HQ and prevent the runner from stealing or trashing any copies of it this run") + :async true + :waiting-prompt true + :effect (req + (wait-for + (reveal state side target) + (let [revealed-card target] + (register-lingering-effect + state side card + {:type :cannot-steal + :req (req (= (:title target) (:title revealed-card))) + :value true + :duration :end-of-run}) + (register-lingering-effect + state side card + {:type :cannot-be-trashed + :req (req (and (= (:title target) (:title revealed-card)) + (= :runner side))) + :value true + :duration :end-of-run})) + (effect-completed state side eid)))}] + {:events (mapv + #(merge {:async true :effect (req (add-counter state side eid card :power 1))} %) + [{:event :agenda-stolen + :req (req (= (:previous-zone (:card context)) (get-zone card)))} + {:event :agenda-scored + :req (req (= (:previous-zone (:card context)) (get-zone card)))}]) + :on-rez {:silent (req true) + :async true + :effect (req (add-counter state side eid card :power 1))} + :abilities [(assoc ab :cost [(->c :power 1)])]})) + (defcard "Port Anson Grid" {:on-rez {:msg "prevent the Runner from jacking out unless they trash an installed program"} :static-abilities [{:type :jack-out-additional-cost @@ -1696,6 +1783,20 @@ :effect (effect (damage eid :net 3 {:card card}))}}} card nil))))}]}) +(defcard "Shackleton Grid" + (let [ev {:optional + {:prompt "do 4 meat damage?" + :waiting-prompt true + :req (req (and run this-server (not-used-once? state {:once :per-turn} card) + (or (not (:card target)) + (runner? (:card target))))) + :yes-ability {:once :per-run + :async true + :msg "do 4 meat damage" + :effect (req (damage state side eid :meat 4))}}}] + {:events [(merge ev {:event :bad-publicity-spent}) + (merge ev {:event :spent-credits-from-card})]})) + (defcard "Shell Corporation" {:abilities [{:action true @@ -1804,6 +1905,24 @@ {:abilities [abi] :events [(mobile-sysop-event :corp-turn-begins)]})) +(defcard "The Red Room" + {:legal-zones (req (filter #{"R&D" "HQ" "Archives"} targets)) + :events [{:event :agenda-stolen + :async true + :effect (req (add-counter state side eid card :power 1)) + :req (req (and (first-event? state side :agenda-stolen) + (no-event? state side :agenda-scored)))} + {:event :agenda-scored + :async true + :effect (req (add-counter state side eid card :power 1)) + :req (req (and (first-event? state side :agenda-scored) + (no-event? state side :agenda-stolen)))}] + :abilities [{:cost [(->c :power 1)] + :req (req (and run (not this-server))) + :async true + :effect (req (end-run state side eid card)) + :msg "End the run"}]}) + (defcard "The Twins" {:events [{:event :pass-ice :optional @@ -1848,7 +1967,7 @@ :effect (effect (gain-credits eid 1))}}}]}) (defcard "Tranquility Home Grid" - {:install-req (req (remove #{"HQ" "R&D" "Archives"} targets)) + {:legal-zones (req (remove #{"HQ" "R&D" "Archives"} targets)) :events [{:event :corp-install :interactive (req true) :req (req (and (or (asset? (:card context)) @@ -1881,7 +2000,7 @@ (system-msg state side (str "shuffles R&D")) (effect-completed state side eid))) :cancel shuffle-my-deck!}}}] - {:install-req (req (remove #{"HQ" "R&D" "Archives"} targets)) + {:legal-zones (req (remove #{"HQ" "R&D" "Archives"} targets)) :events [(assoc ability :event :agenda-stolen :req (req (= (:previous-zone (:card context)) (get-zone card)))) @@ -2035,7 +2154,7 @@ :effect (effect (gain-credits eid 2))}]})) (defcard "ZATO City Grid" - {:install-req (req (remove #{"HQ" "R&D" "Archives"} targets)) + {:legal-zones (req (remove #{"HQ" "R&D" "Archives"} targets)) :static-abilities [{:type :gain-encounter-ability :req (req (and (protecting-same-server? card target) (not (:disabled target)))) diff --git a/src/clj/game/core/access.clj b/src/clj/game/core/access.clj index 73159d2eb0..7f340d4580 100644 --- a/src/clj/game/core/access.clj +++ b/src/clj/game/core/access.clj @@ -509,7 +509,8 @@ ([state server] (->> (get-in @state [:corp :servers server :content]) get-all-content - (filter #(can-access? state :runner %)))) + (filter #(can-access? state :runner %)) + (filter #(not (any-effects state :runner :disable-access-candidacy true? % [%]))))) ([state server already-accessed-fn] (remove already-accessed-fn (root-content state server)))) @@ -626,9 +627,8 @@ (defn access-helper-rd [state {:keys [chosen random-access-limit] :as access-amount} already-accessed {:keys [no-root] :as args}] - (let [current-available (set (concat (map :cid (get-in @state [:corp :deck])) + (let [current-available (set (concat (if-not (any-effects state :runner :disable-random-accesses true? {:server :rd}) (map :cid (get-in @state [:corp :deck])) []) (map :cid (root-content state :rd)))) - already-accessed (clj-set/intersection already-accessed current-available) already-accessed-fn (fn [card] (contains? already-accessed (:cid card))) deck (access-cards-from-rd state) @@ -814,11 +814,11 @@ (defn access-helper-hq [state {:keys [chosen random-access-limit] :as access-amount} already-accessed {:keys [no-root access-first] :as args}] - (let [hand (when (not (:prevent-hand-access (:run @state))) + (let [hand (when (not (or (:prevent-hand-access (:run @state)) + (any-effects state :runner :disable-random-accesses true? {:server :hq}))) (get-in @state [:corp :hand])) current-available (set (concat (map :cid hand) (map :cid (root-content state :hq)))) - already-accessed (clj-set/intersection already-accessed current-available) already-accessed-fn (fn [card] (contains? already-accessed (:cid card))) @@ -1337,16 +1337,20 @@ :chosen 0})) (defn turn-archives-faceup - [state side server] + [state side eid server] ;; note - in a paper game, players may freely re-order Archives ;; We don't have that functionality, so we have to pseudo-shuffle archives before cards are flipped ;; in order to stop information leaking that should not be leaking (ie order of cards trashed) ;; --nbk, 2025 - (when (= :archives (get-server-type (first server))) + (if (= :archives (get-server-type (first server))) (let [discard (get-in @state [:corp :discard]) - known (->> discard (filter :seen) (map #(dissoc % :new))) - unknown (->> discard (filter (complement :seen)) shuffle (map #(assoc % :seen true :new true)))] - (swap! state assoc-in [:corp :discard] (concat known unknown))))) + known (->> discard (filter :seen) (mapv #(dissoc % :new))) + unknown (->> discard (filter (complement :seen)) shuffle (mapv #(assoc % :seen true :new true)))] + (swap! state assoc-in [:corp :discard] (concat known unknown)) + (if (pos? (count unknown)) + (trigger-event-simult state side eid :archives-flipped nil {:count (count unknown)}) + (effect-completed state side eid))) + (effect-completed state side eid))) (defn clean-access-args [{:keys [access-first] :as args}] @@ -1378,12 +1382,13 @@ (swap! state assoc :breach {:breach-server (first server) :from-server (first server)}) (let [args (clean-access-args args) access-amount (num-cards-to-access state side (first server) nil)] - (turn-archives-faceup state side server) - (when (:run @state) - (swap! state assoc-in [:run :did-access] true)) - (wait-for (resolve-ability state side (choose-access access-amount server (assoc args :server server)) nil nil) - (wait-for (trigger-event-sync state side :end-breach-server (:breach @state)) - (swap! state assoc :breach nil) - (unregister-lingering-effects state side :end-of-access) - (unregister-floating-events state side :end-of-access) - (effect-completed state side eid))))))) + (wait-for + (turn-archives-faceup state side server) + (when (:run @state) + (swap! state assoc-in [:run :did-access] true)) + (wait-for (resolve-ability state side (choose-access access-amount server (assoc args :server server)) nil nil) + (wait-for (trigger-event-sync state side :end-breach-server (:breach @state)) + (swap! state assoc :breach nil) + (unregister-lingering-effects state side :end-of-access) + (unregister-floating-events state side :end-of-access) + (effect-completed state side eid)))))))) diff --git a/src/clj/game/core/actions.clj b/src/clj/game/core/actions.clj index d0a0cbf826..e41f245bc3 100644 --- a/src/clj/game/core/actions.clj +++ b/src/clj/game/core/actions.clj @@ -4,6 +4,7 @@ [clojure.stacktrace :refer [print-stack-trace]] [clojure.string :as string] [game.core.agendas :refer [update-advancement-requirement update-all-advancement-requirements update-all-agenda-points]] + [game.core.bad-publicity :refer [bad-publicity-available]] [game.core.board :refer [installable-servers]] [game.core.card :refer [get-advancement-requirement get-agenda-points get-card get-counters]] [game.core.card-defs :refer [card-def]] @@ -239,6 +240,21 @@ (pay state side eid card (->c :credit (min choice (get-in @state [side :credit])))) (effect-completed state side eid))) +(defn resolve-bad-pub-choice + [state side {:keys [eid] :as args}] + (if (pos? (bad-publicity-available state side)) + (let [prompt (or (first-prompt-by-eid state side eid) + (first (get-in @state [side :prompt]))) + card (:card prompt) + prompt-eid eid + effect (:effect prompt)] + (if (:offer-bad-pub? prompt) + (do (remove-from-prompt-queue state side prompt) + (when effect (effect :bad-publicity)) + (finish-prompt state side prompt card)) + (toast state side (str "You cannot choose Bad Publicity for this effect.") "warning"))) + (toast state side (str "You cannot choose Bad Publicity for this effect.") "warning"))) + ;; TODO - resolve-prompt does some evil things with eids, maybe we can fix it later - nbk, 2025 (defn resolve-prompt "Resolves a prompt by invoking its effect function with the selected target of the prompt. @@ -733,19 +749,26 @@ _ (update-all-agenda-points state) c (get-card state c) points (get-agenda-points c)] - (system-msg state :corp (str "scores " (:title c) - " and gains " (quantify points "agenda point"))) - (implementation-msg state card) - (set-prop state :corp (get-card state c) :advance-counter 0) - (swap! state update-in [:corp :register :scored-agenda] #(+ (or % 0) points)) - (play-sfx state side "agenda-score") - (when-let [on-score (:on-score (card-def c))] - (register-pending-event state :agenda-scored c on-score)) - (queue-event state :agenda-scored {:card c - :advancement-requirement advancement-requirement - :advancement-tokens advancement-tokens - :points points}) - (checkpoint state nil eid {:duration :agenda-scored}))) + (wait-for (trigger-event-simult state side :pre-agenda-scored nil + {:card c + :scored-card card + :advancement-requirement advancement-requirement + :advancement-tokens advancement-tokens + :points points}) + (system-msg state :corp (str "scores " (:title c) + " and gains " (quantify points "agenda point"))) + (implementation-msg state card) + (set-prop state :corp (get-card state c) :advance-counter 0) + (swap! state update-in [:corp :register :scored-agenda] #(+ (or % 0) points)) + (play-sfx state side "agenda-score") + (when-let [on-score (:on-score (card-def c))] + (register-pending-event state :agenda-scored c on-score)) + (queue-event state :agenda-scored {:card c + :scored-card card + :advancement-requirement advancement-requirement + :advancement-tokens advancement-tokens + :points points}) + (checkpoint state nil eid {:duration :agenda-scored})))) (defn score "Score an agenda." diff --git a/src/clj/game/core/bad_publicity.clj b/src/clj/game/core/bad_publicity.clj index 069554431e..f72d7e21ec 100644 --- a/src/clj/game/core/bad_publicity.clj +++ b/src/clj/game/core/bad_publicity.clj @@ -1,13 +1,21 @@ (ns game.core.bad-publicity (:require - [game.core.eid :refer [effect-completed make-eid make-result]] - [game.core.engine :refer [queue-event checkpoint trigger-event-sync]] - [game.core.gaining :refer [gain lose]] - [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] - [game.core.prevention :refer [resolve-bad-pub-prevention]] - [game.core.say :refer [system-msg]] - [game.core.toasts :refer [toast]] - [game.macros :refer [wait-for]])) + [game.core.eid :refer [effect-completed make-eid make-result]] + [game.core.effects :refer [any-effects]] + [game.core.engine :refer [queue-event checkpoint trigger-event-sync]] + [game.core.gaining :refer [gain lose]] + [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] + [game.core.prevention :refer [resolve-bad-pub-prevention]] + [game.core.say :refer [system-msg]] + [game.core.toasts :refer [toast]] + [game.macros :refer [wait-for]])) + +(defn bad-publicity-available + "The amount of bad publicity available for this run" + ([state side] + (if (= side :runner) + (get-in @state [:run :bad-publicity-available] 0) + 0))) (defn- resolve-bad-publicity [state side eid n {:keys [suppress-checkpoint] :as args}] @@ -30,9 +38,19 @@ (defn lose-bad-publicity ([state side n] (lose-bad-publicity state side (make-eid state) n)) - ([state side eid n] + ([state side eid n] (lose-bad-publicity state side eid n nil)) + ([state side eid n {:keys [no-event]}] (if (= n :all) (lose-bad-publicity state side eid (get-in @state [:corp :bad-publicity :base])) - (do (lose state :corp :bad-publicity n) - (trigger-event-sync state side eid :corp-lose-bad-publicity {:amount n - :side side}))))) + (let [n (min n (get-in @state [:corp :bad-publicity :base]))] + (do (lose state :corp :bad-publicity n) + (if no-event + (effect-completed state side eid) + (trigger-event-sync state side eid :corp-lose-bad-publicity {:amount n + :side side}))))))) + +(defn spend-bad-publicity + "Spend a bad pub" + [state side amt] + (when (and (= side :runner) (pos? (bad-publicity-available state side))) + (swap! state update-in [:run :bad-publicity-available] #(- % amt)))) diff --git a/src/clj/game/core/board.clj b/src/clj/game/core/board.clj index f6422587ae..833c9c0ee2 100644 --- a/src/clj/game/core/board.clj +++ b/src/clj/game/core/board.clj @@ -175,7 +175,7 @@ (can-host state :corp (make-eid state) % [card]))) (all-installed state :corp)) base-list (concat hosts (server-list state) (when-not at-remote-limit? ["New remote"]))] - (if-let [install-req (-> card card-def :install-req)] + (if-let [install-req (or (-> card card-def :install-req) (-> card card-def :legal-zones))] ;; Install req function overrides normal list of install locations (install-req state :corp card (make-eid state) base-list) ;; Standard list diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index f52a951693..cc3659936c 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -1,6 +1,6 @@ (ns game.core.costs (:require - [game.core.bad-publicity :refer [gain-bad-publicity]] + [game.core.bad-publicity :refer [gain-bad-publicity bad-publicity-available lose-bad-publicity]] [game.core.board :refer [all-active all-active-installed all-installed all-installed-runner-type]] [game.core.card :refer [active? agenda? corp? facedown? get-card get-counters get-zone hardware? has-subtype? ice? in-hand? installed? program? resource? rezzed? runner?]] [game.core.card-defs :refer [card-def]] @@ -24,6 +24,8 @@ [game.macros :refer [continue-ability req wait-for]] [game.utils :refer [enumerate-cards enumerate-str quantify same-card?]])) +(defn- can-forfeit? [card] (not (get-in card [:flags :cannot-forfeit]))) + ;; Click (defmethod value :click [cost] (:cost/amount cost)) (defmethod label :click [cost] @@ -138,7 +140,10 @@ (defn total-available-credits [state side eid card] (if-not (any-effects state side :cannot-pay-credit) - (+ (get-in @state [side :credit]) + (+ (if-not (any-effects state side :cannot-pay-credits-from-pool) + (get-in @state [side :credit]) + 0) + (bad-publicity-available state side) (->> (concat (eligible-pay-credit-cards state side eid card) (eligible-reduce-credit-cards state side eid card)) (map #(+ (get-counters % :recurring) @@ -175,7 +180,7 @@ [cost state side eid card] (and (<= 0 (- (total-available-stealth-credits state side eid card) (stealth-value cost))) (<= 0 (- (value cost) (stealth-value cost))) - (or (<= 0 (- (get-in @state [side :credit]) (value cost))) + (or (when-not (any-effects state side :cannot-pay-credits-from-pool) (<= 0 (- (get-in @state [side :credit]) (value cost)))) (<= 0 (- (total-available-credits state side eid card) (value cost)))))) (defmethod handler :credit [cost state side eid card] @@ -186,8 +191,9 @@ (let [updated-cost (max 0 (- (value cost) (or (:reduction async-result) 0)))] (cond (and (pos? updated-cost) - (pos? (count (provider-func)))) - (wait-for (resolve-ability state side (pick-credit-providing-cards provider-func eid updated-cost (stealth-value cost)) card nil) + (or (pos? (count (provider-func))) + (pos? (bad-publicity-available state side)))) + (wait-for (resolve-ability state side (pick-credit-providing-cards provider-func eid updated-cost (stealth-value cost) (hash-map) nil {} (bad-publicity-available state side)) card nil) (let [pay-async-result async-result] (queue-event state (if (= side :corp) :corp-spent-credits :runner-spent-credits) {:value updated-cost}) (swap! state update-in [:stats side :spent :credit] (fnil + 0) updated-cost) @@ -323,7 +329,7 @@ (defmethod label :forfeit [cost] (str "forfeit " (quantify (value cost) "Agenda"))) (defmethod payable? :forfeit [cost state side _eid _card] - (<= 0 (- (count (get-in @state [side :scored])) (value cost)))) + (<= 0 (- (count (filter can-forfeit? (get-in @state [side :scored]))) (value cost)))) (defmethod handler :forfeit [cost state side eid card] (continue-ability @@ -332,7 +338,8 @@ :async true :choices {:max (value cost) :all true - :card #(is-scored? state side %)} + :req (req (and (is-scored? state side target) + (can-forfeit? target)))} :effect (req (doseq [agenda targets] ;; We don't have to await this because we're suppressing the ;; checkpoint and forfeit makes all of the trashing unpreventable, @@ -372,7 +379,7 @@ (defmethod payable? :forfeit-or-trash-x-from-hand [cost state side eid card] (or (<= 0 (- (count (get-in @state [side :hand])) (value cost))) - (pos? (count (get-in @state [side :scored]))))) + (pos? (count (filter can-forfeit? (get-in @state [side :scored])))))) (defmethod handler :forfeit-or-trash-x-from-hand [cost state side eid card] (let [hand (if (= :corp side) "HQ" "the grip") @@ -399,7 +406,8 @@ :async true :choices {:max 1 :all true - :card #(is-scored? state side %)} + :req (req (and (is-scored? state side target) + (can-forfeit? target)))} :effect (req (doseq [agenda targets] (forfeit state side (make-eid state eid) agenda {:msg false :suppress-checkpoint true})) @@ -1396,3 +1404,20 @@ " from on " title) :paid/type :virus :paid/value (value cost)}))))) + +(defmethod value :host-bad-pub [cost] (:cost/amount cost)) +(defmethod label :host-bad-pub [cost] (str "host " (value cost) " bad publicity")) +(defmethod payable? :host-bad-pub + [cost state side eid card] + (<= 0 (- (get-in @state [:corp :bad-publicity :base] 0) (value cost)))) +(defmethod handler :host-bad-pub + [cost state side eid card] + (wait-for + (lose-bad-publicity state side (value cost) nil) + (wait-for + (add-counter state side card :bad-publicity (value cost) {:suppress-checkpoint true}) + (complete-with-result + state side eid + {:paid/msg (str "hosts " (value cost) " bad publicity on " (:title card)) + :paid/type :host-bad-pub + :paid/value (value cost)})))) diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index fb42258c19..0fb4063cdb 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -77,7 +77,7 @@ (when (= dmg-type :brain) (swap! state update-in [:runner :brain-damage] #(+ % n))) (when-let [trashed-msg (enumerate-cards cards-trashed :sorted)] - (system-msg state :runner (str "trashes " trashed-msg " due to " (damage-name dmg-type) " damage")) + (system-msg state side (str "trashes " trashed-msg " due to " (damage-name dmg-type) " damage")) (swap! state update-in [:stats :corp :damage :all] (fnil + 0) n) (swap! state update-in [:stats :corp :damage dmg-type] (fnil + 0) n) (if (< (count hand) n) diff --git a/src/clj/game/core/def_helpers.clj b/src/clj/game/core/def_helpers.clj index af5dc90d67..98b9b6eadc 100644 --- a/src/clj/game/core/def_helpers.clj +++ b/src/clj/game/core/def_helpers.clj @@ -5,6 +5,7 @@ [game.core.board :refer [all-installed get-all-cards]] [game.core.card :refer [active? can-be-advanced? corp? faceup? get-card get-counters has-subtype? in-discard? in-hand? operation? runner? ]] [game.core.card-defs :as card-defs] + [game.core.choose-one :refer [choose-one-helper]] [game.core.damage :refer [damage]] [game.core.drawing :refer [draw]] [game.core.eid :refer [effect-completed make-eid]] @@ -20,8 +21,10 @@ [game.core.revealing :refer [conceal-hand reveal reveal-hand reveal-loud]] [game.core.runs :refer [can-run-server? make-run jack-out]] [game.core.say :refer [play-sfx system-msg system-say]] - [game.core.servers :refer [zone->name]] + [game.core.servers :refer [zone->name name-zone]] [game.core.shuffling :refer [shuffle! fail-to-find!]] + [game.core.servers :refer [zone->name name-zone]] + [game.core.shuffling :refer [shuffle!]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.tags :refer [gain-tags]] @@ -233,6 +236,7 @@ (merge {:async true :prompt "Choose a server" :choices (req runnable-servers) + :req (req (seq runnable-servers)) :label "Run a server" :makes-run true :msg (msg "make a run on " target) @@ -262,13 +266,20 @@ :effect (effect (make-run eid target card))}) (defn run-server-from-choices-ability - [choices] - {:prompt "Choose a server" - :choices (req (filter #(can-run-server? state %) choices)) - :change-in-game-state {:req (req (seq (filter (set choices) runnable-servers)))} - :async true - :msg (msg "make a run on " target) - :effect (effect (make-run eid target card))}) + ([choices] (run-server-from-choices-ability choices nil)) + ([choices {:keys [events] :as ab-base}] + (merge + {:prompt "Choose a server" + :choices (req (filter #(can-run-server? state %) choices)) + :change-in-game-state {:req (req (seq (filter (set choices) runnable-servers)))} + :async true + :msg (msg "make a run on " target) + :effect (req (when (seq events) + (register-events state side card events)) + (when (:action ab-base) + (play-sfx state side "click-run")) + (make-run state side eid target card))} + (dissoc ab-base :events)))) (defn take-credits "Take n counters from a card and place them in your credit pool as if they were credits (if possible)" @@ -349,6 +360,31 @@ (update ability :abilities #(conj (into [] %) recurring-ability))) ability)) +(defn move-to-top + [target-card] + {:msg (msg "add " (card-str state target-card) " from " + (name-zone (:side target-card) (:zone target-card)) + " to the top of " (if (runner? target-card) "the Stack" "R&D")) + :effect (req (move state side target-card :deck {:front true}))}) + +(defn move-to-bottom + [target-card] + {:msg (msg "add " (card-str state target-card) " from " + (name-zone (:side target-card) (:zone target-card)) + " to the bottom of " (if (runner? target-card) "the Stack" "R&D")) + :effect (req (move state side target-card :deck))}) + +(defn move-card-to-top-or-bottom + "Ability to move a card to the top or bottom of the deck" + [target-card] + (let [zone (if (runner? target-card) "the Stack" "R&D")] + (choose-one-helper + {:prompt (str "Move " (:title target-card) " where?")} + [{:option (str "Top of " zone) + :ability (move-to-top target-card)} + {:option (str "Bottom of " zone) + :ability (move-to-bottom target-card)}]))) + (defn trash-or-rfg [state _ eid card] (let [side (to-keyword (:side card)) diff --git a/src/clj/game/core/diffs.clj b/src/clj/game/core/diffs.clj index e32bd81e16..634b13528d 100644 --- a/src/clj/game/core/diffs.clj +++ b/src/clj/game/core/diffs.clj @@ -230,8 +230,11 @@ :card :prompt-type :show-discard + :show-opponent-discard :selectable :eid + ;; bad pub + :offer-bad-pub? ;; traces :player :base @@ -363,6 +366,7 @@ (def runner-keys [:rig :run-credit + :bad-pub-credit :link :tag :memory @@ -385,6 +389,7 @@ (update :deck deck-summary runner-player? runner) (update :hand hand-summary state runner-player? :runner runner) (update :discard prune-cards) + (assoc :bad-pub-credit (get-in @state [:run :bad-publicity-available] 0)) (assoc :deck-count (count (:deck runner)) :hand-count (count (:hand runner)) diff --git a/src/clj/game/core/engine.clj b/src/clj/game/core/engine.clj index 2280a2f3d6..8a3c6fb09b 100644 --- a/src/clj/game/core/engine.clj +++ b/src/clj/game/core/engine.clj @@ -148,6 +148,7 @@ ; some ability once between all of them, then the card should specify a manual :once-key that can ; be any value, preferrably a unique keyword. ; :install-req -- a function which returns a list of servers a card may be installed into +; :legal-zones -- like install-req, but also disallows movement into the given zones ; :makes-run -- boolean. indicates if the ability makes a run. ; COMPLEX ABILITY WRAPPERS diff --git a/src/clj/game/core/gaining.clj b/src/clj/game/core/gaining.clj index 7df62bdb4d..224960b5ef 100644 --- a/src/clj/game/core/gaining.clj +++ b/src/clj/game/core/gaining.clj @@ -1,7 +1,8 @@ (ns game.core.gaining (:require - [game.core.eid :refer [effect-completed]] - [game.core.engine :refer [trigger-event trigger-event-sync queue-event checkpoint]])) + [game.core.eid :refer [effect-completed]] + [game.core.effects :refer [any-effects]] + [game.core.engine :refer [trigger-event trigger-event-sync queue-event checkpoint]])) (defn safe-inc-n "Helper function to safely update a value by n. Returns a function to use with `update` / `update-in`" @@ -103,7 +104,8 @@ (if (and amount (or (= :all amount) (pos? amount)) - (pos? (:credit (side @state)))) + (pos? (:credit (side @state))) + (not (any-effects state side :cannot-lose-credits))) (do (lose state side :credit amount) (when (and (= side :runner) (= :all amount)) diff --git a/src/clj/game/core/ice.clj b/src/clj/game/core/ice.clj index 47e736ec0b..4678ff6dbb 100644 --- a/src/clj/game/core/ice.clj +++ b/src/clj/game/core/ice.clj @@ -566,6 +566,7 @@ :during-run (some? (:run @state)) :on-attacked-server (= (get-in @state [:run :server]) [(second (:zone ice))]) :all-subs-broken (all-subs-broken? ice) + :was-zero-or-less-strength? (<= (get-strength ice) 0) :broken-subs broken-subs ;; enough info to backtrack and find breakers without bloating the gamestate ;; could just be the card itself if we don't care too much though diff --git a/src/clj/game/core/installing.clj b/src/clj/game/core/installing.clj index aa3914223f..25ece73618 100644 --- a/src/clj/game/core/installing.clj +++ b/src/clj/game/core/installing.clj @@ -580,15 +580,46 @@ (->c :credit cost)) additional-costs]))])) +(defn- some-hosting-effect + [state card] + "Gets the first (only) host effect of a card, if it exists and is not disabled" + (when (and card (not (is-disabled-reg? state card))) + (first (filter #(= (:type %) :can-host) (:static-abilities (card-def card)))))) + +(defn runner-can-host + [state side eid card {:keys [host-card facedown] :as args}] + "Gets a list of all cards that the runner can host the install target on" + (when-not (or host-card facedown) + (let [all-hosts (filter #(some-hosting-effect state %) (all-installed state :runner)) + relevant (filter #(let [ab (some-hosting-effect state %)] + (or (nil? (:req ab)) + ((:req ab) state side eid % [card]))) + all-hosts)] + (seq relevant)))) + (defn runner-can-pay-and-install? ([state side eid card] (runner-can-pay-and-install? state side eid card nil)) - ([state side eid card {:keys [facedown] :as args}] + ([state side eid card {:keys [facedown host-card no-host?] :as args}] (let [eid (assoc eid :source-type :runner-install) - costs (runner-install-cost state side (assoc card :facedown facedown) args)] - (and (runner-can-install? state side eid card (assoc args :no-toast true)) - (can-pay? state side eid card nil costs) - ;; explicitly return true - true)))) + host-abi (some-hosting-effect state host-card) + old-cost-bonus (or (:cost-bonus args) 0) + new-cost-bonus (or (:cost-bonus host-abi) 0) + combined-cost-bonus (+ old-cost-bonus new-cost-bonus) + cost-bonus (if (zero? combined-cost-bonus) nil combined-cost-bonus) + costs (runner-install-cost state side + (assoc card :facedown facedown) + (assoc args :cost-bonus cost-bonus))] + (or (and (runner-can-install? state side eid card (assoc args :no-toast true)) + (can-pay? state side eid card nil costs) + ;; explicitly return true + true) + ;; note: Some cards (hackerspace, dhegder) provide a discount to installing cards + ;; so long as they are installed hosted on themselves, so we need to check for that. + ;; -nbkelly, 2026.02 + (and (not host-card) + (not no-host?) + (some #(runner-can-pay-and-install? state side eid card (assoc args :host-card %)) + (runner-can-host state side eid card args))))))) (defn runner-install-pay [state side eid card {:keys [no-mu facedown host-card resolved-optional-trash] :as args}] @@ -643,23 +674,6 @@ :previous-zone (:previous-zone card))) (effect-completed state side eid))))))))) -(defn- some-hosting-effect - [state card] - "Gets the first (only) host effect of a card, if it exists and is not disabled" - (when-not (is-disabled-reg? state card) - (first (filter #(= (:type %) :can-host) (:static-abilities (card-def card)))))) - -(defn runner-can-host - [state side eid card {:keys [host-card facedown] :as args}] - "Gets a list of all cards that the runner can host the install target on" - (when-not (or host-card facedown) - (let [all-hosts (filter #(some-hosting-effect state %) (all-installed state :runner)) - relevant (filter #(let [ab (some-hosting-effect state %)] - (or (nil? (:req ab)) - ((:req ab) state side eid % [card]))) - all-hosts)] - (seq relevant)))) - (defn runner-host-enforce-specific-memory [state side eid card potential-host args] "enforces limits on the total MU a host can support during install" diff --git a/src/clj/game/core/moving.clj b/src/clj/game/core/moving.clj index daa2e1356d..08d4a00193 100644 --- a/src/clj/game/core/moving.clj +++ b/src/clj/game/core/moving.clj @@ -40,10 +40,10 @@ (defn uninstall "Triggers :uninstall effects" - ([state side {:keys [disabled] :as card}] + ([state side {:keys [disabled] :as card} old-card] (when-let [uninstall-effect (:uninstall (card-def card))] (when (not disabled) - (uninstall-effect state side (make-eid state) card nil))) + (uninstall-effect state side (make-eid state) card [{:old-card old-card}]))) card)) (defn- should-moved-card-be-known? @@ -125,7 +125,7 @@ c) c (if (and from-installed (not (facedown? c))) - (uninstall state side c) + (uninstall state side c card) c) c (if to-installed (assoc c :installed :this-turn) diff --git a/src/clj/game/core/pick_counters.clj b/src/clj/game/core/pick_counters.clj index 8b4d9f4e9d..133e3fb89f 100644 --- a/src/clj/game/core/pick_counters.clj +++ b/src/clj/game/core/pick_counters.clj @@ -1,24 +1,27 @@ (ns game.core.pick-counters (:require - [game.core.card :refer [get-card get-counters has-subtype? installed? runner?]] - [game.core.card-defs :refer [card-def]] - [game.core.eid :refer [effect-completed make-eid complete-with-result]] - [game.core.engine :refer [resolve-ability queue-event]] - [game.core.gaining :refer [lose]] - [game.core.props :refer [add-counter]] - [game.core.update :refer [update!]] - [game.macros :refer [continue-ability req wait-for]] - [game.utils :refer [enumerate-str in-coll? quantify same-card?]])) + [game.core.bad-publicity :refer [spend-bad-publicity]] + [game.core.card :refer [get-card get-counters has-subtype? installed? runner?]] + [game.core.card-defs :refer [card-def]] + [game.core.eid :refer [effect-completed make-eid complete-with-result]] + [game.core.effects :refer [any-effects]] + [game.core.engine :refer [resolve-ability queue-event]] + [game.core.gaining :refer [lose]] + [game.core.props :refer [add-counter]] + [game.core.update :refer [update!]] + [game.macros :refer [continue-ability req wait-for]] + [game.utils :refer [enumerate-str in-coll? quantify same-card?]])) (defn- pick-counter-triggers - [state side eid current-cards selected-cards counter-type counter-count message] + [state side eid current-cards selected-cards counter-type counter-count message credits] (if-let [[_ selected] (first current-cards)] (if-let [{:keys [card number]} selected] (do (queue-event state :counter-added {:card (get-card state card) :counter-type counter-type :amount number}) - (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message)) - (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message)) + (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message credits)) + (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message credits)) (complete-with-result state side eid {:number counter-count :msg message + :credits-spent-from-pool credits :targets (keep #(:card (second %)) selected-cards)}))) (defn pick-virus-counters-to-spend @@ -56,7 +59,7 @@ title (:title card)] (str (quantify number "virus counter") " from " title)) (vals selected-cards)))] - (pick-counter-triggers state side eid selected-cards selected-cards :virus counter-count message))))) + (pick-counter-triggers state side eid selected-cards selected-cards :virus counter-count message 0))))) :cancel {:async true :effect (if target-count (req (doseq [{:keys [card number]} (vals selected-cards)] @@ -75,6 +78,11 @@ (trigger-spend-credits-from-cards state side eid (rest cards))) (effect-completed state side eid))) +(defn- queue-spend-from-bad-pub + [state side spent] + (when (and spent (pos? spent)) + (queue-event state :bad-publicity-spent {:value spent}))) + (defn- take-counters-of-type "This builds an effect to remove a single counter of the given type, including credits. This does not fire any events." [counter-type] @@ -124,9 +132,9 @@ #(assoc % :card providing-card :number (+ (:number % 0) async-result))) (use-card uses providing-card async-result)) card targets)))) - :cancel{:async true - :effect (req (complete-with-result state side eid {:reduction counter-count - :targets (keep #(:card (second %)) selected-cards)}))}})))) + :cancel {:async true + :effect (req (complete-with-result state side eid {:reduction counter-count + :targets (keep #(:card (second %)) selected-cards)}))}})))) (defn pick-credit-providing-cards "Similar to pick-virus-counters-to-spend. Works on :recurring and normal credits." @@ -135,8 +143,11 @@ ([provider-func outereid target-count stealth-target] (pick-credit-providing-cards provider-func outereid target-count stealth-target (hash-map))) ([provider-func outereid target-count stealth-target selected-cards] (pick-credit-providing-cards provider-func outereid target-count stealth-target selected-cards nil)) ([provider-func outereid target-count stealth-target selected-cards pre-chosen] (pick-credit-providing-cards provider-func outereid target-count stealth-target selected-cards pre-chosen {})) - ([provider-func outereid target-count stealth-target selected-cards pre-chosen uses] - (let [counter-count (reduce + 0 (map #(:number (second %) 0) selected-cards)) + ([provider-func outereid target-count stealth-target selected-cards pre-chosen uses] (pick-credit-providing-cards provider-func outereid target-count stealth-target selected-cards pre-chosen {} 0)) + ([provider-func outereid target-count stealth-target selected-cards pre-chosen uses bad-pub-available] (pick-credit-providing-cards provider-func outereid target-count stealth-target selected-cards pre-chosen {} bad-pub-available 0)) + ([provider-func outereid target-count stealth-target selected-cards pre-chosen uses bad-pub-available bad-pub-spent] + (let [counter-count (+ (reduce + 0 (map #(:number (second %) 0) selected-cards)) + (or bad-pub-spent 0)) selected-stealth (filter #(has-subtype? (:card (second %)) "Stealth") selected-cards) stealth-count (reduce + 0 (map #(:number (second %) 0) selected-stealth)) provider-cards (if (= (- counter-count target-count) (- stealth-count stealth-target)) @@ -145,40 +156,50 @@ all-used-up? (fn [cid] (>= (get-in uses [cid :used] 0) (get-in uses [cid :max-uses] 99))) provider-cards (filter #(not (all-used-up? (:cid %))) provider-cards) provider-cards (filter #(not (get-in (card-def %) [:interactions :pay-credits :cost-reduction])) provider-cards) + can-use-bad-pub? (and (pos? bad-pub-available) (not= stealth-target target-count) true) + can-use-credits? (fn [state side] + (not (any-effects state side :cannot-pay-credits-from-pool))) ;; note - this allows holding the shift key while clicking a card to keep picking that card while possible ;; ie: taking 5cr from miss bones with one click, instead of waiting for 5 server round-trips should-auto-repeat? (fn [state side] (get-in @state [side :shift-key-select] nil)) pay-rest (req - (if (and (<= (- target-count counter-count) (get-in @state [side :credit])) + (if (and (<= (- target-count counter-count) + (if (can-use-credits? state side) (get-in @state [side :credit]) 0)) (<= 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))))) + card-strs (when (or (pos? (count selected-cards)) (pos? bad-pub-spent)) + (enumerate-str (concat (mapv #(let [{:keys [card number]} % + title (:title card)] + (str number " [Credits] from " title)) + (vals selected-cards)) + (when (pos? bad-pub-spent) + [(str bad-pub-spent "[Credits] from bad publicity")])))) message (str card-strs (when (and card-strs remainder-str) " and ") remainder-str (when (and card-strs remainder-str) " from [their] credit pool"))] + (when (pos? bad-pub-spent) + (spend-bad-publicity state side bad-pub-spent)) (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 :credit target-count message)))) + (queue-spend-from-bad-pub state side bad-pub-spent) + ;; Now we trigger all of the :counter-added events we'd neglected previously + (pick-counter-triggers state side eid selected-cards selected-cards :credit target-count message remainder)))) (continue-ability state side - (pick-credit-providing-cards provider-func eid target-count stealth-target selected-cards uses) + (pick-credit-providing-cards provider-func eid target-count stealth-target selected-cards uses bad-pub-available bad-pub-spent) 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 + (if (or (not (pos? target-count)) ;; there is a limit + (<= target-count counter-count) ;; paid everything + (and (zero? (count provider-cards)) ;; no more additional credit sources found + (not can-use-bad-pub?))) {:async true :effect pay-rest} (if (and pre-chosen (in-coll? (map :cid provider-cards) (:cid pre-chosen))) @@ -209,23 +230,31 @@ (str ", " (min stealth-count stealth-target) " of " stealth-target " stealth") "") ")") + :offer-bad-pub? (when can-use-bad-pub? bad-pub-available) :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 + :effect (req (if (= target :bad-publicity) + (continue-ability + state side + (pick-credit-providing-cards + provider-func eid target-count stealth-target selected-cards (when (should-auto-repeat? state side) target) uses (dec bad-pub-available) (inc bad-pub-spent)) + card targets) + (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))) (when (should-auto-repeat? state side) target) - (use-card uses providing-card async-result)) - card targets)))) + (use-card uses providing-card async-result) + bad-pub-available bad-pub-spent) + card targets))))) :cancel {:async true :effect pay-rest}}))))) diff --git a/src/clj/game/core/player.clj b/src/clj/game/core/player.clj index 43f642a64a..7ba7f6eaea 100644 --- a/src/clj/game/core/player.clj +++ b/src/clj/game/core/player.clj @@ -85,6 +85,7 @@ click-per-turn credit run-credit + bad-pub-credit link tag properties diff --git a/src/clj/game/core/process_actions.clj b/src/clj/game/core/process_actions.clj index 26af3c7ba2..79f699b780 100644 --- a/src/clj/game/core/process_actions.clj +++ b/src/clj/game/core/process_actions.clj @@ -6,7 +6,7 @@ generate-runnable-zones move-card expend-ability play play-ability play-corp-ability play-dynamic-ability play-runner-ability play-subroutine play-unbroken-subroutines remove-tag - resolve-prompt score select trash-button trash-resource view-deck]] + resolve-bad-pub-choice resolve-prompt score select trash-button trash-resource view-deck]] [game.core.card :refer [get-card]] [game.core.change-vals :refer [change]] [game.core.checkpoint :refer [fake-checkpoint]] @@ -67,6 +67,7 @@ (def commands {"ability" #'play-ability "advance" #'click-advance + "bad-pub-choice" #'resolve-bad-pub-choice "change" #'change "choice" #'resolve-prompt "close-deck" #'close-deck diff --git a/src/clj/game/core/prompts.clj b/src/clj/game/core/prompts.clj index e844536c9e..22ae0e2d53 100644 --- a/src/clj/game/core/prompts.clj +++ b/src/clj/game/core/prompts.clj @@ -1,6 +1,7 @@ (ns game.core.prompts (:require [clj-uuid :as uuid] + [clojure.string :as str] [game.core.board :refer [get-all-cards]] [game.core.eid :refer [effect-completed make-eid]] [game.core.prompt-state :refer [add-to-prompt-queue remove-from-prompt-queue]] @@ -32,7 +33,7 @@ ([state side card message choices f] (show-prompt state side (make-eid state) card message choices f nil)) ([state side card message choices f args] (show-prompt state side (make-eid state) card message choices f args)) ([state side eid card message choices f - {:keys [waiting-prompt prompt-type show-discard cancel end-effect targets selectable]}] + {:keys [waiting-prompt prompt-type show-discard show-opponent-discard cancel end-effect targets selectable offer-bad-pub?]}] (let [prompt (if (string? message) message (message state side eid card targets)) choices (choice-parser choices) selectable (update-selectable selectable choices) @@ -43,8 +44,10 @@ :effect f :card card :selectable selectable + :offer-bad-pub? offer-bad-pub? :prompt-type (or prompt-type :other) :show-discard show-discard + :show-opponent-discard show-opponent-discard :cancel cancel :end-effect end-effect}] (when (or (#{:waiting :run} prompt-type) @@ -139,6 +142,18 @@ (cancel nil) (effect-completed state side (:eid (:ability selected))))))) +(defn resolve-select-bad-publicity! + "Resolves a selection prompt by invoking the prompt's ability with the targeted cards. + Called when the user clicks 'Done' or selects the :max number of cards." + [state side card args update! resolve-ability button] + (let [selected (get-in @state [side :selected 0]) + cards (map #(dissoc % :selected) (:cards selected)) + prompt (first (filter #(= :select (:prompt-type %)) (get-in @state [side :prompt])))] + (swap! state update-in [side :selected] #(vec (rest %))) + (when prompt + (remove-from-prompt-queue state side prompt)) + (resolve-ability state side (:ability selected) card [button]))) + (defn- compute-selectable [state side card ability req-fn card-fn] (let [valid (filter #(not= (:zone %) [:deck]) (get-all-cards state)) @@ -198,21 +213,25 @@ ; prompts that lie "beneath" the current select prompt. (toast state side (str "You must choose " max-choices " " (pluralize "card" max-choices))) (show-select state side card ability update! resolve-ability args)) - (fn [_] - (let [selected (or (first-selection-by-eid state side (:eid ability)) - (get-in @state [side :selected 0])) - cards (map #(dissoc % :selected) (:cards selected))] - ; check for :min. If not enough cards are selected, show toast and stay in select prompt - (if (and min-choices (< (count cards) min-choices)) - (do - (toast state side (str "You must choose at least " min-choices " " (pluralize "card" min-choices))) - (show-select state side card ability update! resolve-ability args)) - (resolve-select state side (:eid ability) card - (select-keys (wrap-function args :cancel) [:cancel]) - update! resolve-ability))))) + (fn [s] + (if (= s :bad-publicity) + (resolve-select-bad-publicity! state side card ability update! resolve-ability :bad-publicity) + (let [selected (or (first-selection-by-eid state side (:eid ability)) + (get-in @state [side :selected 0])) + cards (map #(dissoc % :selected) (:cards selected))] + ;; check for :min. If not enough cards are selected, show toast and stay in select prompt + (if (and min-choices (< (count cards) min-choices)) + (do + (toast state side (str "You must choose at least " min-choices " " (pluralize "card" min-choices))) + (show-select state side card ability update! resolve-ability args)) + (resolve-select state side (:eid ability) card + (select-keys (wrap-function args :cancel) [:cancel]) + update! resolve-ability)))))) (-> args (assoc :prompt-type :select + :offer-bad-pub? (:offer-bad-pub? ability) :selectable selectable-cards + :show-opponent-discard (:show-opponent-discard ability) :show-discard (:show-discard ability)) (wrap-function :cancel))))))) diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index 35214bb67f..2f57ca02bc 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -1,7 +1,7 @@ (ns game.core.runs (:require [game.core.access :refer [breach-server]] - [game.core.board :refer [get-zones server->zone]] + [game.core.board :refer [get-zones server->zone card->server]] [game.core.card :refer [get-card get-zone rezzed?]] [game.core.card-defs :refer [card-def]] [game.core.cost-fns :refer [jack-out-cost run-cost run-additional-cost-bonus]] @@ -10,7 +10,7 @@ [game.core.engine :refer [checkpoint end-of-phase-checkpoint register-pending-event pay queue-event resolve-ability trigger-event trigger-event-simult]] [game.core.flags :refer [can-run? clear-run-register!]] [game.core.gaining :refer [gain-credits]] - [game.core.ice :refer [active-ice? break-subs-event-context get-current-ice get-run-ices update-ice-strength reset-all-ice reset-all-subs! set-current-ice]] + [game.core.ice :refer [active-ice? all-subs-broken? break-subs-event-context get-current-ice get-run-ices update-ice-strength reset-all-ice reset-all-subs! set-current-ice]] [game.core.mark :refer [is-mark?]] [game.core.payment :refer [build-cost-string build-spend-msg can-pay? merge-costs ->c]] [game.core.prevention :refer [resolve-encounter-prevention resolve-end-run-prevention resolve-jack-out-prevention]] @@ -159,8 +159,8 @@ (wait-for (gain-run-credits state side (make-eid state eid) - (+ (or (get-in @state [:runner :next-run-credit]) 0) - (count-bad-pub state))) + (get-in @state [:runner :next-run-credit] 0)) + (swap! state assoc-in [:run :bad-publicity-available] (count-bad-pub state)) (swap! state assoc-in [:runner :next-run-credit] 0) (swap! state update-in [:runner :register :made-run] conj (first s)) (swap! state update-in [:stats side :runs :started] (fnil inc 0)) @@ -443,7 +443,12 @@ (swap! state assoc-in [:run :no-action] false) (when pass-ice? (system-msg state :runner (str "passes " (card-str state ice))) - (queue-event state :pass-ice {:ice (get-card state ice)})) + (let [nice (get-card state ice)] + (queue-event state :pass-ice + {:ice nice + :outermost (when-let [server-ice (:ices (card->server state nice))] + (same-card? nice (last server-ice))) + :all-subs-broken (all-subs-broken? ice)}))) (swap! state assoc-in [:run :position] new-position) (when passed-all-ice (queue-event state :pass-all-ice {:ice (get-card state ice)})) diff --git a/src/clj/tasks/images.clj b/src/clj/tasks/images.clj index 2376782eb3..0c9dc8c7f6 100644 --- a/src/clj/tasks/images.clj +++ b/src/clj/tasks/images.clj @@ -47,7 +47,7 @@ ;; so long as it either has front,back,or some numbers behind it ;; the excess dots are because the lookbehind needs to be fixed width ;; but this ensures we don't split on "front.", and instead split on "." for multi-faced cards -(def ^:cost image-select-regex #"(?<=(.tank|house|ewery|front|.back|....[0123456789]))[a-zA-Z]*\.") +(def ^:cost image-select-regex #"(?<=(.tank|house|ewery|front|posal|rface|enure|.back|....[0123456789]))[a-zA-Z]*\.") (defn- add-flip-card-image [db base-path lang resolution art-set filename] @@ -62,7 +62,7 @@ (mc/update db card-collection {:code code} {$addToSet {k path}}) (mc/update db card-collection {:previous-versions {$elemMatch {:code code}}} {$set {prev-k path}}))) -(def ^:const cards-to-skip #{"08012" "09001" "26066" "26120" "35023" "35057"}) +(def ^:const cards-to-skip #{"08012" "09001" "26066" "26120" "35023" "35057" "36036"}) (defn- add-card-image "Add an image to a card in the db" diff --git a/src/clj/tasks/nrdb.clj b/src/clj/tasks/nrdb.clj index e1dd3b4631..864447b32f 100644 --- a/src/clj/tasks/nrdb.clj +++ b/src/clj/tasks/nrdb.clj @@ -77,7 +77,7 @@ (reduce expand-card `() c))) ;; these are cards with multiple faces, so we can't download them directly -(def ^:const cards-to-skip #{"08012" "09001" "26066" "26120" "35023" "35057"}) +(def ^:const cards-to-skip #{"08012" "09001" "26066" "26120" "35023" "35057" "36036"}) (defn download-card-images "Download card images (if necessary) from NRDB" diff --git a/src/cljc/jinteki/validator.cljc b/src/cljc/jinteki/validator.cljc index 3c3cb79734..0e4de60b07 100644 --- a/src/cljc/jinteki/validator.cljc +++ b/src/cljc/jinteki/validator.cljc @@ -292,14 +292,14 @@ :reason (str "Illegal identity: " id)}))) (defn startup-agenda-restriction - "As of 25.04, startup decks may only have 4 agendas worth 3 or more points" + "As of 25.04, startup decks may only have 3 agendas worth 3 or more points" [fmt {:keys [cards] :as deck}] (if (= :startup fmt) (let [relevant-agenda (fn [c] (and (= (:type (:card c)) "Agenda") (>= (:agendapoints (:card c)) 3))) relevant-agendas (filter relevant-agenda cards) ct (reduce + 0 (map :qty relevant-agendas))] - (if (> ct 4) + (if (> ct 3) {:reason "Too many agendas worth 3 or more points (startup restriction)"} {:legal true})) {:legal true})) diff --git a/src/cljs/nr/gameboard/board.cljs b/src/cljs/nr/gameboard/board.cljs index 54c80b3378..c1633828e1 100644 --- a/src/cljs/nr/gameboard/board.cljs +++ b/src/cljs/nr/gameboard/board.cljs @@ -1780,7 +1780,7 @@ [tr-span [:game_ok "OK"]]]])) (defn prompt-div - [me {:keys [card msg prompt-type choices] :as prompt-state}] + [me {:keys [card msg prompt-type choices offer-bad-pub?] :as prompt-state}] (let [id (atom 0)] [:div.panel.blue-shade (when (and card (not= "Basic Action" (:type card))) @@ -1868,16 +1868,21 @@ ;; otherwise choice of all present choices :else - (doall (for [{:keys [idx uuid value]} choices - :when (not= value "Hide")] - [:button {:key idx - :on-click #(do (send-command "choice" {:eid (prompt-eid (:side @game-state)) :choice {:uuid uuid}}) - (card-highlight-mouse-out % value button-channel)) - :on-mouse-over - #(card-highlight-mouse-over % value button-channel) - :on-mouse-out - #(card-highlight-mouse-out % value button-channel)} - (render-message (or (not-empty (get-title value)) value))])))])) + (concat [(when offer-bad-pub? + ;; TODO - translate this + [:button {:key "Bad Pub" + :on-click #(send-command "bad-pub-choice" {:eid (prompt-eid (:side @game-state))})} + (str "Bad Publicity (" offer-bad-pub? " available)")])] + (doall (for [{:keys [idx uuid value]} choices + :when (not= value "Hide")] + [:button {:key idx + :on-click #(do (send-command "choice" {:eid (prompt-eid (:side @game-state)) :choice {:uuid uuid}}) + (card-highlight-mouse-out % value button-channel)) + :on-mouse-over + #(card-highlight-mouse-over % value button-channel) + :on-mouse-out + #(card-highlight-mouse-out % value button-channel)} + (render-message (or (not-empty (get-title value)) value))]))))])) (defn basic-actions [{:keys [side active-player end-turn runner-phase-12 corp-phase-12 me runner-post-discard corp-post-discard]}] (let [phase-12 (or @runner-phase-12 @corp-phase-12) @@ -1974,7 +1979,9 @@ (let [autocomp (r/track (fn [] (get-in @prompt-state [:choices :autocomplete]))) show-discard? (r/track (fn [] (get-in @prompt-state [:show-discard]))) prompt-type (r/track (fn [] (get-in @prompt-state [:prompt-type]))) - opened-by-system (r/atom false)] + discard-opened-by-system (r/atom false) + show-opponent-discard? (r/track (fn [] (get-in @prompt-state [:show-opponent-discard]))) + opponent-discard-opened-by-system (r/atom false)] (r/create-class {:display-name "button-pane" @@ -1983,9 +1990,14 @@ (when (pos? (count @autocomp)) (-> "#card-title" js/$ (.autocomplete (clj->js {"source" @autocomp})))) (cond @show-discard? (do (-> ".me .discard-container .popup" js/$ .fadeIn) - (reset! opened-by-system true)) - @opened-by-system (do (-> ".me .discard-container .popup" js/$ .fadeOut) - (reset! opened-by-system false))) + (reset! discard-opened-by-system true)) + @discard-opened-by-system (do (-> ".me .discard-container .popup" js/$ .fadeOut) + (reset! discard-opened-by-system false))) + (cond @show-opponent-discard? (do (-> ".opponent .discard-container .popup" js/$ .fadeIn) + (reset! opponent-discard-opened-by-system true)) + @opponent-discard-opened-by-system (do (-> ".opponent .discard-container .popup" js/$ .fadeOut) + (reset! opponent-discard-opened-by-system false))) + (if (= "select" @prompt-type) (set! (.-cursor (.-style (.-body js/document))) "url('/img/gold_crosshair.png') 12 12, crosshair") (set! (.-cursor (.-style (.-body js/document))) "default")) diff --git a/src/cljs/nr/gameboard/player_stats.cljs b/src/cljs/nr/gameboard/player_stats.cljs index 291cdf19e7..10d45aa2f8 100644 --- a/src/cljs/nr/gameboard/player_stats.cljs +++ b/src/cljs/nr/gameboard/player_stats.cljs @@ -70,9 +70,10 @@ (defmethod stats-area "Runner" [runner] (let [ctrl (stat-controls-for-side :runner)] (fn [] - (let [{:keys [click credit run-credit memory link tag brain-damage]} @runner + (let [{:keys [click credit run-credit bad-pub-credit memory link tag brain-damage]} @runner base-credit (- credit run-credit) - plus-run-credit (when (pos? run-credit) (str "+" run-credit)) + plus-run-credit (when (or (pos? run-credit) (pos? bad-pub-credit)) + (str "+" (+ bad-pub-credit run-credit))) icons? (get-in @app-state [:options :player-stats-icons] true)] [:div.stats-area (if icons? diff --git a/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 9a1dc1a6bc..0953fbbc0d 100644 --- a/test/clj/game/cards/agendas_test.clj +++ b/test/clj/game/cards/agendas_test.clj @@ -502,11 +502,11 @@ (click-prompt state :runner "Bacterial Programming") (click-prompt state :runner "Steal") (click-prompt state :corp "Yes") + (click-prompt state :corp "OK") (dotimes [_ 7] (let [card (first (prompt-titles :corp))] (click-prompt state :corp card))) - (click-prompt state :corp "Done") ; Finished with trashing - (click-prompt state :corp "Done") ; Finished with move-to-hq (no cards to move) + (click-prompt state :corp "OK") (dotimes [_ 7] (click-prompt state :runner "Facedown card in Archives") (click-prompt state :runner "No action")) @@ -520,32 +520,26 @@ (new-game {:corp {:deck ["Bacterial Programming" "Hedge Fund"]}}) (starting-hand state :corp ["Bacterial Programming"]) (play-and-score state "Bacterial Programming") - (click-prompt state :corp "Yes") - (click-prompt state :corp "Done") - (click-prompt state :corp "Done") - (click-prompt state :corp (first (:deck (get-corp)))) - (click-prompt state :corp "Done") + (click-prompts state :corp "Yes" "OK" "Done" "Done") + (click-prompt state :corp (first (:set-aside (get-corp)))) + (click-prompt state :corp "OK") (is (no-prompt? state :corp) "Bacterial Programming prompts finished") (is (not (:run @state)) "No run is active"))) (deftest bacterial-programming-removing-all-cards-from-r-d-should-not-freeze-for-runner-nor-give-an-extra-access ;; Removing all cards from R&D should not freeze for runner, nor give an extra access. (do-game - (new-game {:corp {:deck [(qty "Bacterial Programming" 8)] - :hand ["Ice Wall"]} + (new-game {:corp {:hand ["Bacterial Programming"] + :deck ["Bacterial Programming" (qty "Vanilla" 7)]} :options {:start-as :runner}}) + (stack-deck state :corp ["Bacterial Programming"]) (run-empty-server state :rd) (click-prompt state :runner "Steal") - (click-prompt state :corp "Yes") + (click-prompts state :corp "Yes" "OK") ;; Move all 7 cards to trash (dotimes [_ 7] - ;; Get the first card listed in the prompt choice - ;; TODO make this function - (let [card (first (prompt-titles :corp))] - (click-prompt state :corp card))) - (click-prompt state :corp "Done") ; Finished with trashing - (click-prompt state :corp "Done") ; Finished with move-to-hq (no cards to move) - ;; Run and prompts should be over now + (click-prompt state :corp "Vanilla")) + (click-prompt state :corp "OK") (is (no-prompt? state :corp) "Bacterial Programming prompts finished") (is (no-prompt? state :runner) "Bacterial Programming prompts finished") (is (not (:run @state))))) @@ -2272,6 +2266,35 @@ (click-prompt state :corp "Done")) (str "Corp drew " n " cards"))))) +(deftest let-them-dream + (doseq [[from agenda] [["HQ" "Project Atlas"] ["R&D" "Ikawah Project"] ["Archives" "Project Kusanagi"]] + to ["HQ" "Bottom of R&D"]] + (do-game + (new-game {:corp {:hand ["Let Them Dream" "Project Atlas"] + :deck [(qty "IPO" 15) "Ikawah Project"] + :discard ["Project Kusanagi"]}}) + (play-and-score state "Let Them Dream") + (click-prompts state :corp from agenda to) + (case to + "HQ" (some #(= (:title %) agenda) (:hand (get-corp))) + "Bottom of R&D" (is (= (:title (last (:deck (get-corp)))) agenda)))))) + +(deftest let-them-dream-points + ;; Global Food Initiative + (do-game + (new-game {:corp {:deck [(qty "Let Them Dream" 2)]}}) + (testing "Corp scores" + (is (zero? (:agenda-point (get-runner))) "Runner should start with 0 agenda points") + (is (zero? (:agenda-point (get-corp))) "Corp should start with 0 agenda points") + (play-and-score state "Let Them Dream") + (click-prompt state :corp "Done") + (is (= 2 (:agenda-point (get-corp))) "Corp should gain 2 agenda points")) + (testing "Runner steals" + (play-from-hand state :corp "Let Them Dream" "New remote") + (take-credits state :corp) + (run-empty-server state :remote2) + (click-prompt state :runner "Steal") + (is (= 1 (:agenda-point (get-runner))) "Runner should gain 1 agenda points, not 2")))) (deftest license-acquisition ;; License Acquisition @@ -2410,6 +2433,33 @@ (is (waiting? state :corp)) (click-prompt state :runner "No"))) +(deftest lotus-haze-basic-test + (do-game + (new-game {:corp {:hand ["Lotus Haze" "Crisium Grid"]}}) + (play-and-score state "Lotus Haze") + (play-from-hand state :corp "Crisium Grid" "HQ") + (rez state :corp (get-content state :hq 0)) + (card-ability state :corp (get-scored state :corp 0) 0) + (click-card state :corp "Crisium Grid") + (is (= (prompt-titles :corp) ["Archives" "R&D"]) "Cannot go to own serveR") + (click-prompt state :corp "R&D") + (is (= "Crisium Grid" (:title (get-content state :rd 0))) "Moved to HQ") + (is (no-prompt? state :runner)))) + +(deftest lotus-haze-movement-rules-test + (do-game + (new-game {:corp {:hand ["Lotus Haze" "Crisium Grid" "ZATO City Grid"] :credits 15}}) + (play-and-score state "Lotus Haze") + (play-cards state :corp ["Crisium Grid" "HQ" :rezzed]) + (play-cards state :corp ["ZATO City Grid" "New remote" :rezzed]) + (card-ability state :corp (get-scored state :corp 0) 0) + (click-card state :corp "Crisium Grid") + (is (= (prompt-titles :corp) ["Archives" "R&D"]) "Cannot go to ontop of ZATO City Grid (region clash)") + (click-prompt state :corp "R&D") + (card-ability state :corp (get-scored state :corp 0) 0) + (click-card state :corp "ZATO City Grid") + (is (= (prompt-titles :corp) ["OK"]) "Cannot go to onto central servers because of the restriction on ZATO City Grid"))) + (deftest luminal-transubstantiation ;; Luminal Transubstantiation (do-game @@ -2592,6 +2642,16 @@ (is (= 7 (:agenda-point (get-corp))) "Corp at 7 points") (is (= :corp (:winner @state)) "Corp has won"))) +(deftest melies-city-luxury-line + (do-game + (new-game {:corp {:hand [(qty "Méliès City Luxury Line" 2)]}}) + ;; play and score spends 1 click + (is (changed? [(:click (get-corp)) 0] + (play-and-score state "Méliès City Luxury Line"))) + (take-credits state :corp) + (run-empty-server state :hq) + (click-prompt state :runner "Pay to steal"))) + (deftest merger ;; Merger (do-game @@ -4054,6 +4114,22 @@ (click-prompt state :runner "0") (is (= 1 (count-tags state)) "Runner should gain a tag from Restructured Datapool ability")))) +(deftest sacrifice-zone-expansion-test + (do-game + (new-game {:corp {:hand ["Sacrifice Zone Expansion"]}}) + (play-from-hand state :corp "Sacrifice Zone Expansion" "New remote") + (is (changed? [(:credit (get-corp)) 2] + (click-advance state :corp (get-content state :remote1 0))) + "gained 3") + (is (changed? [(:credit (get-corp)) -1] + (click-advance state :corp (get-content state :remote1 0))) + "gained 0") + (take-credits state :corp) + (run-empty-server state :hq) + (is (changed? [(count (:hand (get-runner))) -1] + (click-prompt state :corp "Yes")) + "1 meat"))) + (deftest salvo-testing (do-game (new-game {:corp {:hand ["Salvo Testing" "Project Vitruvius"] @@ -5297,3 +5373,15 @@ (play-from-hand state :runner "Hunting Grounds") (card-ability state :runner (get-resource state 0) 0) (is (= 3 (:credit (get-runner))) "Shouldn't lose any credits"))) + +(deftest witch-hunt-correct-tags + (doseq [t [0 1 2 3 4 5 6]] + (do-game + (new-game {:corp {:hand ["Witch Hunt"]}}) + (play-and-score state "Witch Hunt") + (take-credits state :corp) + (is (= 3 (count-tags state))) + (take-credits state :runner) + (is (changed? [(count-tags state) 0 + (count-bad-pub state) 0] + (take-credits state :corp)))))) diff --git a/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index da7520f4ac..4a11b93f0d 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -757,6 +757,7 @@ (run-empty-server state :remote1) (click-prompt state :runner "Steal") (click-prompt state :corp "Yes") + (click-prompt state :corp "OK") (click-prompt state :corp "Chairman Hiro") (click-prompt state :corp "Done") (click-prompt state :corp "Done") @@ -766,7 +767,7 @@ (click-prompt state :corp "Excalibur") (click-prompt state :corp "Fire Wall") (click-prompt state :corp "Gemini") - (click-prompt state :corp "Done") + (click-prompt state :corp "OK") (is (= ["Bacterial Programming"] (mapv :title (get-scored state :runner))) "Runner shouldn't score Chairman Hiro") (is (= ["Chairman Hiro"] (mapv :title (:discard (get-corp)))) "Chairman Hiro should be in Archives"))) @@ -1904,6 +1905,18 @@ (is (= 3 (core/trash-cost state :runner (refresh ep2))) "Trash cost increased to 3 by one active Encryption Protocol")))) +(deftest esca + (doseq [[tags damage] [[0 0] [1 1] [15 1]]] + (do-game + (new-game {:corp {:discard ["Esca"]} + :runner {:hand ["Ika" "Ika"] + :tags tags}}) + (take-credits state :corp) + (is (changed? [(:credit (get-runner)) -1 + (count (:hand (get-runner))) (- damage)] + (run-empty-server state :archives)) + "Tanked it")))) + (deftest estelle-moon ;; Estelle Moon (letfn [(estelle-test [number] @@ -3392,6 +3405,46 @@ (is (no-prompt? state :runner) "No prompt") (is (not (:run @state)) "Access ended after 1 card seen - todachine did his work"))) +(deftest luana-test + (do-game + (new-game {:corp {:hand ["Luana Campos" "Extract"] + :deck [(qty "IPO" 10)] + :bad-pub 1}}) + (play-cards state :corp ["Luana Campos" "New remote" :rezzed]) + (take-credits state :corp) + (take-credits state :runner) + (is (changed? [(:credit (get-corp)) 3 + (count-bad-pub state) -1 + (count (:hand (get-corp))) 2] + (click-prompt state :corp "Yes")) + "Took a BP to get value") + (is (changed? [(count-bad-pub state) 1] + (play-cards state :corp ["Extract" "Luana Campos"])) + "Took BP back"))) + +(deftest magistrate-revontuler + (do-game + (new-game {:corp {:hand ["Magistrate Revontulet" "Greenmail" "Project Beale" "Project Atlas"]} + :runner {:credits 20}}) + (play-from-hand state :corp "Magistrate Revontulet" "New remote") + (play-from-hand state :corp "Project Atlas" "New remote") + (rez state :corp (get-content state :remote1 0)) + (is (rezzed? (get-content state :remote1 0))) + (play-and-score state "Greenmail") + (is (changed? [(:credit (get-runner)) -3] + (click-prompt state :corp "Greenmail")) + "Taxed on score") + (take-credits state :corp) + (run-empty-server state :hq) + (is (changed? [(:credit (get-runner)) -3] + (click-prompt state :runner "Pay to steal")) + "paid 3 to steal") + (is (no-prompt? state :runner)) + (run-empty-server state :remote2) + (is (changed? [(:credit (get-runner)) -3] + (click-prompt state :runner "Pay to steal")) + "paid 3 to steal"))) + (deftest malia-icon-goes-away-with-cupellation (do-game (new-game {:corp {:hand ["Malia Z0L0K4"]} @@ -4100,6 +4153,24 @@ (take-credits state :runner)) "Drew 2 cards -> mandatory + nico trash effect")))) +(deftest nihilo-agent + (do-game + (new-game {:corp {:hand ["Nihilo Agent"]}}) + (play-from-hand state :corp "Nihilo Agent" "New remote") + (rez state :corp (get-content state :remote1 0)) + (dotimes [n 3] + (is (not (jinteki.utils/is-tagged? state)) "Not tagged") + (take-credits state :corp) + (start-turn state :runner) + (is (= 1 (count-bad-pub state)) "Took 1 bad pub") + (is (jinteki.utils/is-tagged? state) "tagged") + (take-credits state :runner) + (when-not (= n 2) + (is (= 0 (count-bad-pub state)) "lost 1 bad pub") + (is (not (jinteki.utils/is-tagged? state)) "untagged again"))) + (is (= 1 (count-bad-pub state)) "Took 1 bad pub") + (is (jinteki.utils/is-tagged? state) "tagged"))) + (deftest open-forum ;; Open Forum (do-game diff --git a/test/clj/game/cards/events_test.clj b/test/clj/game/cards/events_test.clj index a779984ec8..827fa6880c 100644 --- a/test/clj/game/cards/events_test.clj +++ b/test/clj/game/cards/events_test.clj @@ -39,27 +39,54 @@ ;; New Angeles City Hall interaction (do-game (new-game {:runner {:deck ["Account Siphon" - "New Angeles City Hall"]}}) - (core/gain state :corp :bad-publicity 1) - (is (= 1 (count-bad-pub state)) "Corp has 1 bad publicity") - (core/lose state :runner :credit 1) - (is (= 4 (:credit (get-runner))) "Runner has 4 credits") - (take-credits state :corp) ; pass to runner's turn by taking credits - (is (= 8 (:credit (get-corp))) "Corp has 8 credits") + "New Angeles City Hall"] + :credits 4} + :corp {:bad-pub 1}}) + (take-credits state :corp) (play-from-hand state :runner "New Angeles City Hall") (is (= 3 (:credit (get-runner))) "Runner has 3 credits") - (let [nach (get-resource state 0)] - (play-run-event state "Account Siphon" :hq) - (click-prompt state :runner "Account Siphon") - (is (= 4 (:credit (get-runner))) "Runner still has 4 credits due to BP") - (click-prompt state :runner "New Angeles City Hall") - (click-prompt state :runner "Yes") - (is (= 2 (:credit (get-runner))) "Runner has 2 credits left") - (click-prompt state :runner "Yes")) + (play-run-event state "Account Siphon" :hq) + (click-prompt state :runner "Account Siphon") + (is (changed? [(:credit (get-runner)) -1] + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") + (select-bad-pub state 1)) + "Spent 1 + 1 from bad pub") + (is (= 2 (:credit (get-runner))) "Runner has 2 credits left") + (click-prompt state :runner "Yes") (is (zero? (count-tags state)) "Runner did not take any tags") (is (= 10 (:credit (get-runner))) "Runner gained 10 credits") (is (= 3 (:credit (get-corp))) "Corp lost 5 credits"))) +(deftest aircheck-basic-test + (do-game + (new-game {:runner {:hand ["Aircheck"]} + :corp {:hand ["Adonis Campaign"] + :deck ["Adonis Campaign"]}}) + (play-from-hand state :corp "Adonis Campaign" "New remote") + (take-credits state :corp) + (play-from-hand state :runner "Aircheck") + (click-prompt state :runner "R&D") + (run-continue-until state :success) + (do-trash-prompt state 3) + (click-prompts state :runner "Aircheck" "Aircheck" "Aircheck" "Server 1") + (run-continue-until state :success) + (is (= ["No action"] (prompt-titles :runner)) + "Cannot pay to trash because paying from the credit pool is forbidden"))) + +(deftest aircheck-cannot-lose-credits + (do-game + (new-game {:runner {:hand ["Aircheck"]} + :corp {:hand ["Whitespace"]}}) + (play-cards state :corp ["Whitespace" "HQ" :rezzed]) + (take-credits state :corp) + (play-cards state :runner ["Aircheck" "HQ"]) + (run-continue-until state :encounter-ice) + (is (changed? [(:credit (get-runner)) 0] + (fire-subs state (get-ice state :hq 0)) + (is (not (:run @state)) "Run ended")) + "Run ended, but no credits were lost because they cannot be"))) + (deftest always-have-a-backup-plan-jacking-out-correctly-triggers-ahbp ;; Jacking out correctly triggers AHBP (do-game @@ -507,6 +534,30 @@ (is (= (inc n) (count (get-in @state [:corp :deck]))) "1 card was shuffled into R&D") (is (zero? (count (get-in @state [:corp :servers :remote2 :content]))) "No cards left in server 3")))) +(deftest beta-build + (do-game + (new-game {:runner {:hand ["Beta Build"] :deck ["Orca"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Beta Build") + (click-prompt state :runner "Orca") + (is (= "Orca" (:title (get-program state 0)))) + (click-prompt state :runner "HQ") + (run-continue-until state :success) + (click-prompt state :runner "No action") + (is-deck? state :runner ["Orca"]))) + +#_(deftest ^:kaocha/pending beta-build-cannot-run + ;; note that peace in our time needs to be updated currently, it forbids + ;; run events when it should not + (do-game + (new-game {:runner {:hand ["Beta Build" "Peace in Our Time"] + :deck ["Orca"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Peace in Our Time") + (play-from-hand state :runner "Beta Build") + (click-prompt state :runner "Orca") + (is (no-prompt? state :runner)))) + (deftest black-hat ;; Black Hat (do-game @@ -1159,6 +1210,29 @@ (is (= "Jackson Howard" (:title (second (rest (rest (:deck (get-corp)))))))) (is (= "Global Food Initiative" (:title (second (rest (rest (rest (:deck (get-corp))))))))))) +(deftest chain-reaction-test + (do-game + (new-game {:corp {:hand ["Vanilla" "Enigma" "PAD Campaign"]} + :runner {:hand ["Chain Reaction" "Chain Reaction" "Ika"]}}) + (play-from-hand state :corp "PAD Campaign" "New remote") + (play-from-hand state :corp "Vanilla" "Server 1") + (play-from-hand state :corp "Enigma" "Server 1") + (take-credits state :corp) + (core/gain state :runner :click 2) + (run-empty-server state :hq) + (run-empty-server state :rd) + (run-empty-server state :archives) + (play-from-hand state :runner "Chain Reaction") + (click-prompts state :runner "Vanilla" "Enigma") + (is (= 2 (count (:discard (get-corp)))) "Trashed 2") + (is (no-prompt? state :corp) "No prompt to trash nothing") + (play-from-hand state :runner "Ika") + (play-from-hand state :runner "Chain Reaction") + (click-card state :runner "PAD Campaign") + (is (= 3 (count (:discard (get-corp)))) "Trashed 1") + (click-card state :corp "Ika") + (is (= 3 (count (:discard (get-runner)))) "Trashed 1, runner prompt did not block"))) + (deftest charm-offensive (do-game (new-game {:corp {:deck [(qty "Hedge Fund" 5)] @@ -1681,7 +1755,7 @@ (play-from-hand state :runner "Investigative Journalism") (is (= "Investigative Journalism" (:title (get-resource state 1))) "IJ able to be installed") (run-on state "HQ") - (is (= 1 (:run-credit (get-runner))) "1 run credit from bad publicity") + (is (= 1 (:bad-publicity-available (:run @state))) "1 run credit from bad publicity") (run-jack-out state) (play-from-hand state :runner "Activist Support") (take-credits state :runner) @@ -2048,9 +2122,7 @@ (click-prompt state :runner "Steal") (is (not (:run @state))) ;; resolve bacterial - (click-prompt state :corp "Yes") - (click-prompt state :corp "Done") - (click-prompt state :corp "Done") + (click-prompts state :corp "Yes" "OK" "Done" "Done") (click-prompt state :corp "Hedge Fund") (click-prompt state :corp "Hedge Fund") (click-prompt state :corp "Hedge Fund") @@ -2058,7 +2130,7 @@ (click-prompt state :corp "Hedge Fund") (click-prompt state :corp "Hedge Fund") (click-prompt state :corp "Hedge Fund") - (click-prompt state :corp "Done") + (click-prompt state :corp "OK") (is (not (:run @state))) ;; should be able to continue to stealing jumon (click-prompt state :runner "Yes") @@ -4431,6 +4503,44 @@ (run-continue state) (is (get-ice state :hq 0) "Second Ice Wall is not trashed"))) +(deftest kompromat-test + (testing "Kompromat" + (do-game + (new-game {:corp {:hand ["Ice Wall"]} + :runner {:hand ["Kompromat"]}}) + (play-from-hand state :corp "Ice Wall" "HQ") + (take-credits state :corp) + (play-from-hand state :runner "Kompromat") + (click-prompt state :runner "HQ") + (rez state :corp (get-ice state :hq 0)) + (run-continue-until state :encounter-ice) + (fire-subs state (get-ice state :hq 0)) + (is (no-prompt? state :corp)))) + (testing "Kompromat successful" + (do-game + (new-game {:corp {:hand ["Ice Wall"]} + :runner {:hand ["Kompromat"]}}) + (play-from-hand state :corp "Ice Wall" "HQ") + (take-credits state :corp) + (play-from-hand state :runner "Kompromat") + (click-prompt state :runner "HQ") + (rez state :corp (get-ice state :hq 0)) + (run-continue-until state :success) + (click-card state :corp "Ice Wall")))) + +(deftest kompromat-test-take-bp + (do-game + (new-game {:corp {:hand ["Ice Wall"]} + :runner {:hand ["Kompromat"]}}) + (play-from-hand state :corp "Ice Wall" "HQ") + (take-credits state :corp) + (play-from-hand state :runner "Kompromat") + (click-prompt state :runner "HQ") + (rez state :corp (get-ice state :hq 0)) + (run-continue-until state :success) + (click-prompt state :corp "Done") + (is (= 1 (count-bad-pub state))))) + (deftest kraken ;; Kraken (do-game @@ -6500,13 +6610,14 @@ (deftest rumor-mill-full-test ;; Full test (do-game - (new-game {:corp {:deck [(qty "Project Atlas" 2) + (new-game {:corp {:hand [(qty "Project Atlas" 2) "Caprice Nisei" "Chairman Hiro" "Cybernetics Court" "Elizabeth Mills" "Ibrahim Salem" - "Housekeeping" "Director Haas" "Oberth Protocol"]} + "Housekeeping" "Director Haas" "Oberth Protocol"] + :credits 100 + :bad-pub 1} :runner {:deck ["Rumor Mill"]}}) - (core/gain state :corp :credit 100 :click 100 :bad-publicity 1) - (draw state :corp 100) + (core/gain state :corp :click 100) (play-from-hand state :corp "Caprice Nisei" "New remote") (play-from-hand state :corp "Chairman Hiro" "New remote") (play-from-hand state :corp "Cybernetics Court" "New remote") @@ -6542,9 +6653,11 @@ ;; Trashable execs (run-empty-server state :remote2) (click-prompt state :runner "Pay 6 [Credits] to trash") + (select-bad-pub state 1) (is (empty? (:scored (get-runner))) "Chairman Hiro not added to runner's score area") (run-empty-server state "R&D") (click-prompt state :runner "Pay 5 [Credits] to trash") + (select-bad-pub state 1) (is (empty? (:scored (get-runner))) "Director Haas not added to runner's score area") (take-credits state :runner) ;; Trash RM, make sure everything works again @@ -7137,6 +7250,17 @@ (play-from-hand state :runner "Strike Fund")) "Gained 3 credits from playing Strike Fund"))) +(deftest sell-out-test + (do-game + (new-game {:runner {:hand ["Sell Out" "The Supplier"] :deck [(qty "Ika" 10)]}}) + (take-credits state :corp) + (play-from-hand state :runner "The Supplier") + (play-from-hand state :runner "Sell Out") + (is (changed? [(:credit (get-runner)) 4 + (count (:hand (get-runner))) 2] + (click-card state :runner "The Supplier")) + "Gained 4 and drew 2"))) + (deftest sure-gamble ;; Sure Gamble (do-game @@ -7307,6 +7431,42 @@ (is (= 2 (core/breaker-strength state :runner (refresh c1))) "Corroder 1 has 2 strength") (is (= 2 (core/breaker-strength state :runner (refresh c2))) "Corroder 2 has 2 strength")))) +(deftest tailgate-test + (dotimes [ices 4] + (do-game + (new-game {:corp {:hand (vec (take (+ 4 ices) ["Hedge Fund" "Hedge Fund" "Hedge Fund" + "Hedge Fund" + "Vanilla" "Vanilla" "Vanilla"]))} + :runner {:hand ["Tailgate"]}}) + (dotimes [_ ices] + (play-from-hand state :corp "Vanilla" "HQ")) + (take-credits state :corp) + (is (changed? [(:credit (get-runner)) (min 0 (- ices 3))] + (play-from-hand state :runner "Tailgate")) + "discounted") + (run-continue-until state :success) + (dotimes [n 3] + (click-prompt state :runner "No action")) + (is (no-prompt? state :runner))))) + +(deftest take-a-dive-test + (doseq [fire? [true nil]] + (do-game + (new-game {:corp {:hand ["Rime"]} + :runner {:hand ["Take a Dive"]}}) + (play-from-hand state :corp "Rime" "HQ") + (rez state :corp (get-ice state :hq 0)) + (take-credits state :corp) + (play-from-hand state :runner "Take a Dive") + (click-prompt state :runner "HQ") + (run-continue-until state :encounter-ice) + (when fire? + (card-subroutine state :corp (get-ice state :hq 0) 0)) + (run-continue-until state :success) + (is (no-prompt? state :corp)) + (is (= (if fire? 1 0) (count-bad-pub state)) "BP when fired") + (is (= "Take a Dive" (get-in @state [:runner :rfg 0 :title])))))) + (deftest test-run-programs-hosted-after-install-get-returned-to-stack-issue-1081 ;; Programs hosted after install get returned to Stack. Issue #1081 (do-game diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index 8202d07f26..7f3b60655a 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -1044,6 +1044,18 @@ (click-prompt state :runner "End the run") (is (:broken (first (:subroutines (refresh iw)))) "Ice Wall has been broken")))) +(deftest borrowed-goods-test + (do-game + (new-game {:runner {:hand [(qty "Borrowed Goods" 4)]}}) + (take-credits state :corp) + (is (changed? [(count-tags state) 1] + (play-from-hand state :runner "Borrowed Goods")) + "Took a tag") + (dotimes [_ 3] + (is (changed? [(count-tags state) 0] + (play-from-hand state :runner "Borrowed Goods")) + "Did not take a tag")))) + (deftest box-e ;; Box-E - +2 MU, +2 max hand size (do-game @@ -1314,6 +1326,7 @@ (click-card state :corp "Hostile Takeover") (run-continue state) (card-ability state :runner (get-program state 0) 2) + (select-bad-pub state 1) (card-ability state :runner (get-program state 0) 2) (card-ability state :runner (get-program state 0) 0) (click-prompt state :runner "Gain 2 [Credits]") @@ -1932,7 +1945,7 @@ (is (:run @state) "New run started") (run-continue state) (is (= [:rd] (:server (:run @state))) "Running on R&D") - (is (= 1 (:run-credit (get-runner))) "Runner has 1 BP credit"))) + (is (= 1 (:bad-publicity-available (:run @state))) "Runner has 1 BP credit"))) (deftest doppelganger-makers-eye-interaction ;; Makers eye interaction @@ -3698,6 +3711,22 @@ (is (no-prompt? state :runner) "No more prompts for runner") (is (not (:run @state)) "Run is ended"))) +(deftest methuselah-test + (do-game + (new-game {:corp {:deck [(qty "PAD Campaign" 20)] :hand ["IPO"]} + :runner {:hand ["Mantle" "Methuselah" "DZMZ Optimizer"] + :credits 10}}) + (take-credits state :corp) + (play-from-hand state :runner "Mantle") + (play-from-hand state :runner "Methuselah") + (run-on state :rd) + (click-card state :runner "DZMZ Optimizer") + (run-continue-until state :success) + (click-prompts state :runner "Pay 4 [Credits] to trash") + (dotimes [_ 2] + (click-card state :runner "Methuselah")) + (is (no-prompt? state :runner) "Paid 2 with methuselah"))) + (deftest mind-s-eye-interaction-with-rdi-aeneas ;; Interaction with RDI + Aeneas (do-game @@ -4853,6 +4882,20 @@ (is (= 0 (count (:hand (get-runner))))) (is (= ["Easy Mark" "Ika"] (map :title (:discard (get-runner))))))) +(deftest rotary-test + (do-game + (new-game {:runner {:hand ["Rotary"]} + :corp {:hand [(qty "IPO" 4)] + :deck ["IPO" "IPO" "IPO"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Rotary") + (run-empty-server state :rd) + (is (changed? [(count-tags state) 1] + (click-prompt state :runner "Yes")) + "Tag on") + (click-prompt state :runner "No action") + (click-prompt state :runner "No action"))) + (deftest rubicon-switch ;; Rubicon Switch (do-game @@ -5588,6 +5631,23 @@ (click-card state :runner tt)) "Used 2 credits from The Toolbox")))) +(deftest the-tungsten-tailor-test + (do-game + (new-game {:runner {:hand ["The Tungsten Tailor" "Corroder"] + :credits 10} + :corp {:hand ["Ice Wall"]}}) + (play-from-hand state :corp "Ice Wall" "HQ") + (take-credits state :corp) + (rez state :corp (get-ice state :hq 0)) + (play-from-hand state :runner "The Tungsten Tailor") + (play-from-hand state :runner "Corroder") + (run-on state :hq) + (run-continue-until state :encounter-ice) + (is (= 0 (get-strength (get-ice state :hq 0))) "-1 str") + (is (changed? [(:credit (get-runner)) 0] + (card-ability state :runner (get-program state 0) 0) + (click-prompt state :runner "End the run"))))) + (deftest the-wizards-chest (do-game (new-game {:corp {:hand [] :deck []} @@ -5810,6 +5870,20 @@ (is (= 3 (:agenda-point (get-runner))) "Runner got 3 points") (is (= 2 (count (:scored (get-runner)))) "Runner got 2 cards in score area"))) +(deftest touchstone-test + (do-game + (new-game {:runner {:hand ["Touchstone" "Clean Getaway"]} + :corp {:hand ["PAD Campaign"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Touchstone") + (play-from-hand state :runner "Clean Getaway") + (click-prompt state :runner "HQ") + (run-continue-until state :success) + (is (changed? [(:credit (get-runner)) -3] + (do-trash-prompt state 4) + (click-card state :runner "Touchstone")) + "3 + 1 for touchstone"))) + (deftest turntable ;; Turntable - Swap a stolen agenda for a scored agenda (do-game diff --git a/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index 2fda35979c..a3c9f2d92f 100644 --- a/test/clj/game/cards/ice_test.clj +++ b/test/clj/game/cards/ice_test.clj @@ -608,6 +608,26 @@ (is (waiting? state :runner) "Runner has prompt to wait for Corp to use Ganked!")))) +(deftest ansel-2.0-subs-test + (testing "trash 1 installed card" + (do-game + (subroutine-test "Ansel 2.0" 0 nil {:rig ["Fermenter"]}) + (click-card state :corp "Fermenter") + (is (= "Fermenter" (->> (get-runner) :discard first :title)) "Trashed fermenter"))) + (testing "remove a card in the heap from the game" + (do-game + (subroutine-test "Ansel 2.0" 1 {:runner {:discard ["Fermenter" "Ika" "Rezeki"]}}) + (click-card state :corp "Fermenter") + (is (= ["Ika" "Rezeki"] (->> (get-runner) :discard (mapv :title))) "Only ika/zeki in bin"))) + (testing "install a card from HQ or Archives" + (doseq [zone [:hand :discard]] + (do-game + (subroutine-test "Ansel 2.0" 2 {:corp {zone ["PAD Campaign"]}}) + (click-card state :corp "PAD Campaign") + (click-prompt state :corp "New remote")))) + (testing "end the run" + (do-game (etr-sub "Ansel 2.0" 3)))) + (deftest anvil (do-game (new-game {:corp {:hand ["Anvil" "Ice Wall"]} @@ -2379,6 +2399,30 @@ (is (= 1 (count (:subroutines (get-ice state :hq 0))))) (is (= 0 (:index (first (:subroutines (get-ice state :hq 0)))))))) +(deftest event-horizon-subs + (doseq [opt [:pay :resolve]] + (do-game + (subroutine-test "Event Horizon" 0 nil {:rig ["Rezeki"]}) + (case opt + :resolve (do (click-prompt state :runner "The Corp trashes a Program") + (click-card state :corp "Rezeki") + (is (= 1 (count (:discard (get-runner)))))) + :pay (click-prompt state :runner "Pay 3 [Credits]"))) + (do-game + (subroutine-test "Event Horizon" 1) + (case opt + :resolve (do (click-prompt state :runner "End the run") + (is (not (:run @state)))) + :pay (click-prompt state :runner "Pay 3 [Credits]"))))) + +(deftest event-horizon-ability-end-the-run + (do-game + (run-and-encounter-ice-test "Event Horizon") + (is (:run @state) "Running") + (card-ability state :corp (get-ice state :hq 0) 0) + (is (not (:run @state)) "Run ended") + (is (= "Event Horizon" (-> (get-corp) :discard first :title)) "Event Horizon trashed"))) + (deftest excalibur ;; Excalibur - Prevent Runner from making another run this turn (do-game @@ -2407,6 +2451,39 @@ (run-on state "HQ") (is (:run @state) "Run initiated ok")))) +(deftest ezam-subroutines-test + (testing "Look at the top card of R&D. Place it on the bottom." + (do-game + (subroutine-test "ezaM" 0 {:corp {:deck ["PAD Campaign" "IPO"]}}) + (let [d (map :title (:deck (get-corp)))] + (click-prompt state :corp "Place it on the bottom of R&D") + (is-deck? state :corp (reverse d))))) + (testing "Look at the top card of R&D. Leave it there." + (do-game + (subroutine-test "ezaM" 0 {:corp {:deck ["PAD Campaign" "IPO"]}}) + (let [d (map :title (:deck (get-corp)))] + (click-prompt state :corp "Done") + (is-deck? state :corp d)))) + (testing "Give ice +1 strength." + (do-game + (subroutine-test "ezaM" 1) + (is (= (get-strength (get-ice state :hq 0)) (+ 1 (:strength (get-ice state :hq 0)))) + "Ice strength boosted") + (run-continue-until state :success) + (is (= (get-strength (get-ice state :hq 0)) (:strength (get-ice state :hq 0))) + "Ice strength reset")))) + +(deftest ezam-swaps-with-other-ice + (do-game + (new-game {:corp {:hand ["ezaM" "Vanilla"]}}) + (play-from-hand state :corp "ezaM" "HQ") + (play-from-hand state :corp "Vanilla" "Archives") + (rez state :corp (get-ice state :hq 0)) + (card-ability state :corp (get-ice state :hq 0) 0) + (click-card state :corp "Vanilla") + (is (= "Vanilla" (:title (get-ice state :hq 0)))) + (is (= "ezaM" (:title (get-ice state :archives 0)))))) + (deftest f2p ;; F2P (do-game @@ -2587,6 +2664,20 @@ (deftest flyswatter-etr-sub (do-game (etr-sub "Flyswatter" 0))) +(deftest flywheel-subs + (doseq [sub [0 1] + opt [:draw :no-draw]] + (do-game + (subroutine-test "Flywheel" sub {:corp {:deck [(qty "IPO" 10)]}}) + (is (= 6 (:credit (get-corp))) "Gained 1 credit") + (case opt + :draw (is (changed? [(count (:hand (get-corp))) 1] + (click-prompt state :corp "Yes")) + "Drew and gained a cred") + :no-draw (is (changed? [(count (:hand (get-corp))) 0] + (click-prompt state :corp "No")) + "Did not draw, just gained a cred"))))) + (deftest formicary-verifies-basic-functionality ;; Verifies basic functionality (do-game @@ -3253,6 +3344,50 @@ (click-prompt state :runner "End the run unless the Runner pays 3 [Credits]")) "Get taxed 1c for breaking with Grappling Hook")))) +(deftest grubber-subroutine-test + (doseq [sub [0 1] + option ["Pay 3 [Credits]" "End the run"]] + (do-game + (subroutine-test "Grubber" sub) + (click-prompt state :runner option) + (case option + "End the run" (is (not (:run @state)) "Run ended") + "Pay 3 [Credits]" (is (changed? [(:credit (get-runner)) -3] + (click-prompt state :runner "Done") + (is (:run @state) "still running") + (is (no-prompt? state :runner) "No prompt")) + "Paid 3 (not using bad pub)"))))) + +(deftest grubber-subroutine-test + (doseq [sub [0 1] + option ["Pay 3 [Credits]" "End the run"]] + (do-game + (subroutine-test "Grubber" sub) + (click-prompt state :runner option) + (case option + "End the run" (is (not (:run @state)) "Run ended") + "Pay 3 [Credits]" (is (changed? [(:credit (get-runner)) -3] + (click-prompt state :runner "Done") + (is (:run @state) "still running") + (is (no-prompt? state :runner) "No prompt")) + "Paid 3 (not using bad pub)"))))) + +(deftest grubber-bad-pub-on-centrals + (doseq [[server s-key] [["HQ" :hq] ["R&D" :rd] ["Archives" :archives]]] + (testing (str "bad publicity on " server) + (do-game + (new-game {:corp {:hand ["Grubber"] :credits 10}}) + (play-from-hand state :corp "Grubber" server) + (rez state :corp (get-ice state s-key 0)) + (is (= 1 (count-bad-pub state)) "Gained a bad pub"))))) + +(deftest grubber-no-bad-pub-on-remotes + (do-game + (new-game {:corp {:hand ["Grubber"] :credits 10}}) + (play-from-hand state :corp "Grubber" "New remote") + (rez state :corp (get-ice state :remote1 0)) + (is (= 0 (count-bad-pub state)) "Gained no bad pub"))) + (deftest gyri-labyrinth ;; Gyri Labyrinth - reduce runner handsize by 2 until beginning of corp's next turn (do-game @@ -4630,6 +4765,52 @@ (score-agenda state :corp (get-content state :remote1 0)) (is (= 1 (count (:discard (get-corp)))) "Trashed pickpocket"))) +(deftest lethe-sub-2-move-a-card-to-top-or-bottom-of-rd + (doseq [[p f] [["Top of R&D" first] ["Bottom of R&D" last]]] + (do-game + (subroutine-test "Lethe" 0 {:corp {:discard ["IPO"] :deck [(qty "Hedge Fund" 15)]}}) + (click-card state :corp "IPO") + (click-prompt state :corp p) + (is (= "IPO" (:title (f (:deck (get-corp))))))))) + +(deftest lethe-sub-1-return-runner-card-to-grip + (do-game + (subroutine-test "Lethe" 1 nil {:rig ["Rezeki"]}) + (click-card state :corp "Rezeki") + (is-hand? state :runner ["Rezeki"]))) + +(deftest lethe-on-bypass-take-a-tag + (do-game + (run-and-encounter-ice-test "Lethe" nil {:run-event ["Inside Job" "HQ"]}) + (is (= 1 (count-tags state)) "Tagged on bypassing Lethe"))) + +(deftest lethe-give-tag-on-fully-breaking + (do-game + (run-and-encounter-ice-test "Lethe" {:runner {:credits 15}} {:rig ["Carmen"]}) + (auto-pump-and-break state (get-program state 0)) + (is (= 1 (count-tags state)) "Tagged on fully breaking Lethe"))) + +(deftest lionsmane-other-subs + (testing "Do 2 net damage" + (do-game (does-damage-sub "Lionsmane" 0 2))) + (testing "Do 2 net damage unless the Runner pays 3 Credits" + (do-game + (subroutine-test "Lionsmane" 1 {:runner {:hand 5}}) + (click-prompt state :runner "Pay 3 [Credits]")) + (do-game + (subroutine-test "Lionsmane" 1 {:runner {:hand 5}}) + (click-prompt state :runner "Corp does 2 net damage") + (is (= 3 (count (:hand (get-runner))))))) + (testing "Do 2 net damage unless the runner jacks out" + (do-game + (subroutine-test "Lionsmane" 2 {:runner {:hand 5}}) + (click-prompt state :runner "Jack out") + (is (= 5 (count (:hand (get-runner)))))) + (do-game + (subroutine-test "Lionsmane" 2 {:runner {:hand 5}}) + (click-prompt state :runner "Corp does 2 net damage") + (is (= 3 (count (:hand (get-runner)))))))) + (deftest lockdown ;; Lockdown - Prevent Runner from drawing cards for the rest of the turn (do-game @@ -6230,6 +6411,15 @@ (auto-pump-and-break state corroder) (is (nil? (get-ice state :hq 0)) "Paper Wall was trashed")))) +(deftest paywall-test + (do-game + (run-and-encounter-ice-test "Paywall") + (is (= 4 (:credit (get-runner))) "lose 1 credit on encounter") + (fire-subs state (get-ice state :hq 0)) + (click-prompt state :runner "Pay 1 [Credits]") + (is (:run @state) "run not ended") + (is (= 3 (:credit (get-runner))) "paid 1 to not etr"))) + (deftest peeping-tom ;;Peeping Tom - Counts # of chosen card type in Runner grip (do-game @@ -6501,6 +6691,18 @@ (click-prompt state :runner "0") (is (not (:run @state)) "Run has been ended")))) +(deftest reverb-discount + (do-game + (new-game {:corp {:hand ["Reverb" "Vanilla"]}}) + (play-from-hand state :corp "Reverb" "HQ") + (play-from-hand state :corp "Vanilla" "HQ") + (is (changed? [(:credit (get-corp)) -3] + (rez state :corp (get-ice state :hq 0))) + "Only spent 3"))) + +(deftest reverb-sub-0-etr (do-game (new-game (etr-sub "Reverb" 0)))) +(deftest reverb-sub-1-etr (do-game (new-game (etr-sub "Reverb" 1)))) + (deftest rime ;; Rime (do-game @@ -7223,6 +7425,22 @@ (run-jack-out state) (is (= (+ credits 10) (:credit (get-corp))) "Corp should only gain money once"))))) +;; Tests for 'Jog Gate' (version 11.0) +(deftest sleipnir-sub-0-maybe-draw-1-cards + (do-game + (subroutine-test "Sleipnir" 0 {:corp {:hand 0 :deck (inc 1)}}) + (click-prompt state :corp "Yes") + (is (= 1 (count (:hand (get-corp)))) "Drew 1"))) + +(deftest sleipnir-sub-1-shuffle-from-hq-or-archives + (doseq [zone [:hand :discard]] + (do-game + (subroutine-test "Sleipnir" 1 {:corp {:deck 0 zone ["IPO"]}}) + (click-card state :corp "IPO") + (is-deck? state :corp ["IPO"])))) + +(deftest sleipnir-sub-2-etr (do-game (new-game (etr-sub "Sleipnir" 2)))) + (deftest slot-machine ;; Slot Machine (do-game @@ -7942,6 +8160,7 @@ (run-continue state) (card-ability state :runner (get-program state 0) 0) (click-prompt state :runner "End the run") + (select-bad-pub state 1) (run-continue state) (click-prompt state :corp "Yes") (click-card state :corp "Ice Wall") @@ -8086,6 +8305,31 @@ (click-prompt state :runner "2") (is (not (rezzed? (refresh tmi))))))) +(deftest tocsin-sub-0-lose-2-creds + (do-game + (subroutine-test "Tocsin" 0 {:runner {:credits 3}}) + (is (= 1 (:credit (get-runner))) "lost 2 credits"))) + +(deftest tocsin-sub-1-etr (do-game (new-game (etr-sub "Tocsin" 1)))) +(deftest tocsin-sub-2-etr (do-game (new-game (etr-sub "Tocsin" 2)))) + +(deftest tocsin-expend-ability + (do-game + (new-game {:corp {:hand ["Tocsin"] + :deck ["Guard" "Ice Wall"]}}) + (expend state :corp (first (:hand (get-corp)))) + (click-prompts state :corp "Ice Wall" "Guard" "OK") + (is-hand? state :corp ["Guard" "Ice Wall"]))) + +(deftest tocsin-expend-ability-with-cancel + (do-game + (new-game {:corp {:hand ["Tocsin"] + :deck ["Guard" "Ice Wall"]}}) + (expend state :corp (first (:hand (get-corp)))) + (click-prompts state :corp "Ice Wall" "Guard" "I want to start over" "Ice Wall" "Cancel" "OK") + (is-hand? state :corp ["Ice Wall"]))) + + (deftest tour-guide-rez-before-other-assets ;; Rez before other assets (do-game @@ -8650,6 +8894,35 @@ (card-subroutine state :corp (refresh vas) 0) (is (= 1 (count-tags state)) "Runner took 1 tag")))) +(deftest vertigo-sub-0-lose-click + (do-game + (subroutine-test "Vertigo" 0) + (is (= 2 (:click (get-runner))) "Lost a click"))) + +(deftest vertigo-skill-issue + (doseq [target ["Rashida Jaheem" "Project Atlas"]] + (do-game + (new-game {:corp {:hand [target "Vertigo"]}}) + (play-from-hand state :corp "Vertigo" "HQ") + (take-credits state :corp) + (rez state :corp (get-ice state :hq 0)) + (core/lose state :runner :click 3) + (run-on state :hq) + (run-continue-until state :success) + (is (= ["No action"] (prompt-titles :runner)) "Cannot trash/steal due to Vertigo")))) + +(deftest vicsek-test-x-damage-and-x-tags + (dotimes [x 10] + (do-game (subroutine-test "Vicsek" 0 {:runner {:tags x :hand (inc x)}}) + (is (= x (count (:discard (get-runner))))) + (is (= (* 2 x) (count-tags state)))))) + +(deftest viksek-give-a-tag-and-trash-itself + (dotimes [x 10] + (do-game (subroutine-test "Vicsek" 1 {:runner {:tags x}}) + (is (= (inc x) (count-tags state))) + (is (= "Vicsek" (:title (first (:discard (get-corp))))) "Trashed itself")))) + (deftest virtual-service-agent (do-game (new-game {:corp {:hand ["Virtual Service Agent"]} diff --git a/test/clj/game/cards/identities_test.clj b/test/clj/game/cards/identities_test.clj index 95194b22ff..a2150edff8 100644 --- a/test/clj/game/cards/identities_test.clj +++ b/test/clj/game/cards/identities_test.clj @@ -1441,6 +1441,15 @@ (click-card state :corp "NASX") (is (= "NASX" (:title (first (:hosted (get-content state :remote1 0)))))))) +(deftest editorial-division-ad-nihilum + (do-game + (new-game {:corp {:id "Editorial Division: Ad Nihilum" + :hand ["Too Big to Fail"] + :deck ["Closed Accounts"]}}) + (play-from-hand state :corp "Too Big to Fail") + (click-prompts state :corp "Yes" "Closed Accounts") + (is-hand? state :corp ["Closed Accounts"]))) + (deftest edward-kim-humanity-s-hammer-trash-first-operation-accessed-each-turn-but-not-if-first-one-was-in-archives ;; Trash first operation accessed each turn, but not if first one was in Archives (do-game @@ -2370,6 +2379,19 @@ (card-ability state :runner (get-resource state 0) 0) (is (no-prompt? state :corp) "No Hayley wait prompt for facedown installs."))) +(deftest hiram-0mission-svensson-shadow-of-the-past + (do-game + (new-game {:corp {:deck ["IPO"] :hand ["Beanstalk Royalties"]} + :runner {:id "Hiram \"0mission\" Svensson: Shadow of the Past" + :hand ["Sports Hopper"]}}) + (play-from-hand state :corp "Beanstalk Royalties") + (is (no-prompt? state :runner)) + (take-credits state :corp) + (play-from-hand state :runner "Sports Hopper") + (click-prompt state :runner "Noted") + (card-ability state :runner (get-hardware state 0) 0) + (click-prompt state :runner "Noted"))) + (deftest hoshiko-shiro-untold-protagonist-id-ability ;; ID ability (do-game @@ -3500,6 +3522,19 @@ (is (= 3 (:click (get-runner))) "Wyldside caused 1 click to be lost") (is (= 3 (count (:hand (get-runner)))) "3 cards drawn total")))) +(deftest meiles-u-only-the-brightest-basic + (doseq [[s sn] [[:hq "HQ"] [:rd "R&D"] ["Archives" :archives]]] + (do-game + (new-game {:corp {:id "Méliès U: Only the Brightest" + :hand ["IPO"] + :deck ["Snare!"] + :discard ["Beanstalk Royalties"]}}) + (take-credits state :corp) + (click-prompt state :corp "R&D") + (run-empty-server state :rd) + (click-prompts state :corp "Yes" "IPO" "Snare!") + (is (is-hand? state :corp ["IPO" "Snare!"]))))) + (deftest mercury-chrome-libertador (do-game (new-game {:corp {:deck [(qty "Hedge Fund" 5)] @@ -5751,6 +5786,16 @@ (click-prompt state :runner "Yes") (is (= 2 (count (:hand (get-runner)))) "Took damage, then drew up"))) +(deftest virtual-intelligence-p-i-you-can-call-me-vic + (doseq [tags [0 1]] + (do-game + (new-game {:runner {:id "Virtual Intelligence, P.I.: \"You Can Call Me Vic\"" :tags tags :deck [(qty "Ika" 15)]}}) + (take-credits state :corp) + (is (changed? [(count (:hand (get-runner))) 1] + (card-ability state :runner (get-in @state [:runner :identity]) 0)) + "Drew 1 card") + (is (= 0 (count-tags state)) "Untagged")))) + (deftest weyland-consortium-because-we-built-it-pay-credits-prompt ;; Pay-credits prompt (do-game diff --git a/test/clj/game/cards/operations_test.clj b/test/clj/game/cards/operations_test.clj index 7097e85f7c..13d8c855ab 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -760,6 +760,20 @@ (click-prompt state :runner "Steal") (is (= 2 (count-tags state)) "Runner took 2 tags from accessing agenda with Casting Call hosted on it")))))) +(deftest caveat-emptor-test + (doseq [[opt gain cs] [["Gain 6 [Credits]. Runner has -1 [Click] next turn" 6 3] + ["Gain 10 [Credits]. Runner has +1 [Click] next turn" 10 5]]] + (do-game + (new-game {:corp {:hand ["Caveat Emptor"] :credits 6}}) + (play-from-hand state :corp "Caveat Emptor") + (is (changed? [(:credit (get-corp)) gain] + (click-prompt state :corp opt)) + "Gained creds") + (take-credits state :corp) + (is (changed? [(:credit (get-runner)) cs] + (take-credits state :runner)) + "Take-credits modified cred gain because the number of clicks was different")))) + (deftest celebrity-gift ;; Ice Wall (do-game @@ -1023,6 +1037,39 @@ (is (= 5 (count (:hand (get-corp)))) "Corp should draw up to 5 cards") (is (= 1 (count (:discard (get-corp)))) "Corp should have 1 card in discard from playing"))) +(deftest cultivate-full-test + (testing "R&D is empty" + (do-game + (new-game {:corp {:hand ["Cultivate"]}}) + (play-from-hand state :corp "Cultivate") + (is (no-prompt? state :corp) "No lingering prompts"))) + (testing "R&D has only one card. It gets trashed." + (do-game + (new-game {:corp {:hand ["Cultivate"] + :deck ["IPO"]}}) + (play-cards state :corp ["Cultivate"]) + (is (no-prompt? state :corp) "No lingering prompts.") + (is-discard? state :corp ["Cultivate" "IPO"]))) + (testing "R&D has two cards in it. One gets trashed, one gets added to HQ" + (do-game + (new-game {:corp {:hand ["Cultivate"] + :deck ["IPO" "Vanilla"]}}) + (play-cards state :corp ["Cultivate" "OK" "IPO" "Vanilla" "OK"]) + (is (no-prompt? state :corp) "No lingering prompts.") + (is-discard? state :corp ["Cultivate" "IPO"]) + (is-hand? state :corp ["Vanilla"]))) + (testing "R&D has 3-5 cards in it. One to trash, one to hand, rest stacked" + (doseq [to-deck [["Beanstalk Royalties"] + ["Beanstalk Royalties" "Ice Wall"] + ["Beanstalk Royalties" "Ice Wall" "Stavka"]]] + (do-game + (new-game {:corp {:hand ["Cultivate"] + :deck (concat ["IPO" "Vanilla"] to-deck)}}) + (play-cards state :corp (concat ["Cultivate" "OK" "IPO" "Vanilla"] (reverse to-deck) ["OK"])) + (is-deck-stacked? state :corp to-deck) + (is (no-prompt? state :corp)) + (is (no-prompt? state :runner)))))) + (deftest cyberdex-trial ;; Cyberdex Trial (do-game @@ -3121,6 +3168,34 @@ (is (= 2 (count-tags state)) "Runner should have two tags from MAD") (is (= 3 (count (:discard (get-corp)))) "MAD + 2 cards in discard"))) +(deftest myoshu + (do-game + (new-game {:corp {:credits 50 + :hand ["Greenmail" "Myōshu"]}}) + (play-from-hand state :corp "Greenmail" "New remote") + (dotimes [_ 2] + (click-advance state :corp (get-content state :remote1 0))) + (take-credits state :corp) + (take-credits state :runner) + (score state :corp (get-content state :remote1 0)) + (is (changed? [(:credit (get-corp)) -10 + (:agenda-point (get-corp)) 2] + (play-from-hand state :corp "Myōshu")) + "Traded 10c for 2 agenda points"))) + +(deftest myoshu-doesnt-work-if-installed-this-turn + (do-game + (new-game {:corp {:credits 50 + :hand ["Greenmail" "Myōshu"]}}) + (core/gain state :corp :click 4) + (play-from-hand state :corp "Greenmail" "New remote") + (dotimes [_ 2] + (click-advance state :corp (get-content state :remote1 0))) + (score state :corp (get-content state :remote1 0)) + (is (changed? [(:credit (get-corp)) 0] + (play-from-hand state :corp "Myōshu")) + "Could not play, installed the greenmail this turn"))) + (deftest nanomanagement ;; Biotic Labor - Gain 2 clicks (do-game @@ -3853,6 +3928,45 @@ (click-prompt state :runner "0") (is (empty? (:hand (get-runner))) "Runner took 3 meat damage"))) +(deftest realloc-test + (do-game + (new-game {:corp {:hand ["realloc()" "Ice Wall" "Enigma"] :credits 10}}) + (play-cards state :corp ["Ice Wall" "HQ" :rezzed] ["Enigma" "R&D" :rezzed]) + (core/gain state :corp :click 1) + (play-from-hand state :corp "realloc()") + (is (changed? [(:credit (get-corp)) 4] + (click-prompts state :corp "Ice Wall" "Enigma")) + "Gained 4 (1 + 3) from derezzing ice wall and enigma"))) + +(deftest animation-protocol-do-nothing + (doseq [opt [:nothing :rez-illicit :rez-other :no-rez]] + (do-game + (new-game {:corp {:discard ["Bulwark" "Chiyashi" "Archer"] + :hand ["Reanimation Protocol"] + :credits 7}}) + (play-from-hand state :corp "Reanimation Protocol") + (case opt + :nothing (is (changed? [(count-bad-pub state) 0] + (click-prompt state :corp "Done")) + "Took no bp") + :rez-illicit (is (changed? [(:credit (get-corp)) (- 10 10) + (count-bad-pub state) 1] + (click-prompts state :corp "Bulwark" "HQ") + (is (= "Bulwark" (:title (get-ice state :hq 0))) "Rezzed it")) + "Installed and rezzed bulwark, only took 1 bad pub") + :no-rez (is (changed? [(:credit (get-corp)) 0 + (count-bad-pub state) 0] + (click-prompts state :corp "Archer" "HQ") + (is (not (rezzed? (get-ice state :hq 0))) "Unrezzed") + (is (= "Archer" (:title (get-ice state :hq 0))) "Did not rez it")) + "Installed Archer, took no bad pub") + :rez-other (is (changed? [(:credit (get-corp)) (- 10 12) + (count-bad-pub state) 1] + (click-prompts state :corp "Chiyashi" "HQ") + (is (= "Chiyashi" (:title (get-ice state :hq 0))) "Installed chiyashi") + (is (rezzed? (get-ice state :hq 0)) "rezzed it")) + "Installed and rezzed Chiyashi, took 1 bad pub"))))) + (deftest red-level-clearance ;; Red Level Clearance (do-game @@ -3970,6 +4084,14 @@ (is (rezzed? (get-content state :remote1 0)) "Marilyn Campaign was rezzed") (is (= 2 (:credit (get-corp))) "Rezzed Marilyn Campaign 2 credit + 1 credit for Restore"))) +(deftest retirement-plan + (do-game + (new-game {:corp {:hand ["Retirement Plan"] + :discard ["PAD Campaign"]}}) + (play-from-hand state :corp "Retirement Plan") + (click-prompts state :corp "PAD Campaign" "New remote") + (is (= "PAD Campaign" (:title (get-content state :remote1 0)))))) + (deftest retribution ;; Retribution (do-game @@ -4213,6 +4335,28 @@ (click-prompt state :corp "Plascrete Carapace") (is (= 2 (count (:hand (get-runner))))))) +(deftest scapegoat-remove-bad-pub + (do-game + (new-game {:corp {:hand ["Scapegoat"] :bad-pub 1}}) + (is (changed? [(count-bad-pub state) -1] + (play-from-hand state :corp "Scapegoat") + (click-prompt state :runner "Corp removes 2 bad publicity")) + "Lost a bad pub"))) + +(deftest scapegoat-shuffle-a-card + (do-game + (new-game {:corp {:hand ["Scapegoat"] :bad-pub 1} + :runner {:hand ["Rezeki" "Ika"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Rezeki") + (play-from-hand state :runner "Ika") + (take-credits state :runner) + (play-from-hand state :corp "Scapegoat") + (click-prompt state :runner "Corp shuffles 1 Runner card into the Stack") + (click-prompts state :corp "Rezeki") + (is (no-prompt? state :corp)) + (is (= 1 (count (:deck (get-runner))))))) + (deftest scapenet (doseq [[title func] [["Misdirection" get-program] ["Clone Chip" get-hardware] @@ -5594,6 +5738,17 @@ (is (= 2 (count (:discard (get-runner)))) "Runner has 2 trashed cards") (is (= 1 (count-bad-pub state)) "Corp takes 1 bad pub"))) +(deftest unleash-test + (do-game + (new-game {:corp {:hand ["Unleash" "Neural Katana"]} + :runner {:hand [(qty "Ika" 5)] :tags 1}}) + (play-from-hand state :corp "Neural Katana" "HQ") + (is (changed? [(count (:hand (get-runner))) -3 + (count-tags state) -1] + (play-from-hand state :corp "Unleash") + (click-prompts state :corp "Neural Katana" "Do 3 net damage")) + "Neural katana was unleashed"))) + (deftest violet-level-clearance ;; Violet Level Clearance (do-game @@ -5622,6 +5777,13 @@ (click-card state :corp "Kati Jones") (is (not (get-resource state 0)) "Kati Jones is trashed"))) +(deftest vulture-fund-test + (do-game + (new-game {:corp {:hand ["Vulture Fund"] :credits 7}}) + (is (changed? [(:credit (get-corp)) 7 + (count-bad-pub state) 1] + (play-from-hand state :corp "Vulture Fund"))))) + (deftest wake-up-call-should-fire-after-using-en-passant-to-trash-ice ;; should fire after using En Passant to trash ice (do-game diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index a8953629ba..443412aa78 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -1003,6 +1003,54 @@ (card-ability state :runner (refresh baba) 2)) "Spent 1c to boost baba yaga")))) +(deftest baker-stealth-hq + (do-game + (new-game {:runner {:hand ["Baker" "Mantle"]} + :corp {:hand [(qty "Rashida Jaheem" 3)] + :deck ["Hostile Takeover"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Baker") + (play-from-hand state :runner "Mantle") + (card-ability state :runner (get-program state 0) 0) + (run-continue-until state :success) + (click-prompt state :runner "Pay 1 [Credits]: Switch to HQ") + (click-card state :runner "Mantle") + (do-trash-prompt state 1) + (run-empty-server state :archives) + (is (no-prompt? state :runner)))) + +(deftest baker-stealth-rd + (do-game + (new-game {:runner {:hand ["Baker" "Mantle"]} + :corp {:hand [(qty "Rashida Jaheem" 3)] + :deck ["Hostile Takeover"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Baker") + (play-from-hand state :runner "Mantle") + (card-ability state :runner (get-program state 0) 0) + (run-continue-until state :success) + (click-prompt state :runner "Pay 1 [Credits]: Switch to R&D") + (click-card state :runner "Mantle") + (click-prompt state :runner "Steal") + (run-empty-server state :archives) + (is (no-prompt? state :runner)))) + +(deftest baker-vs-skunkworks + (do-game + (new-game {:runner {:hand ["Baker" "Mantle"]} + :corp {:hand [(qty "Rashida Jaheem" 3) "Manegarm Skunkworks"] + :deck ["Hostile Takeover"]}}) + (play-cards state :corp ["Manegarm Skunkworks" "HQ" :rezzed]) + (take-credits state :corp) + (play-from-hand state :runner "Baker") + (play-from-hand state :runner "Mantle") + (card-ability state :runner (get-program state 0) 0) + (run-continue-until state :success) + (click-prompt state :runner "Pay 1 [Credits]: Switch to HQ") + (click-card state :runner "Mantle") + (click-prompt state :runner "End the run") + (is (not (:run @state)) "Skunkworks fired on HQ approach"))) + (deftest bankroll ;; Bankroll - Includes check for Issue #4334 (do-game @@ -7418,6 +7466,20 @@ (click-prompt state :runner "End the run")) "Spent 1 credit to break")))) +(deftest read-write-share + (do-game + (new-game {:runner {:hand ["Read-Write Share" "Rezeki" "Corroder"] + :deck [(qty "Ika" 2)]}}) + (take-credits state :corp) + (play-from-hand state :runner "Read-Write Share") + (click-card state :runner "Rezeki") + (take-credits state :runner) + (take-credits state :corp) + (start-turn state :runner) + (click-card state :runner "Corroder") + (card-ability state :runner (get-program state 0) 0) + (is-deck? state :runner ["Corroder" "Rezeki"]))) + (deftest reaver ;; Reaver - Draw a card the first time you trash an installed card each turn (do-game @@ -8007,6 +8069,31 @@ (is (= 2 (get-link state)) "2 link") (is (= 2 (core/available-mu state)) "Shiv stops using MU when 2+ link")))) +(deftest sipa-test + (do-game + (new-game {:corp {:hand ["Vanilla" "Ice Wall"]} + :runner {:hand ["Sipa" "Corroder"] :credits 15}}) + (play-from-hand state :corp "Vanilla" "HQ") + (play-from-hand state :corp "Ice Wall" "HQ") + (rez state :corp (get-ice state :hq 0)) + (rez state :corp (get-ice state :hq 1)) + (take-credits state :corp) + (play-from-hand state :runner "Sipa") + (play-from-hand state :runner "Corroder") + (run-on state :hq) + (run-continue-until state :encounter-ice) + (auto-pump-and-break state (get-program state 1)) + (run-continue state) + (click-card state :runner "Vanilla") + (is (= "Ice Wall" (:title (get-ice state :hq 0))) "Ice wall outer") + (is (= "Vanilla" (:title (get-ice state :hq 1))) "Ice wall outer") + (run-continue-until state :success) + (run-on state :hq) + (run-continue-until state :encounter-ice) + (auto-pump-and-break state (get-program state 1)) + (run-continue-until state :movement) + (is (no-prompt? state :runner)))) + (deftest slap-vandal (do-game (new-game {:runner {:hand ["Slap Vandal"]} @@ -8468,6 +8555,19 @@ (is (= "Troll" (-> (get-corp) :discard first :title)) "Troll was trashed") (is (= "Herald" (-> (get-corp) :deck first :title)) "Herald now on top of R&D")))) +(deftest stowaway-test + (do-game + (new-game {:runner {:hand ["Stowaway"]} + :corp {:hand ["Ice Wall"]}}) + (play-from-hand state :corp "Ice Wall" "HQ") + (take-credits state :corp) + (play-from-hand state :runner "Stowaway") + (click-card state :runner "Ice Wall") + (run-on state :hq) + (is (changed? [(:credit (get-runner)) 2] + (run-continue-until state :success)) + "Gained 2c for a successful run on stowaway server"))) + (deftest study-guide ;; Study Guide - 2c to add a power counter; +1 strength per counter (do-game diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 4916f90f78..23d4a148cf 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -3112,6 +3112,49 @@ (click-prompt state :runner "Trash Guru Davinder") (is (no-prompt? state :runner) "Dummy Box not prompting to prevent trash"))) +(deftest hackerspace-test + (do-game + (new-game {:runner {:hand ["Hackerspace" "Kati Jones" "Underworld Contact" "Paladin Poemu"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Hackerspace") + (play-from-hand state :runner "Kati Jones") + (click-prompt state :runner "Hackerspace") + (is (= 5 (core/hand-size state :runner)) "Runner should start with 5 max hand size") + (play-from-hand state :runner "Paladin Poemu") + (click-prompt state :runner "Hackerspace") + (is (= 7 (core/hand-size state :runner)) "Runner should start with 5 max hand size"))) + +(deftest hackerspace-with-cost-discount-requirement + (do-game + (new-game {:runner {:hand ["Career Fair" "Hackerspace" "The Class Act"] + :credits 2}}) + (take-credits state :corp) + (play-from-hand state :runner "Hackerspace") + (play-from-hand state :runner "Career Fair") + (click-card state :runner "The Class Act") + (click-prompt state :runner "Hackerspace") + (is (no-prompt? state :runner) "It worked") + (is (= "The Class Act" (-> (get-resource state 0) :hosted first :title)) "Installed for 0"))) + +(deftest hackerspace-class-act-repl + (do-game + (new-game {:runner {:hand ["Hackerspace" "The Class Act" "The Class Act"] + :deck ["Sure Gamble" "Easy Mark" "The Class Act" "Euler" "Ika" "Dirty Laundry" "Corroder" "Carpe Diem"] + :credits 15} + :corp {:hand ["IPO"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Hackerspace") + (play-from-hand state :runner "The Class Act") + (click-prompt state :runner "Hackerspace") + (take-credits state :runner) + (click-card state :runner (last (:set-aside (get-runner)))) + (is (no-prompt? state :runner) "No prompt") + (take-credits state :corp) + (play-from-hand state :runner "The Class Act") + (click-prompt state :runner "Hackerspace") + (take-credits state :runner) + (is (not (no-prompt? state :runner)) "TCA Prompt"))) + (deftest hannah-wheels-pilintra-basic-test (do-game (new-game {:runner {:hand ["Hannah \"Wheels\" Pilintra"]} @@ -4319,6 +4362,45 @@ (is (= 2 (count (:hand (get-runner)))) "Darwin never got played, Chameleon returned to hand") (is (= 2 (count (:discard (get-runner)))) "Femme Fatale and Study Guide trashed")))) +(deftest man-in-the-middle-negative-points-mode + (do-game + (new-game {:corp {:hand ["Hostile Takeover" (qty "Archer" 2)] + :credits 30} + :runner {:hand ["Word on the Street"]}}) + (play-from-hand state :corp "Archer" "Archives") + (play-from-hand state :corp "Archer" "Archives") + (take-credits state :corp) + (play-from-hand state :runner "Word on the Street") + (take-credits state :runner) + (is (changed? [(count (:scored (get-corp))) 2 + (:credit (get-runner)) 0] + (play-and-score state "Hostile Takeover")) + "2 agendas sitting there") + (is (= 0 (:agenda-point (get-corp))) "Scored Bellona for 3 points") + (rez state :corp (get-ice state :archives 0) {:expect-rez false}) + (is (= "Word on the Street" (:printed-title (get-scored state :corp 1)))) + (click-card state :corp (get-scored state :corp 1)) + (is (not (no-prompt? state :corp)) "Cannot do man in the middle") + (click-card state :corp (get-scored state :corp 0)) + (is (rezzed? (get-ice state :archives 0)) "Archer now rezzed") + (rez state :corp (get-ice state :archives 1) {:expect-rez false}) + (is (no-prompt? state :corp) "No prompt to rez"))) + +(deftest man-in-the-middle-regular-points-mode + (do-game + (new-game {:corp {:hand ["Hostile Takeover"]} + :runner {:hand ["Word on the Street"]}}) + (play-from-hand state :corp "Hostile Takeover" "New remote") + (take-credits state :corp) + (play-from-hand state :runner "Word on the Street") + (take-credits state :runner) + (is (changed? [(count (:scored (get-corp))) 1 + (count (:discard (get-runner))) 1 + (:credit (get-runner)) 4] + (score-agenda state :corp (get-content state :remote1 0))) + "Other thing happened") + (is (no-prompt? state :corp)))) + (deftest manuel-lattes-de-moura (do-game (new-game {:corp {:hand [(qty "Hedge Fund" 5)] @@ -4875,6 +4957,18 @@ (do (click-prompt state :runner "Pass priority") (is (<= 3 (count (:discard (get-runner)))) "Took 3 damage")))))) +(deftest nurse-hanh-test + (do-game + (new-game {:runner {:hand ["Nurse Hạnh"] + :deck [(qty "Nurse Hạnh" 15)]} + :corp {:discard [(qty "IPO" 6)]}}) + (take-credits state :corp) + (play-from-hand state :runner "Nurse Hạnh") + (is (changed? [(count (:hand (get-runner))) 2] + (run-empty-server state :archives))) + (is (changed? [(count (:hand (get-runner))) 0] + (run-empty-server state :archives))))) + (deftest off-campus-apartment-ability-shows-a-simultaneous-resolution-prompt-when-appropriate ;; ability shows a simultaneous resolution prompt when appropriate (do-game @@ -6184,6 +6278,25 @@ (is (no-prompt? state :runner) "Runner has no Friday Chip prompt")) "Friday Chip shouldn't gain counters from Spoilers"))) +(deftest stick-and-poke + (do-game + (new-game {:corp {:hand ["Vanilla"]} + :runner {:hand ["Stick and Poke" "Stick and Poke"] :deck ["Ika"]}}) + (play-from-hand state :corp "Vanilla" "HQ") + (rez state :corp (get-ice state :hq 0)) + (take-credits state :corp) + (is (changed? [(count (:subroutines (get-ice state :hq 0))) 0] + (play-from-hand state :runner "Stick and Poke") + (core/fake-checkpoint state)) + "Gained no sub") + (run-on state :hq) + (is (changed? [(count (:subroutines (get-ice state :hq 0))) 1] + (run-continue-until state :encounter-ice)) + "gained sub") + (card-subroutine state :corp (get-ice state :hq 0) 0) + (is-discard? state :runner ["Stick and Poke"]) + (is-hand? state :runner ["Ika"]))) + (deftest stim-dealer ;; Stim Dealer - Take 1 brain damage when it accumulates 2 power counters (do-game diff --git a/test/clj/game/cards/upgrades_test.clj b/test/clj/game/cards/upgrades_test.clj index 40dc560924..379c6d579f 100644 --- a/test/clj/game/cards/upgrades_test.clj +++ b/test/clj/game/cards/upgrades_test.clj @@ -1589,6 +1589,57 @@ (take-credits state :runner) (is (= (+ 3 total-corp-credits) (:credit (get-corp))) "Corp does not gain any extra c with agenda"))))) +(letfn [(setup-state [] + (let [state (new-game {:runner {:hand ["Cupellation" "HQ Interface"] + :credits 10} + :corp {:hand ["Flagship" "Research Station" "Hedge Fund" "Hedge Fund"] + :credits 10}})] + (play-from-hand state :corp "Flagship" "HQ") + (play-from-hand state :corp "Research Station" "HQ") + (rez state :corp (get-content state :hq 0)) + (rez state :corp (get-content state :hq 1)) + (take-credits state :corp) + (play-from-hand state :runner "HQ Interface") + state))] + ;; access both upgrades, cannot access anything else + (deftest flagship-normal-case-two-upgrades + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompts state :runner "Research Station" "No action" "No action") + (is (no-prompt? state :runner))) + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompts state :runner "Flagship" "No action" "Research Station" "No action") + (is (no-prompt? state :runner)))) + ;; upgrade and a card from hand + (deftest flagship-normal-case-one-upgrade + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompts state :runner "Card from hand" "No action" "No action") + (is (no-prompt? state :runner))) + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompts state :runner "Flagship" "No action" "Card from hand" "No action") + (is (no-prompt? state :runner)))) + (deftest flagship-normal-case-trash-station + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompt state :runner "Flagship") + (do-trash-prompt state 4) + (click-prompts state :runner "Card from hand" "No action") + (is (no-prompt? state :runner))) + (do-game + (setup-state) + (run-empty-server state :hq) + (click-prompts state :runner "Card from hand" "No action") + (do-trash-prompt state 4) + (is (no-prompt? state :runner))))) + (deftest forced-connection ;; Forced Connection - ambush, trace(3) give the runner 2 tags (do-game @@ -1803,7 +1854,7 @@ (deftest giordano-memorial-field ;; Giordano Memorial Field (do-game - (new-game {:corp {:deck ["Giordano Memorial Field" "Hostile Takeover"]} + (new-game {:corp {:deck ["Giordano Memorial Field" "Greenmail"]} :runner {:deck [(qty "Fan Site" 3)]}}) (play-from-hand state :corp "Giordano Memorial Field" "New remote") (rez state :corp (get-content state :remote1 0)) @@ -1812,7 +1863,7 @@ (play-from-hand state :runner "Fan Site") (play-from-hand state :runner "Fan Site") (take-credits state :runner) - (play-and-score state "Hostile Takeover") + (play-and-score state "Greenmail") (take-credits state :corp) (run-empty-server state "Server 1") (let [credits (:credit (get-runner))] @@ -1827,8 +1878,9 @@ (deftest giordano-memorial-field-payable-with-net-mercur ;; Payable with net mercur (do-game - (new-game {:corp {:deck ["Giordano Memorial Field" "Hostile Takeover"]} - :runner {:deck [(qty "Fan Site" 3) "Net Mercur"]}}) + (new-game {:corp {:deck ["Giordano Memorial Field" "Greenmail"]} + :runner {:deck [(qty "Fan Site" 3) "Net Mercur"] + :credits 6}}) (play-from-hand state :corp "Giordano Memorial Field" "New remote") (rez state :corp (get-content state :remote1 0)) (take-credits state :corp) @@ -1837,7 +1889,7 @@ (play-from-hand state :runner "Fan Site") (play-from-hand state :runner "Net Mercur") (take-credits state :runner) - (play-and-score state "Hostile Takeover") + (play-and-score state "Greenmail") (take-credits state :corp) (let [nm (get-resource state 0)] (core/command-counter state :runner '("c" "3")) @@ -2026,6 +2078,18 @@ (run-empty-server state :hq) (is (= 1 (count (:discard (get-runner)))) "1 net damage done for successful run on HQ"))) +(deftest hype-machine-test + (do-game + (new-game {:corp {:hand ["Hype Machine" (qty "Hostile Takeover" 2)]}}) + (play-from-hand state :corp "Hype Machine" "New remote") + (play-and-score state "Hostile Takeover") + (is (changed? [(:credit (get-corp)) 0] + (rez state :corp (get-content state :remote1 0)))) + (play-from-hand state :corp "Hostile Takeover" "Server 1") + (card-ability state :corp (get-content state :remote1 0) 0) + (click-card state :corp (get-content state :remote1 1)) + (is (= 1 (get-counters (get-content state :remote1 0) :advancement)) "1 adv"))) + (deftest increased-drop-rates ;; Increased Drop Rates (do-game @@ -2039,6 +2103,7 @@ (click-prompt state :runner "Take 1 tag") (is (= 1 (count-tags state)) "Runner takes 1 tag to prevent Corp from removing 1 BP") (click-prompt state :runner "Pay 2 [Credits] to trash") ; trash + (select-bad-pub state 1) (run-empty-server state "Archives") (is (= 1 (count-bad-pub state))) (click-prompt state :runner "The Corp removes 1 bad publicity") @@ -3686,6 +3751,23 @@ (is (find-card "Enigma" (:hand (get-corp)))) (is (zero? (count (:deck (get-corp)))))))) +(deftest perfect-recall-test + (do-game + (new-game {:corp {:hand ["Perfect Recall" "Merger" "Merger"] :credits 10}}) + (play-from-hand state :corp "Perfect Recall" "New remote") + (rez state :corp (get-content state :remote1 0)) + (play-from-hand state :corp "Merger" "Server 1") + (core/gain state :corp :click 5) + (dotimes [_ 3] + (click-advance state :corp (get-content state :remote1 1))) + (score state :corp (get-content state :remote1 1)) + (take-credits state :corp) + (run-on state :hq) + (card-ability state :corp (get-content state :remote1 0) 0) + (click-card state :corp (first (:hand (get-corp)))) + (run-continue-until state :success) + (is (= ["No action"] (prompt-titles :runner))))) + (deftest port-anson-grid ;; Port Anson Grid - Prevent the Runner from jacking out until they trash a program (do-game @@ -4010,6 +4092,37 @@ (is (= 3 (-> (get-runner) :discard count)) "Runner should take 3 net damage from losing Self-destruct trace") (is (not (:run @state)) "Run has ended because the server disappeared")))) +(deftest shackleton-grid-test-stealth-cred + (do-game + (new-game {:corp {:hand ["Shackleton Grid"] :bad-pub 1} + :runner {:hand ["Mantle" "Self-modifying Code" (qty "Easy Mark" 5)]}}) + (play-from-hand state :corp "Shackleton Grid" "New remote") + (rez state :corp (get-content state :remote1 0)) + (take-credits state :corp) + (play-from-hand state :runner "Mantle") + (play-from-hand state :runner "Self-modifying Code") + (run-on state :remote1) + (is (changed? [(count (:hand (get-runner))) -4] + (card-ability state :runner (get-program state 1) 0) + (click-prompts state :runner "Mantle" "Done") + (click-prompt state :corp "Yes")) + "Got slammed for 4 damage"))) + +(deftest shackleton-grid-test-bad-publicity-cred + (do-game + (new-game {:corp {:hand ["Shackleton Grid"] :bad-pub 1} + :runner {:hand ["Self-modifying Code" (qty "Easy Mark" 5)]}}) + (play-from-hand state :corp "Shackleton Grid" "New remote") + (rez state :corp (get-content state :remote1 0)) + (take-credits state :corp) + (play-from-hand state :runner "Self-modifying Code") + (run-on state :remote1) + (is (changed? [(count (:hand (get-runner))) -4] + (card-ability state :runner (get-program state 0) 0) + (select-bad-pub state 0) + (click-prompt state :corp "Yes")) + "Got slammed for 4 damage"))) + (deftest shell-corporation (do-game (new-game {:corp {:hand ["Shell Corporation"]}}) @@ -4227,6 +4340,18 @@ (click-card state :corp van)) "Corp placed 2 advancement counters on Vanilla"))))) +(deftest the-red-room-test + (do-game + (new-game {:corp {:hand ["The Red Room" "Hostile Takeover"]}}) + (play-from-hand state :corp "The Red Room" "R&D") + (rez state :corp (get-content state :rd 0)) + (play-and-score state "Hostile Takeover") + (click-prompt state :corp "Hostile Takeover") + (take-credits state :corp) + (run-on state :hq) + (card-ability state :corp (get-content state :rd 0) 0) + (is (not (:run @state)) "Ended the run"))) + (deftest the-twins ;; The Twins (do-game diff --git a/test/clj/game/core/rules_test.clj b/test/clj/game/core/rules_test.clj index a4dbf48b49..b97aab617a 100644 --- a/test/clj/game/core/rules_test.clj +++ b/test/clj/game/core/rules_test.clj @@ -378,14 +378,17 @@ (run-empty-server state :remote1) (click-prompt state :corp "No") (click-prompt state :runner "Pay 1 [Credits] to trash") + (select-bad-pub state 1) (is (= 5 (:credit (get-runner))) "1 BP credit spent to trash CVS") (run-empty-server state :hq) (click-prompt state :corp "No") (click-prompt state :runner "Pay 1 [Credits] to trash") + (select-bad-pub state 1) (is (= 5 (:credit (get-runner))) "1 BP credit spent to trash CVS") (run-empty-server state :rd) (click-prompt state :corp "No") (click-prompt state :runner "Pay 1 [Credits] to trash") + (select-bad-pub state 1) (is (= 5 (:credit (get-runner))) "1 BP credit spent to trash CVS"))) (deftest run-psi-bad-publicity-credits diff --git a/test/clj/game/core/runs_test.clj b/test/clj/game/core/runs_test.clj index 455c90c7fe..810c971321 100644 --- a/test/clj/game/core/runs_test.clj +++ b/test/clj/game/core/runs_test.clj @@ -617,18 +617,13 @@ ;; Correct handling of multi accesses with shuffle in between accesses (testing "Shuffle from Bacterial Programming" (do-game - (new-game {:corp {:hand ["Advanced Assembly Lines" "Bacterial Programming" "Chiyashi" + (new-game {:corp {:deck ["Advanced Assembly Lines" "Bacterial Programming" "Chiyashi" "DNA Tracker" "Excalibur" "Fire Wall" "Gold Farmer" "Hostile Infrastructure"] - :deck []}}) - (core/move state :corp (find-card "Advanced Assembly Lines" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Bacterial Programming" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Chiyashi" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "DNA Tracker" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Excalibur" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Fire Wall" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Gold Farmer" (:hand (get-corp))) :deck) - (core/move state :corp (find-card "Hostile Infrastructure" (:hand (get-corp))) :deck) + :hand ["ZATO City Grid"]}}) + (stack-deck state :corp ["Advanced Assembly Lines" "Bacterial Programming" "Chiyashi" + "DNA Tracker" "Excalibur" "Fire Wall" "Gold Farmer" + "Hostile Infrastructure"]) ; Deck is now ABCDEFGH from top to bottom (take-credits state :corp) (run-on state :rd) @@ -638,9 +633,7 @@ (click-prompt state :runner "No action") (is (accessing state "Bacterial Programming") "Accessed B") (click-prompt state :runner "Steal") - (click-prompt state :corp "Yes") - (click-prompt state :corp "Done") - (click-prompt state :corp "Done") + (click-prompts state :corp "Yes" "OK" "Done" "Done") (click-prompt state :corp "Hostile Infrastructure") (click-prompt state :corp "Gold Farmer") (click-prompt state :corp "Fire Wall") @@ -648,7 +641,7 @@ (click-prompt state :corp "DNA Tracker") (click-prompt state :corp "Chiyashi") (click-prompt state :corp "Advanced Assembly Lines") - (click-prompt state :corp "Done") + (click-prompt state :corp "OK") (is (accessing state "Advanced Assembly Lines") "Accessed A again") (click-prompt state :runner "No action") (is (accessing state "Chiyashi") "Accessed C") diff --git a/test/clj/game/test_framework.clj b/test/clj/game/test_framework.clj index 08c626c6a8..fb9508133d 100644 --- a/test/clj/game/test_framework.clj +++ b/test/clj/game/test_framework.clj @@ -47,7 +47,7 @@ (load-all-cards) (defn is-zone-impl - "Is the hand exactly equal to a given set of cards?" + "Is the zone exactly equal to a given set of cards?" [state side zone expected] (let [expected (seq (sort (flatten expected))) contents (seq (sort (map :title (get-in @state [side zone]))))] @@ -59,12 +59,12 @@ `(error-wrapper (is-zone-impl ~state ~side :hand ~expected-hand))) (defmacro is-deck? - "Is the hand exactly equal to a given set of cards?" + "Is the deck exactly equal to a given set of cards?" [state side expected-deck] `(error-wrapper (is-zone-impl ~state ~side :deck ~expected-deck))) (defmacro is-discard? - "Is the hand exactly equal to a given set of cards?" + "Is the discard pile exactly equal to a given set of cards?" [state side expected-discard] `(error-wrapper (is-zone-impl ~state ~side :discard ~expected-discard))) @@ -230,6 +230,10 @@ [state cost] (click-prompt state :runner (str "Pay " cost " [Credits] to trash"))) +(defn select-bad-pub + [state expected] + (core/process-action "bad-pub-choice" state :runner {:eid (:eid (get-prompt state :runner))})) + ;; General utilities necessary for starting a new game (defn find-card "Copied from core so we can check printed title too" @@ -1159,7 +1163,7 @@ (defn run-and-encounter-ice-test ([card] (run-and-encounter-ice-test card nil)) ([card players] (run-and-encounter-ice-test card players nil)) - ([card players {:keys [counters disable rig server threat] :as args}] + ([card players {:keys [counters disable rig server threat run-event] :as args}] (let [;; sometimes the number of cards are the only important things - this lets us do :hand X ;; or :deck X on either side (so we can reduce noise when reading tests) players (update-zones players [[[:corp :hand] "IPO"] @@ -1168,6 +1172,14 @@ [[:runner :deck] "Inti"]]) players (update-in players [:corp :hand] conj card) players (update-in players [:runner :hand] concat rig) + run-event-card (if run-event + (if (string? run-event) + run-event + (first run-event)) + nil) + players (if run-event-card + (update-in players [:runner :hand] conj run-event-card) + players) state (new-game players) server (or server "HQ") server-key (cond @@ -1208,7 +1220,9 @@ (core/gain state :runner :click 1) (play-from-hand state :runner r))) ;; runner should have the default click/credit count (- 1 click for the run) - (run-on state server-key) + (if run-event + (play-cards state :runner run-event) + (run-on state server-key)) (run-continue-until state :encounter-ice) state))))