Skip to content
Merged

turmoil #7978

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
10 changes: 10 additions & 0 deletions resources/public/i18n/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/clj/game/core/actions.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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])))

Expand Down
3 changes: 2 additions & 1 deletion src/clj/game/core/set_up.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
173 changes: 173 additions & 0 deletions src/clj/game/core/turmoil.clj
Original file line number Diff line number Diff line change
@@ -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))))
5 changes: 5 additions & 0 deletions src/clj/game/core/turns.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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))

Expand Down
5 changes: 4 additions & 1 deletion src/clj/web/lobby.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -189,6 +191,7 @@
:save-replay
:singleton
:spectators
:turmoil
:corp-spectators
:runner-spectators
:spectatorhands
Expand Down
3 changes: 2 additions & 1 deletion src/cljs/nr/game_row.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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]])

Expand Down
2 changes: 1 addition & 1 deletion src/cljs/nr/gameboard/board.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]])))
Expand Down
13 changes: 13 additions & 0 deletions src/cljs/nr/new_game.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
:save-replay
:side
:singleton
:turmoil
:spectatorhands
:precon
:gateway-type
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."])]
Expand Down Expand Up @@ -246,6 +258,7 @@
:protected false
:save-replay (not= "casual" (:room @lobby-state))
:singleton false
:turmoil false
:spectatorhands false
:open-decklists false
:timed false
Expand Down
6 changes: 6 additions & 0 deletions src/cljs/nr/pending_game.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/css/lobby.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down