Skip to content

nbkelly's style guide (2025)

NBKelly edited this page Mar 8, 2025 · 4 revisions

Style/Implementation guide

Table of Contents

  1. Using req vs. NCIGS
  2. in-hand? vs in-hand*?
  3. Card-type specific helpers
  4. Using the changed? macro
  5. Writing first-event in a readable way
  6. Writing unit tests for ice
  7. Writing unit tests for icebreakers

req vs no-change-in-game-state

  • :req should be used for restrictions pertaining to the use of an ability (is it legal for me to play this)
  • :no-change-in-game-state should be used for restrictions pertaining to the consequences of an ability (when I use this, does it do anything)?
  • when using no-change-in-game-state, subroutines and consequences/second-order abilities should be silent, first-order/direct abilities should be loud

Examples:

  1. Border Control
(defcard "Border Control"
  {:abilities [{:label "End the run"
                :msg "end the run"
                :async true
                :req (req this-server run) ;; cannot play this ability except during a run on this server
		:cost [(->c :trash-can)]
                :effect (effect (end-run eid card))}]})
  1. Subroutines that trash cards
(def trash-program-sub
  {:prompt "Choose a program to trash"
   :label "Trash a program"
   :msg (msg "trash " (:title target))
   :waiting-prompt true
   ;; this subroutine *does nothing* if there are no programs installed to trash, 
   ;; but the log already says the sub is fired, so we don't need to waste time 
   ;; and log space forcing the players to click through this menu
   :change-in-game-state {:silent true 
                          :req (req (seq (filter program? (all-installed state :runner))))}
   :choices {:card #(and (installed? %)
                         (program? %))}
   :async true
   :effect (effect (trash eid target {:cause :subroutine}))})
  1. Economic Warfare

Play only if the Runner made a successful run during their last turn.
If the Runner has at least 4[credit], they lose 4[credit].

(defcard "Economic Warfare"
  {:on-play
   {:req (req (last-turn? state :runner :successful-run)) ;; the runner must have made a successful run last turn
    :async true
    :change-in-game-state {:req (req (>= (:credit runner) 4))} ;; but if they have 0 credits, we will just print "do nothing" in the log
    :msg "make the runner lose 4 [Credits]"
    :effect (effect (lose-credits :runner eid 4))}})

kelly spends [click] and pays 0 [credits] to play Economic Warfare.
kelly uses Economic Warfare to do nothing.

in-hand? vs in-hand*?

As of the release of elevation, there is at least one card which allows hosted cards to be played as if they were in hand.

For this, you can use the fn (in-hand*? [state target]), and to get the set of all cards that could be in hand, you can use the fn (all-cards-in-hand* [state side]).

An example with Modded:

(defcard "Modded"
  {:on-play
   {:prompt "Choose a program or piece of hardware to install"
    :change-in-game-state {:req (req (seq (all-cards-in-hand* state :runner)))}
    :choices {:req (req (and (or (hardware? target)
                                 (program? target))
                             (in-hand*? state target)
                             (can-pay? state side (assoc eid :source card :source-type :runner-install) target nil
                                       [(->c :credit (install-cost state side target {:cost-bonus -3}))])))}
    :async true
    :effect (effect (runner-install (assoc eid :source card :source-type :runner-install) target {:cost-bonus -3
                                                                                                  :msg-keys {:install-source card
                                                                                                             :display-origin true}}))}})

Card-type specific helpers

I put some work into making certain card types with repeated patterns easy to implement.

