From f6d989135e7893b065ef7b811a58aeeefdef805b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sun, 12 Apr 2020 17:11:21 +0100 Subject: [PATCH 1/2] Added cucumber and gorilla --- .gitignore | 2 ++ doc/naming-of-characters.md | 34 +++++++++++++++++++++++++++ doc/on-dying.ods | 13 ++++++++++ doc/orgnic-quests.md | 47 +++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 doc/naming-of-characters.md create mode 100644 doc/on-dying.ods create mode 100644 doc/orgnic-quests.md diff --git a/.gitignore b/.gitignore index 0910231..7387005 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ pom.xml.asc .nrepl-port .cpcache/ *~ + +docs/cloverage/ diff --git a/doc/naming-of-characters.md b/doc/naming-of-characters.md new file mode 100644 index 0000000..8ca8e80 --- /dev/null +++ b/doc/naming-of-characters.md @@ -0,0 +1,34 @@ +# Naming of Characters + +Generally speaking, in modern RPGs, every character with any impact on the plot has a distinct name. But if we are going to give all non-player characters sufficient agency to impact on the plot, then we must have a way of naming tens or hundreds of thousands of characters, and distinct names will become problematic (even if we're procedurally generating names, which we shall have to do. So this note is about how characters are named. + + +The full name of each character will be made up as follows: + +[epithet] [clan] [personal-name] the [trade-or-rank] of [location], son/daughter of [parent] + +Based on, roughly, historical name patterns like + +Archibald (personal-name) the Grim (epithet), Earl (trade-or-rank) of Douglas (location) + +Where + +1. *epithet* is a prefix based on some notable feature or feat of the character. Most characters won't have an epithet, unless they have some notable feature or they've done something notable. If a character does something notable in the course of the game, they will subsequently gain an epithet; 'notability' may be measured by how many times the event is transmitted through the gossip network. + +2. *clan* is special to the Western Clans, although people from the Great Place may possible use the name of their house similarly. + +3. *personal-name* is chosen from one of a limited set of limited sets; different cultural groups will have different (possibly overlapping) sets of names, but within each set there will only be a limited subset + +4. *trade-or-rank* is just that. "Smith", "Miller", "Ariston", "Captain". Either only master craftsfolk have the trade-or-rank name of their craft, or we distinguish between 'Calon the Smith', who may be a journeyman, and 'Calon the Master Smith', who is a master. + +5. *location* is the name of a location; a village, town, city or province. The location which forms part of a character's name is the location where there current home is, not the location where they were born or where their ancestors came from + +Full names will almost never be used - only, perhaps, in extremely formal circumstances. The form of a name used will depend on context, and will generally be just sufficient to disambiguate the character in the context. + +If the speaker is in Sinhua and referring to someone from Sinhua, they won't refer to them as 'of Sinhua'. + +If everyone present is a bargee and the speaker referring to someone who is also a bargee, they won't refer to them as 'the bargee'. + +The question asked influences the context: in answer to the question 'who is the best sword smith', the answer will not be 'Calon the Smith' but 'Calon of Sinhua'. + +Patronymics/matronymics will not normally be used of adults (although they may be used of apprentices and journeymen. diff --git a/doc/on-dying.ods b/doc/on-dying.ods new file mode 100644 index 0000000..0251f2d --- /dev/null +++ b/doc/on-dying.ods @@ -0,0 +1,13 @@ +# On Dying + +Death is the end of your story. One of the tropes in games which, for me, most breaks immersion is when you lose a fight and are presented with a screen that says 'you are dead. Do you want to reload your last save?' Life is not like that. We do not have save-states. We die. + +So how could this be better handled? + +You lose a fight. Switch to cutscene: the battlefield, after the fight, your body is there. Probably no sound. A party of non-enemies crosses the battlefield and finds your body. We see surprise and concern. They gather around you. Cut to interior scene, you are in a bed, unconcious, being tended; cut to similar interior scene, you are in a bed, conscious, being tended; cut to exterior scene, you are sitting with some of your saviours, and the game restarts. + +Time has passed; events in the game world have moved on. You can talk to your saviours about it. You have lost a lot of strength, and most of the gear you were carrying. You must do whatever it is you do within the game mechanics to rebuild strength, and to acquire more gear. Significantly you have acquired a debt of honour to your saviours, which they may call on later. You almost certainly have new scars, and might possibly have some lasting effects (although how that interacts with other game mechanics might be tricky). + +So who are the non-enemies? It depends on context. If you have a party, and some of that party survived the fight, it's your party. Otherwise, if you're in a populated place, it's locals. If it's on a road or other route, it's passing merchants. If you're in the wilderness, a hunting party. It's a bunch of non-hostiles who might reasonably be expected to be around: that's what matters. It's about not breaking immersion. + +Obviously losing a fight must have weight, it must have meaning, it must have in-game consequences; otherwise it is meaningless. diff --git a/doc/orgnic-quests.md b/doc/orgnic-quests.md new file mode 100644 index 0000000..bf789b7 --- /dev/null +++ b/doc/orgnic-quests.md @@ -0,0 +1,47 @@ +# Organic Quests + +The structure of a modern Role Playing Came revolves around 'quests': tasks that the player character is invited to do, either by the framing narrative of the game or by some non-player character ('the Quest Giver'). Normally there is one core quest which provides the overarching narrative for the whole game. [Wikipedia](https://en.wikipedia.org/wiki/Quest_(gaming)) offers a typology of quests as follows: + +1. Kill quests +2. Combo quests +3. Delivery quests +4. Gather quests +5. Escort quests +6. Syntax quests +7. Hybrids + +'Gather quests' are more frequently referred to in the literature as 'fetch quests', and 'kill quests' are simply a specialised form of fetch quest where the item to be fetched is a trophy of the kill. A delivery quest is a sort of reverse fetch quest: instead of going to some location or NPC and getting a specific item to return to the quest giver, the player is tasked to take a specific item from the quest giver to some location or NPC. + +Hybrids are in effect chains of quests: do this task in order to get this precondition of this other task, in order to get the overall objective; obviously such chains can be deep and involved - the 'main quest' of every role playing game I know of is a chain or hybrid quest. + +My understanding is that what Wikipedia means by a 'syntax quest' is what one would normally call a puzzle. + +An escort quest is typically a request to take a specified non-player character safely through a dangerous area. + +Combo quests are not, in my opinion, particularly relevant to the sorts of game we're discussing here. + +So essentially quests break down into three core types + +1. Fetch and deliver quests +2. Escort quests +3. Puzzles + +which are combined together into more or less complex chains, where the simplest chain is a single quest. + +Given that quests are as simple as this, it's obvious that narrative sophistication is required to make them interesting; and this point is clearly made by some variants of roguelike games which procedurally generate quests: they're generally pretty dull. By contrast, the Witcher series is full of fetch-quests which are made to really matter by being wrapped in interesting character interaction and narrative plausibility. Very often this takes the form of tragedy: as one reviewer pointed out, the missing relatives that Geralt is asked to find generally turn out to be (horribly) dead. In other words, creative scripting tends to deliver much more narratively satisfying quests than is usually delivered by procedural generation. + +But, if we're thinking of a game with much more intelligent non-player characters with much more conversational repertoir, as I am, can satisfying quests emerge organically? In space trading games such as [Elite](https://www.telegraph.co.uk/games/11051122/Elite-the-game-that-changed-the-world.html), a primary activity is moving goods from markets with surplus (and thus low prices) to markets with shortage (and thus high prices). This is, in effect, a game made up of deliver quests - but rather than deliver quests which are scripted, they are deliver quests which arise organically out of the structure of the game world. + +I already have working code for non-player character merchants, who move goods from city to city based on market information available to them. For player characters to join in this trading is an organic activity emerging from the structure of the world, which provides an activity. But moving merchants provides a market opportunity for bandits, who can intercept and steal cargoes, and so for mercenaries, who can protect cargoes from bandits, and so on. And because I have an architecture that allows non-player characters to fill economic niches, there will be non-player characters in all these niches. + +Where a non-player character can act, so can a player character: when a (non-player character) merchant seeks to hire a caravan guard and a player character responds, that's an organic escort quest. + +The key idea behind organic quests is that the circumstance and requirments for quests emerges as an emergent behaviour out of the mechanics of the game world. A non-player character doesn't know that there is a player character who is different from them; rather, when a non-player character needs something they can't readily achieve for themselves, they will ask other characters to help, and that may include the player character. + +This means, of course, that characters need a goal-seeking planning algorithm to decide their actions, with one option in any plan being 'ask for help'. Thus, 'asking for help' becomes a mechanism within the game, a normal behaviour. Ideally non-player characters will keep track of quite complex webs of loyalty and of obligation - debts of honour, duties of hospitality, collective loyalties. So that, if you do a favour for some character in the world, that character's tribe, friends, obligation circle, whatever, are now more likely to do favours for you. + +Obviously, this doesn't stop you doing jobs you get directly paid/rewarded for, but I'd like the web of obligation to be at least potentially much richer than just tit for tat. + +Related to this notion is the notion that, if you are asked to do a task by a character and you do it well, whether for pay or as a favour, your reputation for being competent in tasks of that kind will improve and the more likely it is that other characters will ask you to do similar tasks; and this will apply to virtually anything another character can ask of you in the game world, from carrying out an assassination to delivering a message to finding a quantiy of some specific commodity to having sex. + +So quests can emerge organically from the mechanics of the world and be richly varied; I'm confident that will work. What I'm not confident of is that they can be narratively satisfying. This relates directly to the generation of speech. From 5d37ad29eb87304c7ef6e83b65b0c22330a08a07 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sun, 12 Apr 2020 17:13:49 +0100 Subject: [PATCH 2/2] Sweep-up of changes on illuminator --- .../merchants/merchant_utils.clj.html | 188 +++++++++++------- docs/index.html | 4 +- project.clj | 5 +- src/the_great_game/core.clj | 6 - .../merchants/merchant_utils.clj | 46 +++-- .../the_great_game/merchants/markets_test.clj | 31 ++- .../merchants/merchant_utils_test.clj | 103 ++++++++++ 7 files changed, 281 insertions(+), 102 deletions(-) delete mode 100644 src/the_great_game/core.clj diff --git a/docs/cloverage/the_great_game/merchants/merchant_utils.clj.html b/docs/cloverage/the_great_game/merchants/merchant_utils.clj.html index 70cd0f3..8774b44 100644 --- a/docs/cloverage/the_great_game/merchants/merchant_utils.clj.html +++ b/docs/cloverage/the_great_game/merchants/merchant_utils.clj.html @@ -64,13 +64,13 @@ 020    [merchant world]
- + 021    (let [m (cond
022              (keyword? merchant)
- + 023              (-> world :merchants merchant)
@@ -79,8 +79,8 @@ 025              merchant)
- - 026          cargo (:stock m)] + + 026          cargo (or (:stock m) {})]
027      (reduce @@ -94,7 +94,7 @@ 030        (map
- + 031          #(* (cargo %) (-> world :commodities % :weight))
@@ -124,7 +124,7 @@ 040              (keyword? merchant)
- + 041              (-> world :merchants merchant)
@@ -133,152 +133,194 @@ 043              merchant)]
- - 044      (quot + + 044      (max
- - 045        (- (:capacity m) (burden m world)) + + 045        0 +
+ + 046        (quot +
+ + 047          (- (or (:capacity m) 0) (burden m world))
- 046        (-> world :commodities commodity :weight)))) + 048          (-> world :commodities commodity :weight)))))
- 047   + 049  
- 048  (defn can-afford + 050  (defn can-afford
- 049    "Return the number of units of this `commodity` which this `merchant` + 051    "Return the number of units of this `commodity` which this `merchant`
- 050    can afford to buy in this `world`." + 052    can afford to buy in this `world`."
- 051    [merchant world commodity] + 053    [merchant world commodity]
- 052    (let [m (cond + 054    (let [m (cond
- 053              (keyword? merchant) + 055              (keyword? merchant)
- - 054              (-> world :merchants merchant) + + 056              (-> world :merchants merchant)
- 055              (map? merchant) + 057              (map? merchant)
- 056              merchant) + 058              merchant)
- 057          l (:location m)] + 059          l (:location m)] +
+ + 060      (cond +
+ + 061        (nil? m) +
+ + 062        (throw (Exception. "No merchant?")) +
+ + 063        (or (nil? l) (nil? (-> world :cities l))) +
+ + 064        (throw (Exception. (str "No known location for merchant " m))) +
+ + 065        :else
- 058      (quot + 066        (quot
- 059        (:cash m) + 067          (:cash m)
- 060        (-> world :cities l :prices commodity)))) + 068          (-> world :cities l :prices commodity)))))
- 061   + 069  
- 062  (defn add-stock + 070  (defn add-stock
- 063    "Where `a` and `b` are both maps all of whose values are numbers, return + 071    "Where `a` and `b` are both maps all of whose values are numbers, return
- 064    a map whose keys are a union of the keys of `a` and `b` and whose values + 072    a map whose keys are a union of the keys of `a` and `b` and whose values
- 065    are the sums of their respective values." + 073    are the sums of their respective values."
- 066    [a b] + 074    [a b]
- - 067    (reduce + + 075    (reduce
- - 068      merge + + 076      merge
- - 069      a + + 077      a
- - 070      (map + + 078      (map
- - 071        #(hash-map % (+ (or (a %) 0) (or (b %) 0))) + + 079        #(hash-map % (+ (or (a %) 0) (or (b %) 0)))
- - 072        (keys b)))) + + 080        (keys b))))
- 073   + 081  
- 074  (defn add-known-prices + 082  (defn add-known-prices
- 075    "Add the current prices at this `merchant`'s location in the `world` + 083    "Add the current prices at this `merchant`'s location in the `world`
- 076    to a new cacke of known prices, and return it." + 084    to a new cache of known prices, and return it."
- 077    [merchant world] + 085    [merchant world]
- 078    (let [m (cond + 086    (let [m (cond
- 079              (keyword? merchant) + 087              (keyword? merchant)
- 080              (-> world :merchants merchant) + 088              (-> world :merchants merchant)
- 081              (map? merchant) + 089              (map? merchant)
- 082              merchant) + 090              merchant) +
+ + 091          k (or (:known-prices m) {})
- 083          k (:known-prices m) + 092          l (:location m)
- - 084          l (:location m) -
- - 085          d (:date world) + + 093          d (or (:date world) 0)
- 086          p (-> world :cities l :prices)] -
- - 087      (reduce -
- - 088        merge -
- - 089        k + 094          p (-> world :cities l :prices)]
- 090        (map + 095      (cond +
+ + 096        (nil? m) +
+ + 097        (throw (Exception. "No merchant?")) +
+ + 098        (or (nil? l) (nil? (-> world :cities l))) +
+ + 099        (throw (Exception. (str "No known location for merchant " m))) +
+ + 100        :else +
+ + 101        (reduce +
+ + 102          merge +
+ + 103          k +
+ + 104          (map
- 091          #(hash-map % (apply vector cons {:price (p %) :date d} (k %))) + 105            #(hash-map % (apply vector cons {:price (p %) :date d} (k %)))
- 092          (-> world :commodities keys))))) + 106            (-> world :commodities keys))))))
diff --git a/docs/index.html b/docs/index.html index 35656b5..044b871 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,8 +6,8 @@

