Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

be smart with skorp automation #7945

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/clj/game/cards/hardware.clj
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,16 @@
:req (req (and (grip-or-stack-trash? targets)
(first-trash? state grip-or-stack-trash?)))
:prompt "Choose 1 trashed card to add to the bottom of the stack"
:choices (req (conj (sort (map :title (map :card targets))) "No action"))
:choices (req (conj (sort (keep #(->> (:moved-card %) :title) targets)) "No action"))
:async true
:effect (req (if (= "No action" target)
(effect-completed state side eid)
(do (system-msg state side
(str "uses " (:title card) " to add " target
" to the bottom of the stack"))
(move state side (find-card target (:discard (:runner @state))) :deck)
;; note - need to search in reverse order, to remove the NEWEST copy of the card
;; this is for interactions with the price, etc
(move state side (find-card target (reverse (:discard (:runner @state)))) :deck)
(effect-completed state side eid))))}]
{:events [(assoc triggered-ability :event :runner-trash)
(assoc triggered-ability :event :corp-trash)]
Expand Down
76 changes: 65 additions & 11 deletions src/clj/game/cards/identities.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
[game.core.card :refer [agenda? asset? can-be-advanced?
corp-installable-type? corp? faceup? get-advancement-requirement
get-agenda-points get-card get-counters get-title get-zone hardware? has-subtype?
has-any-subtype? ice? in-discard? in-hand? in-play-area? in-rfg? installed? is-type?
has-any-subtype? ice? in-discard? in-deck? in-hand? in-play-area? in-rfg? installed? is-type?
operation? program? resource? rezzed? runner? upgrade?]]
[game.core.charge :refer [charge-ability]]
[game.core.cost-fns :refer [install-cost play-cost
rez-additional-cost-bonus rez-cost]]
[game.core.damage :refer [chosen-damage corp-can-choose-damage? damage
enable-corp-damage-choice]]
[game.core.def-helpers :refer [corp-recur defcard offer-jack-out with-revealed-hand]]
[game.core.def-helpers :refer [choose-one-helper corp-recur defcard offer-jack-out with-revealed-hand]]
[game.core.drawing :refer [draw]]
[game.core.effects :refer [register-lingering-effect is-disabled?]]
[game.core.eid :refer [effect-completed get-ability-targets is-basic-advance-action? make-eid]]
[game.core.engine :refer [not-used-once? pay register-events register-once resolve-ability trigger-event]]
[game.core.events :refer [event-count first-event?
[game.core.events :refer [event-count first-event? first-trash?
first-successful-run-on-server? no-event? not-last-turn? run-events run-event-count turn-events]]
[game.core.expose :refer [expose]]
[game.core.finding :refer [find-latest]]
Expand Down Expand Up @@ -1913,14 +1913,68 @@
:effect (effect (expose eid target))}]})

(defcard "Skorpios Defense Systems: Persuasive Power"
{:implementation "Manually triggered, no restriction on which cards in Heap can be targeted. Cannot use on in progress run event"
:abilities [{:label "Remove a card in the Heap that was just trashed from the game"
:waiting-prompt true
:prompt "Choose a card in the Heap that was just trashed"
:once :per-turn
:choices (req (cancellable (:discard runner)))
:msg (msg "remove " (:title target) " from the game")
:effect (req (move state :runner target :rfg))}]})
(let [set-resolution-mode
(fn [x] {:label x
:effect (req (update! state side (assoc-in card [:special :resolution-mode] x))
(toast state :corp (str "Set Skorpios resolution to " x " mode"))
(update! state side (assoc (get-card state card) :card-target x)))})
grip-or-stack-trash?
(fn [ctx]
(some #(and (runner? (:card %))
(or (in-hand? (:card %))
(in-deck? (:card %))))
ctx))
relevant-cards-general #{"Labor Rights" "The Price"}
relevant-cards-trashed #{"I've Had Worse" "Strike Fund" "Steelskin Scarring"}
trigger-ability-req (req (let [res-type (get-in (get-card state card) [:special :resolution-mode])
valid-cards (mapv #(get-card state %) (filter runner? context))]
(and (some runner? context)
(cond
;; manual: do nothing
(= res-type "Automatic") true
;; there's either:
(= res-type "Smart")
(or
;; 1) a relevant card resoluton
(contains? relevant-cards-general (->> runner :play-area first :title))
;; 2) a buffer drive that may resolve
(and (some #(= (:title %) "Buffer Drive") (all-installed state :runner))
(grip-or-stack-trash? (map (fn [x] {:card x}) context))
(zero? (+ (event-count state nil :runner-trash grip-or-stack-trash?)
(event-count state nil :corp-trash grip-or-stack-trash?)
(event-count state nil :game-trash grip-or-stack-trash?))))
;; 3) a program among the trashed cards
(some #(->> % program?) context)
;; 4) a relevant card is trashed (Steelskin, Strike fund, I've Had Worse)
(some #(contains? relevant-cards-trashed %) (map #(->> % :title) context)))
:else nil))))
triggered-ability {:once :per-turn
:player :corp
:event :pre-trash-interrupt
:waiting-prompt true
:req trigger-ability-req
:prompt "Remove a card from the game?"
:choices (req (cancellable context))
:msg (msg "remove " (:title target) " from the game")
:async true
:effect (req (move state :runner target :rfg)
(effect-completed state side eid))}]
{:implementation "Switch between Manual, \"Smart\", and Automatic resolution by using the ability on the card"
:events [(assoc (set-resolution-mode "Smart")
:event :pre-first-turn
:req (req (= side :corp)))
triggered-ability]
:abilities [(choose-one-helper
{:optional true
:label "Set resolution mode"}
(mapv (fn [x] {:option x :ability (set-resolution-mode x)}) ["Manual" "Smart" "Automatic"]))
{:label "Remove a card in the Heap that was just trashed from the game"
:waiting-prompt true
:prompt "Choose a card in the Heap that was just trashed"
:once :per-turn
:choices (req (cancellable (:discard runner)))
:msg (msg "remove " (:title target) " from the game")
:effect (req (move state :runner target :rfg))}]}))

(defcard "Spark Agency: Worldswide Reach"
{:events [{:event :rez
Expand Down
96 changes: 51 additions & 45 deletions src/clj/game/core/moving.clj
Original file line number Diff line number Diff line change
Expand Up @@ -402,51 +402,57 @@
([state side eid cards {:keys [accessed cause cause-card keep-server-alive game-trash suppress-checkpoint] :as args}]
(if (empty? (filter identity cards))
(effect-completed state side eid)
(wait-for (prevent-trash state side (make-eid state eid) cards args)
(let [trashlist async-result
_ (update-current-ice-to-trash state trashlist)
trash-event (get-trash-event side game-trash)
;; No card should end up in the opponent's discard pile, so instead
;; of using `side`, we use the card's `:side`.
move-card (fn [card]
(move state (to-keyword (:side card)) card :discard {:keep-server-alive keep-server-alive}))
;; If the trashed card is installed, update all of the indicies
;; of the other installed cards in the same location
update-indicies (fn [card]
(when (installed? card)
(update-installed-card-indices state side (:zone card))))
;; Perform the move of the cards from their current location to
;; the discard. At the same time, gather their `:trash-effect`s
;; to be used in the simult event later.
moved-cards (reduce
(fn [acc card]
(if-let [card (get-card? state card)]
(let [_ (set-duration-on-trash-events state card trash-event)
moved-card (move-card card)
trash-effect (get-trash-effect state side eid card args)]
(update-indicies card)
(conj acc [moved-card trash-effect]))
acc))
[]
trashlist)]
(swap! state update-in [:trash :trash-list] dissoc eid)
(when (and side (seq (remove #{side} (map #(to-keyword (:side %)) trashlist))))
(swap! state assoc-in [side :register :trashed-card] true))
;; Pseudo-shuffle archives. Keeps seen cards in play order and shuffles unseen cards.
(swap! state assoc-in [:corp :discard]
(vec (sort-by #(if (:seen %) -1 1) (get-in @state [:corp :discard]))))
(let [eid (make-result eid (mapv first moved-cards))]
(doseq [[card trash-effect] moved-cards
:when trash-effect]
(register-pending-event state trash-event card trash-effect))
(doseq [trashed-card trashlist]
(queue-event state trash-event {:card trashed-card
:cause cause
:cause-card (trim-cause-card cause-card)
:accessed accessed}))
(if suppress-checkpoint
(effect-completed state nil eid)
(checkpoint state nil eid {:duration trash-event}))))))))
(wait-for
(prevent-trash state side (make-eid state eid) cards args)
(let [trashlist async-result
_ (update-current-ice-to-trash state trashlist)]
(wait-for
(trigger-event-sync state side :pre-trash-interrupt trashlist)
(let [trash-event (get-trash-event side game-trash)
;; No card should end up in the opponent's discard pile, so instead
;; of using `side`, we use the card's `:side`.
move-card (fn [card]
(move state (to-keyword (:side card)) card :discard {:keep-server-alive keep-server-alive}))
;; If the trashed card is installed, update all of the indicies
;; of the other installed cards in the same location
update-indicies (fn [card]
(when (installed? card)
(update-installed-card-indices state side (:zone card))))
;; Perform the move of the cards from their current location to
;; the discard. At the same time, gather their `:trash-effect`s
;; to be used in the simult event later.
moved-cards (reduce
(fn [acc card]
(if-let [card (get-card? state card)]
(let [_ (set-duration-on-trash-events state card trash-event)
moved-card (move-card card)
trash-effect (get-trash-effect state side eid card args)]
(update-indicies card)
(conj acc {:moved-card moved-card
:trash-effect trash-effect
:old-card card}))
(conj acc {:old-card card})))
[]
trashlist)]
(swap! state update-in [:trash :trash-list] dissoc eid)
(when (and side (seq (remove #{side} (map #(to-keyword (:side %)) trashlist))))
(swap! state assoc-in [side :register :trashed-card] true))
;; Pseudo-shuffle archives. Keeps seen cards in play order and shuffles unseen cards.
(swap! state assoc-in [:corp :discard]
(vec (sort-by #(if (:seen %) -1 1) (get-in @state [:corp :discard]))))
(let [eid (make-result eid (vec (keep :moved-card moved-cards)))]
(doseq [{:keys [moved-card trash-effect]} moved-cards
:when trash-effect]
(register-pending-event state trash-event moved-card trash-effect))
(doseq [{:keys [old-card moved-card]} moved-cards]
(queue-event state trash-event {:card old-card
:moved-card moved-card
:cause cause
:cause-card (trim-cause-card cause-card)
:accessed accessed}))
(if suppress-checkpoint
(effect-completed state nil eid)
(checkpoint state nil eid {:duration trash-event}))))))))))

(defmethod engine/move* :trash-cards [state side eid _action cards args]
(trash-cards state side eid cards args))
Expand Down
2 changes: 1 addition & 1 deletion test/clj/game/cards/events_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@
:runner {:deck [(qty "By Any Means" 2)]}})
(take-credits state :corp)
(play-from-hand state :runner "By Any Means")
(card-ability state :corp (get-in @state [:corp :identity]) 0)
(card-ability state :corp (get-in @state [:corp :identity]) 1)
(click-prompt state :corp (find-card "By Any Means" (:discard (get-runner))))
(is (= 1 (count (get-in @state [:runner :rfg]))) "By Any Means RFGed")
(is (zero? (count (:discard (get-corp)))) "Nothing trashed yet")
Expand Down
19 changes: 19 additions & 0 deletions test/clj/game/cards/hardware_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,25 @@
(is (find-card "I've Had Worse" (:deck (get-runner))))
(is (find-card "Buffer Drive" (get-hardware state)))))))

(deftest buffer-drive-dont-offer-duplicate-option-with-the-price
(do-game
(new-game {:runner {:discard ["Boomerang"] :deck ["Ika" "Boomerang" "Sure Gamble"] :hand ["The Price" "Buffer Drive"]}})
(take-credits state :corp)
(play-from-hand state :runner "Buffer Drive")
(play-from-hand state :runner "The Price")
(click-prompt state :runner "Boomerang")
(is (not-any? #{"Boomerang"} (prompt-buttons :runner)) "No offer to install boomerang")))

(deftest buffer-drive-plays-nice-with-skorpios
(do-game
(new-game {:corp {:id "Skorpios Defense Systems: Persuasive Power"}
:runner {:deck ["Ika" "Boomerang" "Sure Gamble"] :hand ["The Price" "Buffer Drive"]}})
(take-credits state :corp)
(play-from-hand state :runner "Buffer Drive")
(play-from-hand state :runner "The Price")
(click-prompt state :corp "Boomerang")
(is (not-any? #{"Boomerang"} (prompt-buttons :runner)) "No offer to install boomerang")))

(deftest capstone
;; Capstone
(do-game
Expand Down
19 changes: 15 additions & 4 deletions test/clj/game/cards/identities_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4724,7 +4724,7 @@
(is (= 12 (:credit (get-runner))) "Gained 4cr")
(is (= 12 (get-counters (get-resource state 0) :credit)) "12 cr on Temujin")))

(deftest skorpios-defense-systems-persuasive-power
(deftest skorpios-defense-systems-persuasive-power-manual-usage
; Remove a card from game when it moves to discard once per round
(do-game
(new-game {:corp {:id "Skorpios Defense Systems: Persuasive Power"
Expand All @@ -4733,11 +4733,13 @@
(play-from-hand state :corp "Hedge Fund")
(dotimes [_ 4] (core/move state :corp (first (:hand (get-corp))) :deck))
(take-credits state :corp)
(card-ability state :corp (get-in @state [:corp :identity]) 0)
(click-prompt state :corp "Manual")
(play-from-hand state :runner "Lucky Find")
(play-from-hand state :runner "The Maker's Eye")
(is (= [:rd] (:server (get-run))))
; Don't allow a run-event in progress to be targeted #2963
(card-ability state :corp (get-in @state [:corp :identity]) 0)
(card-ability state :corp (get-in @state [:corp :identity]) 1)
(is (empty? (filter #(= "The Maker's Eye" (:title %)) (-> (get-corp) :prompt first :choices))) "No Maker's Eye choice")
(click-prompt state :corp "Cancel")
(run-continue state)
Expand All @@ -4748,12 +4750,21 @@
(is (accessing state "Quandary"))
(click-prompt state :runner "No action")
(is (not (:run @state)))
(card-ability state :corp (get-in @state [:corp :identity]) 0)
(card-ability state :corp (get-in @state [:corp :identity]) 1)
(click-prompt state :corp (find-card "The Maker's Eye" (:discard (get-runner))))
(is (= 1 (count (get-in @state [:runner :rfg]))) "One card RFGed")
(card-ability state :corp (get-in @state [:corp :identity]) 0)
(card-ability state :corp (get-in @state [:corp :identity]) 1)
(is (no-prompt? state :corp) "Cannot use Skorpios twice")))

(deftest skorpios-smart-test
(do-game
(new-game {:corp {:id "Skorpios Defense Systems: Persuasive Power"}
:runner {:deck ["Corroder"]}})
(damage state :corp :brain 1)
(click-prompt state :corp "Corroder")
(is (= 1 (count (get-in @state [:runner :rfg]))) "One card RFGed")))


(deftest spark-agency-worldswide-reach
;; Spark Agency - Rezzing advertisements
(do-game
Expand Down