|
| 1 | +(ns game.core.turmoil |
| 2 | + (:require |
| 3 | + [game.core.card :refer [agenda? asset? event? has-subtype? hardware? resource? program? upgrade? ice? operation? identity? corp? runner?]] |
| 4 | + [game.core.commands :refer [lobby-command]] |
| 5 | + [game.core.identities :refer [disable-identity]] |
| 6 | + [game.core.initializing :refer [card-init make-card]] |
| 7 | + [game.core.hosting :refer [host]] |
| 8 | + [game.core.moving :refer [move]] |
| 9 | + [game.core.payment :refer [->c]] |
| 10 | + [game.core.say :refer [system-msg]] |
| 11 | + [game.core.set-up :refer [build-card]] |
| 12 | + [game.utils :refer [same-card? server-cards server-card]] |
| 13 | + [clojure.string :as string])) |
| 14 | + |
| 15 | +;; store all this in memory once so we don't need to recalculate it a trillion times |
| 16 | +;; everything should be a vec, so rand-nth will be O[1] instead of O[n/2] |
| 17 | + |
| 18 | +(defonce agenda-by-points (atom {})) |
| 19 | +(defonce identity-by-side (atom {})) |
| 20 | +(defonce program-by-icebreaker (atom {})) |
| 21 | +(defonce cards-by-type (atom {})) |
| 22 | +(defonce has-been-set? (atom nil)) |
| 23 | + |
| 24 | +(defn- is-econ? |
| 25 | + "Is a card an economy card? |
| 26 | + Something like: |
| 27 | + * gain x credits |
| 28 | + * take x/all host/ed credits" |
| 29 | + [card] |
| 30 | + (re-find #".(ain|ake) (\d+|(.? host.*)).?.?.?redit" (or (:text (server-card (:title card))) ""))) |
| 31 | + |
| 32 | +(defonce filter-by-econ-types #{:asset :operation :resource :event}) |
| 33 | + |
| 34 | +(defn- set-cards! [] |
| 35 | + (when-not @has-been-set? |
| 36 | + (println "assigning server cards for turmoil") |
| 37 | + (reset! agenda-by-points |
| 38 | + (->> (server-cards) |
| 39 | + (filterv agenda?) |
| 40 | + (group-by :agendapoints))) |
| 41 | + (reset! identity-by-side {:corp (->> (server-cards) |
| 42 | + (filterv identity?) |
| 43 | + (filterv corp?)) |
| 44 | + :runner (->> (server-cards) |
| 45 | + (filterv identity?) |
| 46 | + (filterv runner?))}) |
| 47 | + (reset! program-by-icebreaker {:icebreaker (->> (server-cards) |
| 48 | + (filterv program?) |
| 49 | + (filterv #(has-subtype? % "Icebreaker"))) |
| 50 | + :regular (->> (server-cards) |
| 51 | + (filterv program?) |
| 52 | + (filterv #(not (has-subtype? % "Icebreaker"))))}) |
| 53 | + (reset! cards-by-type (let [types {:asset asset? |
| 54 | + :event event? |
| 55 | + :hardware hardware? |
| 56 | + :resource resource? |
| 57 | + :program program? |
| 58 | + :upgrade upgrade? |
| 59 | + :ice ice? |
| 60 | + :operation operation?} |
| 61 | + keys-sorted (sort (keys types))] |
| 62 | + (zipmap keys-sorted (mapv #(if (contains? filter-by-econ-types %) |
| 63 | + {:economy (filterv (every-pred (types %) is-econ?) (server-cards)) |
| 64 | + :regular (filterv (every-pred (types %) (complement is-econ?)) (server-cards))} |
| 65 | + (filterv (types %) (server-cards))) |
| 66 | + keys-sorted)))) |
| 67 | + (reset! has-been-set? true))) |
| 68 | + |
| 69 | +(def replacement-factor |
| 70 | + "how often should we replace these cards?" |
| 71 | + {:hand 4 :deck 8 :discard 3 :id 4 :side 50 :card-type-cross-contam 10 :icebreaker-cross-contam 10 :econ-cross-contam 10}) |
| 72 | + |
| 73 | +(defn- should-replace? |
| 74 | + ([key] (-> (key replacement-factor 25) rand-int zero?)) |
| 75 | + ([key ex] (-> (key replacement-factor 25) (min ex) (max 1) rand-int zero?))) |
| 76 | + |
| 77 | +(def corp-card-types #{:asset :upgrade :ice :operation}) |
| 78 | +(def runner-card-types #{:resource :hardware :program :event}) |
| 79 | + |
| 80 | +(defn- pick-replacement-card |
| 81 | + "given a card, pick a suitable replacement card at random |
| 82 | + agendas maintain point value, |
| 83 | + programs maintain if they are/aren't icebreakers, |
| 84 | + everything else is random" |
| 85 | + [card] |
| 86 | + (set-cards!) |
| 87 | + (let [c-type (-> card :type string/lower-case keyword)] |
| 88 | + (cond |
| 89 | + ;; agenda (x points) -> agenda (x points) should stop people gaming density |
| 90 | + (= c-type :agenda) |
| 91 | + (let [target-points (:agendapoints card)] |
| 92 | + (rand-nth (get @agenda-by-points target-points))) |
| 93 | + (= c-type :identity) |
| 94 | + (let [target-side (-> card :side string/lower-case keyword)] |
| 95 | + (rand-nth (get @identity-by-side target-side))) |
| 96 | + ;; icebreaker -> icebreaker should make it reasonable to not get completely locked out |
| 97 | + (= c-type :program) |
| 98 | + ;; allow 10% cross-contain for icebreakers |
| 99 | + (let [choice (should-replace? :icebreaker-cross-contam) |
| 100 | + choice (if (= 1 (count (filter identity [choice (has-subtype? card "Icebreaker")]))) |
| 101 | + :icebreaker |
| 102 | + :regular)] |
| 103 | + (rand-nth (get @program-by-icebreaker choice))) |
| 104 | + (contains? filter-by-econ-types c-type) |
| 105 | + ;; allow 10% cross-conta for econ |
| 106 | + (let [choice (rand-nth |
| 107 | + (if (is-econ? card) |
| 108 | + [:economy :economy :economy :economy :economy :economy :economy :economy :economy :regular] |
| 109 | + [:regular :regular :regular :regular :regular :regular :regular :regular :regular :economy]))] |
| 110 | + (rand-nth (get-in @cards-by-type [c-type choice]))) |
| 111 | + :else (rand-nth (get @cards-by-type c-type))))) |
| 112 | + |
| 113 | +(defn- replace-hand [state side] |
| 114 | + (let [new-hand (mapv #(if (should-replace? :hand (count (get-in @state [side :hand]))) |
| 115 | + (assoc (build-card (pick-replacement-card %)) :zone [:hand]) |
| 116 | + %) |
| 117 | + (get-in @state [side :hand]))] |
| 118 | + (swap! state assoc-in [side :hand] new-hand))) |
| 119 | + |
| 120 | +(defn- replace-discard [state side] |
| 121 | + (let [new-discard (mapv #(if (should-replace? :discard) |
| 122 | + (assoc (build-card (pick-replacement-card %)) :zone [:discard] :seen (:seen %)) |
| 123 | + %) |
| 124 | + (get-in @state [side :discard]))] |
| 125 | + (swap! state assoc-in [side :discard] new-discard))) |
| 126 | + |
| 127 | +(defn- replace-deck [state side] |
| 128 | + (let [new-deck (mapv #(if (should-replace? :deck) |
| 129 | + (assoc (build-card (pick-replacement-card %)) :zone [:deck]) |
| 130 | + %) |
| 131 | + (get-in @state [side :deck]))] |
| 132 | + (swap! state assoc-in [side :deck] new-deck))) |
| 133 | + |
| 134 | +(defn- replace-id [state side] |
| 135 | + ;; defuse any sillyness with 'replace-id' |
| 136 | + (when (should-replace? :id) |
| 137 | + (let [old-id (get-in @state [side :identity]) |
| 138 | + new-id (pick-replacement-card {:type "Identity" :side (name side)})] |
| 139 | + ;; Handle hosted cards (Ayla) - Part 1 |
| 140 | + (doseq [c (:hosted old-id)] |
| 141 | + (move state side c :temp-hosted)) |
| 142 | + (disable-identity state side) |
| 143 | + ;; Move the selected ID to [:runner :identity] and set the zone |
| 144 | + (let [new-id (-> new-id make-card (assoc :zone [(->c :identity)])) |
| 145 | + num-old-blanks (:num-disabled old-id)] |
| 146 | + (swap! state assoc-in [side :identity] new-id) |
| 147 | + (card-init state side new-id) |
| 148 | + (when num-old-blanks |
| 149 | + (dotimes [_ num-old-blanks] |
| 150 | + (disable-identity state side)))) |
| 151 | + ;; Handle hosted cards (Ayla) - Part 2 |
| 152 | + (doseq [c (get-in @state [side :temp-hosted])] |
| 153 | + ;; Currently assumes all hosted cards are hosted facedown (Ayla) |
| 154 | + (host state side (get-in @state [side :identity]) c {:facedown true}))))) |
| 155 | + |
| 156 | +(defn- transpose-sides |
| 157 | + [state side] |
| 158 | + ;; this is kosher I promise -> Emphyrio, Jack Vance (it's a neat book, I recommend it) |
| 159 | + (system-msg state side "FINUKA TRANSPOSES") |
| 160 | + (lobby-command {:command :swap-sides |
| 161 | + :gameid (:gameid @state)})) |
| 162 | + |
| 163 | +(defn shuffle-cards-for-side |
| 164 | + [state side] |
| 165 | + (do (replace-hand state side) |
| 166 | + (replace-deck state side) |
| 167 | + (replace-discard state side) |
| 168 | + (replace-id state side) |
| 169 | + ;; 1 in 50 chance at the start of each turn to swap the sides you're playing on |
| 170 | + ;; realistically, this should happen on average about 2 games in 4 |
| 171 | + ;; and 0.75 of those games will have 2+ swaps |
| 172 | + (when (should-replace? :side) |
| 173 | + (transpose-sides state side)))) |
0 commit comments