diff --git a/resources/public/i18n/en.ftl b/resources/public/i18n/en.ftl index 839a5fb377..8b04ae985a 100644 --- a/resources/public/i18n/en.ftl +++ b/resources/public/i18n/en.ftl @@ -716,6 +716,16 @@ ingame-settings_sort-heap = Sort Heap ingame-settings_stack-cards = Stack cards +lobby_turmoil = Turmoil + +lobby_turmoil-details = The fickle winds of fate shall decide your future. + +lobby_turmoil-theme = "FINUKA DISPOSES" + +lobby_turmoil-info = This lobby is running in turmoil mode. The winds of fate shall decide your path to the future. + +lobby_span-turmoil = (turmoil) + lobby_aborted = Connection aborted lobby_api-access = Allow API access to game information diff --git a/src/clj/game/core/actions.clj b/src/clj/game/core/actions.clj index 72e7f236f1..048b36bd2e 100644 --- a/src/clj/game/core/actions.clj +++ b/src/clj/game/core/actions.clj @@ -628,7 +628,7 @@ [state _ {:keys [card]}] (if-let [card (get-card state card)] (if (expendable? state card) - (swap! state assoc-in [:corp :install-list] (conj (installable-servers state card) "Expend")) ;;april fools we can make this "cast as a sorcery" + (swap! state assoc-in [:corp :install-list] (conj (installable-servers state card) "Cast as a Sorcery")) ;;april fools we can make this "cast as a sorcery" (swap! state assoc-in [:corp :install-list] (installable-servers state card))) (swap! state dissoc-in [:corp :install-list]))) diff --git a/src/clj/game/core/set_up.clj b/src/clj/game/core/set_up.clj index 8731bc18ea..e28d8affcc 100644 --- a/src/clj/game/core/set_up.clj +++ b/src/clj/game/core/set_up.clj @@ -78,7 +78,7 @@ (defn- init-game-state "Initialises the game state" - [{:keys [players gameid timer spectatorhands api-access save-replay room] :as game}] + [{:keys [players gameid timer spectatorhands api-access save-replay room turmoil] :as game}] (let [corp (some #(when (corp? %) %) players) runner (some #(when (runner? %) %) players) corp-deck (create-deck (:deck corp)) @@ -106,6 +106,7 @@ (inst/now) {:timer timer :spectatorhands spectatorhands + :turmoil turmoil :api-access api-access :save-replay save-replay} (new-corp (:user corp) corp-identity corp-options (map #(assoc % :zone [:deck]) corp-deck) corp-deck-id corp-quote) diff --git a/src/clj/game/core/turmoil.clj b/src/clj/game/core/turmoil.clj new file mode 100644 index 0000000000..fe4497f464 --- /dev/null +++ b/src/clj/game/core/turmoil.clj @@ -0,0 +1,173 @@ +(ns game.core.turmoil + (:require + [game.core.card :refer [agenda? asset? event? has-subtype? hardware? resource? program? upgrade? ice? operation? identity? corp? runner?]] + [game.core.commands :refer [lobby-command]] + [game.core.identities :refer [disable-identity]] + [game.core.initializing :refer [card-init make-card]] + [game.core.hosting :refer [host]] + [game.core.moving :refer [move]] + [game.core.payment :refer [->c]] + [game.core.say :refer [system-msg]] + [game.core.set-up :refer [build-card]] + [game.utils :refer [same-card? server-cards server-card]] + [clojure.string :as string])) + +;; store all this in memory once so we don't need to recalculate it a trillion times +;; everything should be a vec, so rand-nth will be O[1] instead of O[n/2] + +(defonce agenda-by-points (atom {})) +(defonce identity-by-side (atom {})) +(defonce program-by-icebreaker (atom {})) +(defonce cards-by-type (atom {})) +(defonce has-been-set? (atom nil)) + +(defn- is-econ? + "Is a card an economy card? + Something like: + * gain x credits + * take x/all host/ed credits" + [card] + (re-find #".(ain|ake) (\d+|(.? host.*)).?.?.?redit" (or (:text (server-card (:title card))) ""))) + +(defonce filter-by-econ-types #{:asset :operation :resource :event}) + +(defn- set-cards! [] + (when-not @has-been-set? + (println "assigning server cards for turmoil") + (reset! agenda-by-points + (->> (server-cards) + (filterv agenda?) + (group-by :agendapoints))) + (reset! identity-by-side {:corp (->> (server-cards) + (filterv identity?) + (filterv corp?)) + :runner (->> (server-cards) + (filterv identity?) + (filterv runner?))}) + (reset! program-by-icebreaker {:icebreaker (->> (server-cards) + (filterv program?) + (filterv #(has-subtype? % "Icebreaker"))) + :regular (->> (server-cards) + (filterv program?) + (filterv #(not (has-subtype? % "Icebreaker"))))}) + (reset! cards-by-type (let [types {:asset asset? + :event event? + :hardware hardware? + :resource resource? + :program program? + :upgrade upgrade? + :ice ice? + :operation operation?} + keys-sorted (sort (keys types))] + (zipmap keys-sorted (mapv #(if (contains? filter-by-econ-types %) + {:economy (filterv (every-pred (types %) is-econ?) (server-cards)) + :regular (filterv (every-pred (types %) (complement is-econ?)) (server-cards))} + (filterv (types %) (server-cards))) + keys-sorted)))) + (reset! has-been-set? true))) + +(def replacement-factor + "how often should we replace these cards?" + {:hand 4 :deck 8 :discard 3 :id 4 :side 50 :card-type-cross-contam 10 :icebreaker-cross-contam 10 :econ-cross-contam 10}) + +(defn- should-replace? + ([key] (-> (key replacement-factor 25) rand-int zero?)) + ([key ex] (-> (key replacement-factor 25) (min ex) (max 1) rand-int zero?))) + +(def corp-card-types #{:asset :upgrade :ice :operation}) +(def runner-card-types #{:resource :hardware :program :event}) + +(defn- pick-replacement-card + "given a card, pick a suitable replacement card at random + agendas maintain point value, + programs maintain if they are/aren't icebreakers, + everything else is random" + [card] + (set-cards!) + (let [c-type (-> card :type string/lower-case keyword)] + (cond + ;; agenda (x points) -> agenda (x points) should stop people gaming density + (= c-type :agenda) + (let [target-points (:agendapoints card)] + (rand-nth (get @agenda-by-points target-points))) + (= c-type :identity) + (let [target-side (-> card :side string/lower-case keyword)] + (rand-nth (get @identity-by-side target-side))) + ;; icebreaker -> icebreaker should make it reasonable to not get completely locked out + (= c-type :program) + ;; allow 10% cross-contain for icebreakers + (let [choice (should-replace? :icebreaker-cross-contam) + choice (if (= 1 (count (filter identity [choice (has-subtype? card "Icebreaker")]))) + :icebreaker + :regular)] + (rand-nth (get @program-by-icebreaker choice))) + (contains? filter-by-econ-types c-type) + ;; allow 10% cross-conta for econ + (let [choice (rand-nth + (if (is-econ? card) + [:economy :economy :economy :economy :economy :economy :economy :economy :economy :regular] + [:regular :regular :regular :regular :regular :regular :regular :regular :regular :economy]))] + (rand-nth (get-in @cards-by-type [c-type choice]))) + :else (rand-nth (get @cards-by-type c-type))))) + +(defn- replace-hand [state side] + (let [new-hand (mapv #(if (should-replace? :hand (count (get-in @state [side :hand]))) + (assoc (build-card (pick-replacement-card %)) :zone [:hand]) + %) + (get-in @state [side :hand]))] + (swap! state assoc-in [side :hand] new-hand))) + +(defn- replace-discard [state side] + (let [new-discard (mapv #(if (should-replace? :discard) + (assoc (build-card (pick-replacement-card %)) :zone [:discard] :seen (:seen %)) + %) + (get-in @state [side :discard]))] + (swap! state assoc-in [side :discard] new-discard))) + +(defn- replace-deck [state side] + (let [new-deck (mapv #(if (should-replace? :deck) + (assoc (build-card (pick-replacement-card %)) :zone [:deck]) + %) + (get-in @state [side :deck]))] + (swap! state assoc-in [side :deck] new-deck))) + +(defn- replace-id [state side] + ;; defuse any sillyness with 'replace-id' + (when (should-replace? :id) + (let [old-id (get-in @state [side :identity]) + new-id (pick-replacement-card {:type "Identity" :side (name side)})] + ;; Handle hosted cards (Ayla) - Part 1 + (doseq [c (:hosted old-id)] + (move state side c :temp-hosted)) + (disable-identity state side) + ;; Move the selected ID to [:runner :identity] and set the zone + (let [new-id (-> new-id make-card (assoc :zone [(->c :identity)])) + num-old-blanks (:num-disabled old-id)] + (swap! state assoc-in [side :identity] new-id) + (card-init state side new-id) + (when num-old-blanks + (dotimes [_ num-old-blanks] + (disable-identity state side)))) + ;; Handle hosted cards (Ayla) - Part 2 + (doseq [c (get-in @state [side :temp-hosted])] + ;; Currently assumes all hosted cards are hosted facedown (Ayla) + (host state side (get-in @state [side :identity]) c {:facedown true}))))) + +(defn- transpose-sides + [state side] + ;; this is kosher I promise -> Emphyrio, Jack Vance (it's a neat book, I recommend it) + (system-msg state side "FINUKA TRANSPOSES") + (lobby-command {:command :swap-sides + :gameid (:gameid @state)})) + +(defn shuffle-cards-for-side + [state side] + (do (replace-hand state side) + (replace-deck state side) + (replace-discard state side) + (replace-id state side) + ;; 1 in 50 chance at the start of each turn to swap the sides you're playing on + ;; realistically, this should happen on average about 2 games in 4 + ;; and 0.75 of those games will have 2+ swaps + (when (should-replace? :side) + (transpose-sides state side)))) diff --git a/src/clj/game/core/turns.clj b/src/clj/game/core/turns.clj index dba611bbaa..0c5eb7f0da 100644 --- a/src/clj/game/core/turns.clj +++ b/src/clj/game/core/turns.clj @@ -15,6 +15,7 @@ [game.core.say :refer [system-msg]] [game.core.set-aside :refer [clean-set-aside!]] [game.core.toasts :refer [toast]] + [game.core.turmoil :as turmoil] [game.core.update :refer [update!]] [game.core.winning :refer [flatline]] [game.macros :refer [continue-ability req wait-for]] @@ -73,6 +74,10 @@ (swap! state assoc :click-states []) (swap! state assoc :turn-state (dissoc @state :log :history :turn-state)) + ;; resolve turmoil (april fools) + (when (get-in @state [:options :turmoil]) + (turmoil/shuffle-cards-for-side state side)) + (when (= side :corp) (swap! state update-in [:turn] inc)) diff --git a/src/clj/web/lobby.clj b/src/clj/web/lobby.clj index 40a91b7921..599eea7c46 100644 --- a/src/clj/web/lobby.clj +++ b/src/clj/web/lobby.clj @@ -85,7 +85,8 @@ user :user {:keys [gameid now allow-spectator api-access format mute-spectators password room save-replay - precon gateway-type side singleton spectatorhands timer title open-decklists] + precon gateway-type side singleton spectatorhands timer title open-decklists + turmoil] :or {gameid (random-uuid) now (inst/now)}} :options}] (let [player {:user user @@ -109,6 +110,7 @@ :mute-spectators mute-spectators :password (when (not-empty password) (bcrypt/encrypt password)) :room room + :turmoil turmoil :save-replay save-replay :spectatorhands spectatorhands :singleton (when (some #{format} `("standard" "startup" "casual" "eternal")) singleton) @@ -189,6 +191,7 @@ :save-replay :singleton :spectators + :turmoil :corp-spectators :runner-spectators :spectatorhands diff --git a/src/cljs/nr/game_row.cljs b/src/cljs/nr/game_row.cljs index 22cfe5e8f4..66adf17171 100644 --- a/src/cljs/nr/game_row.cljs +++ b/src/cljs/nr/game_row.cljs @@ -191,12 +191,13 @@ (when (and open-decklists (not precon)) [:span.open-decklists (str " " (tr [:lobby_open-decklists-b] "(open decklists)"))])) -(defn game-format [{fmt :format singleton? :singleton precon :precon open-decklists :open-decklists}] +(defn game-format [{fmt :format singleton? :singleton turmoil? :turmoil precon :precon open-decklists :open-decklists}] [:div {:class "game-format"} [:span.format-label (tr [:lobby_format "Format"]) ": "] [:span.format-type (tr-format (slug->format fmt "Unknown"))] [precon-span precon] [:span.format-singleton (str (when singleton? (str " " (tr [:lobby_singleton-b "(singleton)"]))))] + [:span.turmoil (when turmoil? (str " " (tr [:lobby_span-turmoil "(turmoil)"])))] [open-decklists-span precon open-decklists] [precon-under-span precon]]) diff --git a/src/cljs/nr/gameboard/board.cljs b/src/cljs/nr/gameboard/board.cljs index dd6b288c43..363742685d 100644 --- a/src/cljs/nr/gameboard/board.cljs +++ b/src/cljs/nr/gameboard/board.cljs @@ -444,7 +444,7 @@ ^{:key label} [card-menu-item (label-fn label) #(do (close-card-menu) - (if (= "Expend" label) + (if (= "Cast as a Sorcery" label) (send-command "expend" {:card card :server label}) (send-command "play" {:card card :server label})))]) servers))]]))) diff --git a/src/cljs/nr/new_game.cljs b/src/cljs/nr/new_game.cljs index dd49b3fddc..7e599bbcc4 100644 --- a/src/cljs/nr/new_game.cljs +++ b/src/cljs/nr/new_game.cljs @@ -18,6 +18,7 @@ :save-replay :side :singleton + :turmoil :spectatorhands :precon :gateway-type @@ -79,6 +80,12 @@ :on-change #(swap! options assoc :singleton (.. % -target -checked))}] (tr [:lobby_singleton "Singleton"])]) +(defn turmoil-mode [options] + [:span [:label + [:input {:type "checkbox" :checked (:turmoil @options) + :on-change #(swap! options assoc :turmoil (.. % -target -checked))}] + (tr [:lobby_turmoil "Turmoil"])]]) + (defn open-decklists [options] [:label [:input {:type "checkbox" :checked (:open-decklists @options) @@ -124,8 +131,13 @@ ^{:key k} [:option {:value k} (tr-format v)]))] [singleton-only options fmt-state] + [turmoil-mode options] [gateway-constructed-choice fmt-state gateway-type] [precon-choice fmt-state precon] + [:div.infobox.blue-shade + {:style {:display (if (:turmoil @options) "block" "none")}} + [:p (tr [:lobby_turmoil-details "The fickle winds of fate shall decide your future."])] + [:p (tr [:lobby_turmoil-theme "\"FINUKA DISPOSES\""])]] [:div.infobox.blue-shade {:style {:display (if (:singleton @options) "block" "none")}} [:p (tr [:lobby_singleton-details "This will restrict decklists to only those which do not contain any duplicate cards. It is recommended you use the listed singleton-based identities."])] @@ -246,6 +258,7 @@ :protected false :save-replay (not= "casual" (:room @lobby-state)) :singleton false + :turmoil false :spectatorhands false :open-decklists false :timed false diff --git a/src/cljs/nr/pending_game.cljs b/src/cljs/nr/pending_game.cljs index 088ecb1a65..5c900b7451 100644 --- a/src/cljs/nr/pending_game.cljs +++ b/src/cljs/nr/pending_game.cljs @@ -99,6 +99,11 @@ [:div.infobox.blue-shade [:p (tr [:lobby_singleton-restriction "This lobby is running in singleton mode. This means decklists will be restricted to only those which do not contain any duplicate cards."])]])) +(defn turmoil-info-box [current-game] + (when (:turmoil-mode @current-game) + [:div.infobox.blue-shade + [:p (tr [:lobby_turmoil-info "This lobby is running in turmoil mode. The winds of fate shall decide your path to the future."])]])) + (defn swap-sides-button [user gameid players] (when (first-user? @players @user) (if (< 1 (count @players)) @@ -205,6 +210,7 @@ [:h2 (:title @current-game)] [precon-info-box current-game] [singleton-info-box current-game] + [turmoil-info-box current-game] (when-not (or (every? :deck @players) (not (is-constructed? current-game))) [:div.flash-message diff --git a/src/css/lobby.styl b/src/css/lobby.styl index 98da2de269..115f46f5e2 100644 --- a/src/css/lobby.styl +++ b/src/css/lobby.styl @@ -141,6 +141,12 @@ color: #ff571a; font-style: bold; + .format-turmoil + background-image: linear-gradient(to left, green, yellow, orange, red); + -webkit-background-clip: text; + font-style: bold; + color: transparent; + .open-decklists color: #ffE31A; font-style: bold;