These are:

  1. auto-icebreaker - anything which breaks ice, even if it's not actually an icebreaker. There are a few tricks here:
  • If it has multiple break abilities, then you can use :auto-break-sort to specify which one should be preferred
  • if it has an X-cost break ability, you can specify :auto-break-creds-per-sub x to specify the ratio is breaks at for calculations
  1. trojan - trojan cards (even if they aren't actually trojans, it will still work if they get hosted on ice). This will require them to be installed on an ice. For cards like egret, you can do:
  • (trojan {:rezzed true} cdef to specify they must be installed on a rezzed ice.

Using the changed? macro

In unit tests, you can check if a value changes by doing:

(is (changed? [(:credit (get-corp)) 4
               ...]
  (play-from-hand state :corp "Hedge Fund"))
  "Gained 4 Credits")

You can make your text editor indent these nicely as if they were any other macro with:

Emacs

;; these follow the form (name style) in define-clojure-indent                                        
(add-hook 'clojure-mode-hook
          (lambda ()
            (define-clojure-indent
              (before-each '(1 ((:defn)) nil)))
            (define-clojure-indent
              (changed? '(1 ((:defn)) nil)))))

Everything else

You should be able to find a way to add this pattern to your linter: {:style/indent [1 [[:defn]] :form]}

Ensuring 'first-event' in a readable way

I like this particular pattern because (I think) it's pretty readable, and it doesn't require a lot of code duplication.

(letfn [(valid-ctx? [[ctx]] (whatever-set-of-preds-works ctx))]
  (and (valid-ctx? [context]) 
       (first-event state side :whatever-event valid-ctx?)))

If you don't want to box context, you can just pass in targets instead (context is the first arg of targets).

Additionally, you can destructure ctx like (valid-ctx? [[{:keys [relevant-key other-relevant-key]}]], to make it even easier to read/write.

Here's Hyoubu right now:

{:event :corp-reveal
 :req (req (letfn [(valid-ctx? [[{:keys [cards] :as ctx}]] (pos? (count cards)))]
             (and (valid-ctx? [context])

Writing unit tests for ice

I've put a lot of work into making unit tests for ice really easy and readable.

Observe:

  1. Encounter a Toll Booth and pay 3 Credits
(deftest encounter-toll-booth
  (do-game 
    (run-and-encounter-ice-test "Tollbooth")
    (is (and (= 2 (:credit (get-runner)))
             (no-prompt? state :runner)
             run)
        "Runner paid 3, run did not end")))
  1. Encounter a Toll Booth but you can't pay
(deftest encounter-toll-booth
  (do-game 
    (run-and-encounter-ice-test "Tollbooth" {:runner {:credits 2}})
    (is (and (= 2 (:credit (get-runner)))
             (no-prompt? state :runner)
             (not run))
        "Runner paid 0, run did not end")))
  1. Encounter a Toll Booth but you use penumbral toolkit - when you can, and when you can't afford to pay normally
(deftest encounter-toll-booth
  (doseq [creds [0 3 5 7]]
    (do-game 
      (run-and-encounter-ice-test "Tollbooth" {:runner {:credits 0}} {:rig ["Penumbral Toolkit"]})
      (dotimes [n 3] (click-card state :runner "Penumbral Toolkit"))
      (is (and (= credits (:credit (get-runner)))
               (no-prompt? state :runner)
               run))
          "Runner paid 3 from toolkit, run did not end"))))
  1. Test a specific ice subroutine
(deftest trebuchet-trashes-programs
  (do-game
    (subroutine-test "Trebuchet" 0 nil {:rig ["Rezeki"]})
    (is (get-program state 0) "Rezeki installed")
    (click-card state :corp "Rezeki")
    (is (not (get-program state 0)) "Rezeki is not installed")
    (is (= "Rezeki" (->> @state :runner :discard first :title)) "Rezeki in trash")))
  1. Testing arbitrary end-the-run subroutines
(deftest ice-wall-etr-test (do-game (new-game (etr-sub "Ice Wall" 0))))
  1. Testing arbitrary damage subroutines
(deftest neural-katana-does-3-damage-sub (do-game (new-game (does-damage-sub "Neural Katana" 0 3))))

Writing unit tests for icebreakers

Likewise, I put a lot of work into providing an easy way to unit test (most) icebreakers. This is testing (primarily) that:

  • the numbers do what they say on the card (you can write a test straight from the card text)
  • this can set the threat level if required
  • this checks the subtypes that get broken
  • if counters modify the strength of the card, this gets tested too
  1. Buzzsaw
(deftest buzzsaw-automated-test
  (basic-program-test {:name "Buzzsaw"
                       :boost {:ab 1 :amount 1 :cost 3}
                       :break {:ab 0 :amount 2 :cost 1 :type "Code Gate"}}))
  1. Darwin
(deftest darwin-automated-test
  (basic-program-test {:name "Darwin"
                       :counters-modify-strength {:type :virus}
                       :break {:ab 0 :amount 1 :cost 2 :type "All"}}))
  1. Mimic (a fixed strength breaker)
(deftest mimic-automated-test
  (basic-program-test {:name "Mimic"
                       :break {:ab 0 :amount 1 :cost 1 :type "Sentry"}}))
  1. Lustig (a simple case)
(deftest lustig-automated-test
  (basic-program-test {:name "Lustig"
                       :break {:ab 0 :amount 1 :cost 1 :type "Sentry"}
                       :boost {:ab 1 :amount 5 :cost 3}}))
Clone this wiki locally