The Great Game: Dcocumentation

diff --git a/project.clj b/project.clj index ff35841..67a2163 100644 --- a/project.clj +++ b/project.clj @@ -4,6 +4,7 @@ :doc/format :markdown} :output-path "docs/codox" :source-uri "https://github.com/simon-brooke/the-great-game/blob/master/{filepath}#L{line}"} + :cucumber-feature-paths ["test/features/"] :dependencies [[org.clojure/clojure "1.8.0"] [environ "1.1.0"] [com.taoensso/timbre "4.10.0"]] @@ -11,7 +12,9 @@ :license {:name "GNU General Public License,version 2.0 or (at your option) any later version" :url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"} :plugins [[lein-cloverage "1.1.1"] - [lein-codox "0.10.7"]] + [lein-codox "0.10.7"] + [lein-cucumber "1.0.2"] + [lein-gorilla "0.4.0"]] :release-tasks [["vcs" "assert-committed"] ["change" "version" "leiningen.release/bump-version" "release"] diff --git a/src/the_great_game/core.clj b/src/the_great_game/core.clj deleted file mode 100644 index 7a102ed..0000000 --- a/src/the_great_game/core.clj +++ /dev/null @@ -1,6 +0,0 @@ -(ns the-great-game.core) - -(defn foo - "I don't do a whole lot." - [x] - (println x "Hello, World!")) diff --git a/src/the_great_game/merchants/merchant_utils.clj b/src/the_great_game/merchants/merchant_utils.clj index b8cd969..9cfb5b4 100644 --- a/src/the_great_game/merchants/merchant_utils.clj +++ b/src/the_great_game/merchants/merchant_utils.clj @@ -23,7 +23,7 @@ (-> world :merchants merchant) (map? merchant) merchant) - cargo (:stock m)] + cargo (or (:stock m) {})] (reduce + 0 @@ -41,9 +41,11 @@ (-> world :merchants merchant) (map? merchant) merchant)] - (quot - (- (:capacity m) (burden m world)) - (-> world :commodities commodity :weight)))) + (max + 0 + (quot + (- (or (:capacity m) 0) (burden m world)) + (-> world :commodities commodity :weight))))) (defn can-afford "Return the number of units of this `commodity` which this `merchant` @@ -55,9 +57,15 @@ (map? merchant) merchant) l (:location m)] - (quot - (:cash m) - (-> world :cities l :prices commodity)))) + (cond + (nil? m) + (throw (Exception. "No merchant?")) + (or (nil? l) (nil? (-> world :cities l))) + (throw (Exception. (str "No known location for merchant " m))) + :else + (quot + (:cash m) + (-> world :cities l :prices commodity))))) (defn add-stock "Where `a` and `b` are both maps all of whose values are numbers, return @@ -73,20 +81,26 @@ (defn add-known-prices "Add the current prices at this `merchant`'s location in the `world` - to a new cacke of known prices, and return it." + to a new cache of known prices, and return it." [merchant world] (let [m (cond (keyword? merchant) (-> world :merchants merchant) (map? merchant) merchant) - k (:known-prices m) + k (or (:known-prices m) {}) l (:location m) - d (:date world) + d (or (:date world) 0) p (-> world :cities l :prices)] - (reduce - merge - k - (map - #(hash-map % (apply vector cons {:price (p %) :date d} (k %))) - (-> world :commodities keys))))) + (cond + (nil? m) + (throw (Exception. "No merchant?")) + (or (nil? l) (nil? (-> world :cities l))) + (throw (Exception. (str "No known location for merchant " m))) + :else + (reduce + merge + k + (map + #(hash-map % (apply vector cons {:price (p %) :date d} (k %))) + (-> world :commodities keys)))))) diff --git a/test/the_great_game/merchants/markets_test.clj b/test/the_great_game/merchants/markets_test.clj index bc3f515..7cacd30 100644 --- a/test/the_great_game/merchants/markets_test.clj +++ b/test/the_great_game/merchants/markets_test.clj @@ -79,8 +79,31 @@ (-> actual :cities :falkirk :stock :iron) 17) "Stock should be topped up by the difference between the supply and - the demand amount.")) + the demand amount.")))) - ) - - ) +(deftest run-test + (let [world (deep-merge + default-world + {:cities + {:aberdeen + {:stock {:fish 5} + :supplies {:fish 12} + :prices {:fish 1.1}} + :falkirk + {:stock {:iron 10} + :demands {:iron 5} + :supplies {:iron 12} + :prices {:iron 1.1}}}}) + actual (run world)] + (is + (= + (-> actual :cities :aberdeen :stock :fish) + (+ (-> world :cities :aberdeen :supplies :fish) + (-> world :cities :aberdeen :stock :fish))) + "If stock is not empty and price is above cost, stock should be topped up by supply amount.") + (is + (= + (-> actual :cities :falkirk :stock :iron) + 17) + "Stock should be topped up by the difference between the supply and + the demand amount."))) diff --git a/test/the_great_game/merchants/merchant_utils_test.clj b/test/the_great_game/merchants/merchant_utils_test.clj index 2f9071e..086c691 100644 --- a/test/the_great_game/merchants/merchant_utils_test.clj +++ b/test/the_great_game/merchants/merchant_utils_test.clj @@ -22,3 +22,106 @@ expected 1.7] ;; (is (= actual expected) "if information select the most recent"))))) +(deftest burden-test + (testing "Burden of merchant" + (let [world (deep-merge + default-world + {:merchants + {:archie + {:stock + {:iron 1}} + :belinda + {:stock + {:fish 2}} + :callum + {:stock + {:iron 1 + :fish 1}}}})] + (let [actual (burden :archie world) + expected (-> world :commodities :iron :weight)] + (is (= actual expected))) + (let [actual (burden :belinda world) + expected (* 2 (-> world :commodities :fish :weight))] + (is (= actual expected))) + (let [actual (burden :callum world) + expected (+ + (-> world :commodities :iron :weight) + (-> world :commodities :fish :weight))] + (is (= actual expected))) + (let [actual (burden {} world) + expected 0] + (is (= actual expected))) + (let [actual (burden (-> world :merchants :deidre) world) + expected 0] + (is (= actual expected)))))) + +(deftest can-carry-test + (testing "What merchants can carry" + (let [world (deep-merge + default-world + {:merchants + {:archie + {:cash 5 + :stock + {:iron 1}} + :belinda + {:stock + {:fish 2}} + :callum + {:stock + {:iron 1 + :fish 1}}}})] + (let [actual (can-carry :archie world :fish) + expected 0] + (is (= actual expected))) + (let [actual (can-carry :belinda world :fish) + expected 8] + (is (= actual expected))) + (let [actual (can-carry (-> world :merchants :archie) world :fish) + expected 0] + (is (= actual expected))) + (let [actual (can-carry {:stock {:fish 7} :capacity 10} world :fish) + expected 3] + (is (= actual expected)))))) + + +(deftest affordability-test + (testing "What merchants can afford to purchase" + (let [world (deep-merge + default-world + {:merchants + {:archie + {:cash 5 + :stock + {:iron 1}} + :belinda + {:stock + {:fish 2}} + :callum + {:stock + {:iron 1 + :fish 1}}}})] + (let [actual (can-afford :archie world :fish) + expected 5] + (is (= actual expected))) + (let [actual (can-afford :belinda world :fish) + expected 100] + (is (= actual expected))) + (let [actual (can-afford (-> world :merchants :archie) world :fish) + expected 5] + (is (= actual expected))) + (let [actual (can-afford {:cash 3 :location :buckie} world :fish) + expected 3] + (is (= actual expected))) + (is (thrown-with-msg? + Exception + #"No merchant?" + (can-afford :no-one world :fish))) + (is (thrown-with-msg? + Exception + #"No known location for merchant.*" + (can-afford {:cash 3} world :fish)))))) + +(deftest add-stock-test + (let [actual (add-stock {:iron 2 :fish 5} {:fish 3 :whisky 7}) + expected {:iron 2 :fish 8 :whisky 7}]))