diff --git a/.config.d/05-basic.el b/.config.d/05-basic.el index fb57729..847d79e 100644 --- a/.config.d/05-basic.el +++ b/.config.d/05-basic.el @@ -1,7 +1,7 @@ ;;; 05-basic.el --- Setup some basics. -*- no-byte-compile: t; lexical-binding: t; -*- ;; Setup the font -(set-face-attribute 'default nil :height 120 :font "Iosevka Nerd Font" :weight 'normal) +(set-face-attribute 'default nil :height 110 :font "Iosevka Nerd Font" :weight 'normal) ;; I use narrowing, I am a mature emacs user! I do not need the hand-holding ;; (usually). @@ -121,9 +121,14 @@ (unless (memq 'vterm features) (message "Failed to load vterm. It is probably not installed."))) -;; -(add-to-list 'load-path (expand-file-name (concat minimal-emacs-user-directory "load"))) +;; This folder contains some custom libraries I wrote that could be loaded. +(add-to-list + 'load-path + (expand-file-name + (concat minimal-emacs-user-directory "load"))) +;; Add my input method to allow reverse-im to work properly with my keyboard +;; setup. It is defined in a library in the load directory, see above. (register-input-method "cyrillic-workman" "Russian" 'quail-use-package "RUW@" "Russian (ЙЦУКЕН) input method simulating Workman keyboard" diff --git a/custom.el b/custom.el index 223d90f..1c69909 100644 --- a/custom.el +++ b/custom.el @@ -3,7 +3,10 @@ ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. - ) + '(safe-local-variable-values + '((eval add-to-list 'imenu-generic-expression + '("Variables" + "^\\s-*(\\(defvar-zpg\\)\\s-+\\(\\(?:\\w\\|\\s_\\|\\\\.\\)+\\)" 2))))) (custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. diff --git a/load/zpg.el b/load/zpg.el new file mode 100644 index 0000000..30e24e6 --- /dev/null +++ b/load/zpg.el @@ -0,0 +1,754 @@ +;;; zpg.el --- A zero player game -*- lexical-binding: t; -*- + +;; Author: Dizzar +;; Version: 0.0.0 +;; Package-Requires: () +;; Homepage: +;; Keywords: game + +;; This file is not part of GNU Emacs + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; A zero player game inspired by Godville. + +;;; Code: + +(defvar zpg-loadables '() + "Alist containing the keys and symbols whose values will we saved and +loaded. See `defvar-zpg'.") + +(defmacro defvar-zpg (symbol &optional initvalue docstring &rest props) + "Acts exactly like the normal `defvar', but accepts additional properties +that may influence how the variable is defined. Currently supported +properties are: + +:save - the variable will be added to `zpg-loadables' and thus, will be +saved and loaded." + (declare (indent defun) (doc-string 3)) + (let ((new-symbol (make-symbol "new-symbol")) + (exps '())) + (if (plist-get props :save) + (setq exps (cons `(let ((,new-symbol (intern ,(concat ":" (symbol-name symbol))))) + (add-to-list 'zpg-loadables (cons ,new-symbol ',symbol))) + exps))) + (setq exps (cons `(defvar ,symbol ,initvalue ,docstring) exps)) + (macroexp-progn exps))) + +(defvar zpg-step-timeout 5 + "Once active, zpg mode will do a step once every zpg-step-timeout +seconds.") + +(defvar zpg--step-timer nil + "The timer object that does the stepping.") + +(defvar zpg-log-size 10 + "How many log messages are kept.") + +(defvar-zpg zpg-player-health 100 + "Current health of the players character. Cannot exceed tho Maximum +health defined in `zpg-player-max-health'." :save t) + +(defvar-zpg zpg-player-max-health 100 + "Maximum health of the players character. See `zpg-player-health' for the +current health." :save t) + +(defvar zpg-save-location (concat user-emacs-directory "zpg-save.eld") + "Location in which to store the save data of the mode.") + +(defvar-zpg zpg-statistics-kills 0 + "How many enemies were killed." :save t) + +(defvar-zpg zpg-statistics-death 0 + "How many times you died." :save t) + +(defvar-zpg zpg-statistics-steps 0 + "How many steps were done." :save t) + +(defvar-zpg zpg-player-position 0 + "Current position of the player in distance from the capital. Yes, the +world is one dimensional. Deal with it." :save t) + +(defvar-zpg zpg-player-gold 0 + "The amount of gold the player has in their inventory." :save t) + +(defvar-zpg zpg-player-exp 0 + "The amount of experience the player has. See `zpg-player-level' and +`zpg-level-up-xp' for details on how the amount of experience needed for a +level-up is calculated." :save t) + +(defvar-zpg zpg-player-level 1 + "The current level of the player. There is no variable that holds how +much experience is needed to level up, instead, see `zpg-level-up-xp'." :save t) + +(defvar-zpg zpg-player-inventory '() + "An alist representing the players inventory in the form of (NAME . +PROPS). NAME should be a string. + +PROPS is a property list that, currently, only supports one key: + +:rarity - The rarity of the item. Should be one of ('normal 'rare)." :save t) + +(defvar-zpg zpg-player-inventory-size 20 + "Defines how many items can the players inventory contain." :save t) + +(defvar-zpg zpg-log '() + "Stores the log history that is rendered." :save t) + +(defun zpg-add-log (entry &rest args) + "Formats the ENTRY using `format', passing ARGS to it, and adds the +result to the log." + (if (not (stringp entry)) + (error "Expected string, got: %s" entry) + (setq zpg-log + (take + zpg-log-size + (cons + (apply 'format entry args) + zpg-log))))) + +(defvar zpg-cities + '(("Capital" . (:pos 0 :modifiers ())) + ("El Citiano" . (:pos 13)) + ("Los Citios" . (:pos 25)) + ("New City" . (:pos 31)) + ("Town Town" . (:pos 48)) + ("New Town Town" . (:pos 65)) + ("City of Peoples" . (:pos 71)) + ("Town of Rivers" . (:pos 87)) + ("Chinatown" . (:pos 100)) + ("Fartown" . (:pos 148)) + ("Peoples respite" . (:pos 201)) + ("Winlace city" . (:pos 242)) + ("Solace" . (:pos 297))) + "An alist containing all the cities in the game in the form of (NAME . +PROPS). PROPS is a plist containing the properties of the city. It +supports the following keys: + :pos - The position of the city, an integer. + :modifiers - A list of modifiers for the city behaviour, each +represented by an atom. See below for supported modifiers. + +The following modifiers are supported: + ()") + +(defvar-zpg zpg-current-task '(none) + "A list representing the current task and its state, in the form (TASK . +STATE). + +STATE is a plist that supports the following keys: + +:progress - current progress. +:finish-progress - how much progress to finish the task. +:progress-point - indicates a point, upon reaching which the player will +get 10% of quest progress." :save t) + +(defvar-zpg zpg-game-state (cons 'in-town ()) + "Represents the current state of the game world using a cons cell of the +form (STATE . VALUES). STATE is an atom representing the name of the +current state. VALUES is a list representing the current states information, which is +dependent on the current state. + +Depending on STATE, a handler defined in `zpg-state-handlers' is called +each game step (see `zpg-do-game-step')." :save t) + +(defmacro zpg-random-clause (&rest clauses) + "Randomly select a clause based on its weight. + +Each clause looks like (WEIGHT BODY...). WEIGHT must be either an +integer, or a form that evaluates to one. If two clauses have equal +WEIGHT, they are equally likely to be ran. The higher the WEIGHT is, the +more likely it is that this clause will be ran. + +The probability of each clause being selected is dependent on both its +own WEIGHT and the total sum of all weights of all clauses. The simplest +way to think about this is when your weights add up to 100. In this +case, the WEIGHT of each clause is equal to the percentage chance of it +being ran. + +For example, if we have three clauses with weights 10, 40 and 50 (which +add up to 100), the first clause has a 10% chance of being selected, +second 40%, and the third is 50%. + +More generally, to get the probability of a clause being ran, you need +to divide its WEIGHT by the total weight of all clauses. Applying this +to the previous example: + First clause with WEIGHT of 10: 10/100 = 0.1 (= 10%) + Second clause with WEIGHT of 40: 40/100 = 0.4 (= 40%) + Third clause with WEIGHT of 50: 50/100 = 0.5 (= 50%)" + (let ((random-value (gensym "rnd")) + (weight-bindings '())) + + ;; For each weighted clause, we define a symbol and assign to it a value + ;; of WEIGHT + previous symbol, except for the first clause, which just + ;; gets its WEIGHT assigned directly. + `(let* (,@(dotimes (idx (length clauses) (reverse weight-bindings)) + (let* ((el (car (nthcdr idx clauses))) + (prev-el (caar weight-bindings))) + (setq weight-bindings + (cons `(,(gensym "w") + ,(if prev-el + `(+ ,(car el) ,prev-el) + (car el))) + weight-bindings)))) + (,random-value (random ,(caar weight-bindings)))) + (cond ,@(let ((result '()) + (prev-weight '())) + (dotimes (idx (length weight-bindings) result) + (setq result + (cons + `((< ,random-value + ,(caar (nthcdr idx weight-bindings))) + ,(macroexp-progn + (cdr + (nth (- (1- (length clauses)) idx) + clauses)))) + result)))))))) + +(defmacro zpg-with-state (list &rest body) + "Symbol binds VALUE to the states value and evaluates the BODY. Creates a +check that verifies that the current state is equal to NAME. + +(fn (NAME VALUE) BODY)" + `(if (equal (car zpg-game-state) ',(car list)) + (cl-symbol-macrolet ((,(cadr list) (cdr zpg-game-state))) + ,(macroexp-progn body)) + (error "Expected state %s, found: %s" ',(car list) (car zpg-game-state)))) + +(defun zpg-roll-player-damage () + "Returns the amount of damage the player will deal." + (round (+ 20 + (round + (* + (+ + (log zpg-player-level) + 1) + 8))))) + +(defun zpg-roll-enemy-damage (level) + "Returns the amount of damage the enemy will deal." + (round (+ (* (log level) 3) + 5 + (random (ceiling (+ (log level) 1)))))) + +(defun zpg-town-positions () + "Returns the alist of form ((POSITION . TOWN)..)" + (mapcar (lambda (el) + (cons (plist-get (cdr el) :pos) (car el))) + zpg-cities)) + +(defun zpg-generate-enemy (level &rest args) + "Creates an enemy of the specified LEVEL and returns it. An enemy is +currently represented as a plist with the following properties: + +:name - The name of the enemy; +:health - Their health; +:level - Their level; +:turn - Whose turn it is. + +(fn LEVEL &key NAME TURN)" + (let ((health (+ 100 (* level 2))) + (name (plist-get args :name)) + (turn (plist-get args :turn))) + (list + :name (if name name "Enemy") + :health health + :level level + :turn (if turn turn 'player)))) + +(defun zpg-add-quest-progress (amount) + "Adds the AMOUNT of progress to the current quest, and updates the states +as needed." + (if (equal (car zpg-current-task) 'none) + (error "Attempting to add progress to a task, but there is no current task.")) + (cl-symbol-macrolet ((progress (plist-get (cdr zpg-current-task) :progress))) + (setf progress (+ amount progress)) + (if (>= progress (plist-get (cdr zpg-current-task) :finish-progress)) + (setq zpg-game-state (cons 'retreat (list :target (car zpg-cities))))))) + +(defun zpg-handle-travel () + "Handles the 'traveling' state. See `zpg-game-state'. + +In this state, the players hero moves forward on the map, with a chance +of starting a fight instead." + (zpg-with-state + (traveling sval) + + (cond + ;; Retreat if we feeling bad, aka less than half health left. + ((<= zpg-player-health (/ zpg-player-max-health 2)) + (setq zpg-game-state (cons 'retreat nil)) + (zpg-add-log "Health too low, retreating to the closest town.")) + ((and + (plist-get (cdr zpg-current-task) :progress) + (>= (plist-get (cdr zpg-current-task) :progress) + (plist-get (cdr zpg-current-task) :finish-progress))) + (setq zpg-game-state (cons 'retreat (list :target (car zpg-cities)))) + (zpg-add-log "Continuing to %s" (caar zpg-cities))) + (t (zpg-random-clause + ;; Add quest progress + (5 + (zpg-add-quest-progress (+ 1 (random 2))) + (zpg-add-log "You are one step closer to completing your quest." )) + ;; We fight + (20 + (zpg-add-log "Entering a fight.") + (setq + zpg-game-state + (cons 'fighting + (zpg-generate-enemy (+ zpg-player-level (random 5)))))) + ;; We move forward one space + (75 + (setq zpg-player-position (1+ zpg-player-position)))))))) + +(defun zpg-award-exp (amount) + "Adds the specified amount of exp to the experience pool and levels up if +needed." + (cond ((>= (+ amount zpg-player-exp) (zpg-level-up-xp)) + (setq zpg-player-level (1+ zpg-player-level) + zpg-player-exp 0 + zpg-player-max-health (+ 100 (* zpg-player-level 3))) + (zpg-add-log "You leveled up to level %s!" zpg-player-level)) + (t (setq zpg-player-exp (+ amount zpg-player-exp))))) + +(defun zpg-fight-win-reward () + "This function must be called after you win a fight. It gives a reward, +awards EXP, gold and items for this fight, and updates statistics." + (let ((exp-awarded (+ 15 (random 100)))) + (zpg-random-clause + ;; 40% no reward + (40 + (zpg-add-log "Normal fight won, earned %s xp." exp-awarded)) + ;; 30% gold reward + (30 + (let ((reward (+ (random 95) 5))) + (setq zpg-player-gold (+ zpg-player-gold reward)) + (zpg-add-log "Normal fight won, earned gold: %s, and xp: %s." reward exp-awarded))) + ;; 30% item reward ('normal 'rare) + (30 + (let ((rarity (zpg-random-clause (10 'normal) (1 'rare)))) + (if (< (length zpg-player-inventory) zpg-player-inventory-size) + (progn + (setq zpg-player-inventory + (cons + (cons (format "%s item" rarity) (list :rarity rarity)) + zpg-player-inventory)) + (zpg-add-log + "Normal fight won, earned %s exp and a %s item." + exp-awarded rarity)) + (zpg-add-log + "Normal fight won, earned %s exp but lost an item due to full inventory." + exp-awarded)) + ))) + (zpg-award-exp exp-awarded) + (setq zpg-statistics-kills (1+ zpg-statistics-kills)))) + +(defun zpg-handle-fight () + "Handles the 'fighting' state. See `zpg-game-state'. + +In this state, the hero is fighting an enemy." + (zpg-with-state + (fighting state) + (let ((turn (plist-get state :turn)) + (enemy-health (plist-get state :health))) + + ;; Deal damage + (cond ((equal turn 'player) + (let ((damage (zpg-roll-player-damage))) + (setq state + (plist-put state :health + (- enemy-health damage))) + (setq state + (plist-put state :turn 'enemy)) + ;; TODO: incorporate into the status string somehow? + ;; same for the next thing + ;; (zpg-add-log + ;; "Dealt %s damage to enemy. They have %s health left." + ;; damage + ;; (plist-get state :health)) + )) + ((equal turn 'enemy) + (let ((damage (zpg-roll-enemy-damage (plist-get state :level)))) + (setq zpg-player-health (- zpg-player-health damage)) + (setq state + (plist-put state :turn 'player)) + ;; (zpg-add-log + ;; "Enemy dealt you %s damage. %s health remaining." + ;; damage + ;; zpg-player-health) + )) + (t (error "Invalid turn %s" turn))) + + ;; Check death conditions + (cond ((<= zpg-player-health 0) + (setq zpg-game-state (cons 'dead nil) + zpg-statistics-death (1+ zpg-statistics-death)) + (zpg-add-log "You died.")) + ((<= (plist-get state :health) 0) + (zpg-fight-win-reward) + (setq zpg-game-state (cons 'traveling nil))))))) + +(defun zpg-handle-retreat () + "Handles the 'retreat' state. See `zpg-game-state'. + +This state is entered when the hero has low health, it is very much like +the 'traveling' state, except the chances of a fight are low and the +movement is backwards." + (zpg-with-state + (retreat state) + ;; If we now in town + (let* ((towns (sort (zpg-town-positions) :key 'car)) + (town (alist-get (max zpg-player-position (caar towns)) towns)) + (target (plist-get state :target))) + (if (or + (and town (not target)) + (and town target (equal town target)) + (equal town (caar zpg-cities))) + (progn + (setq + zpg-game-state + (cons 'healing (list :town town))) + (zpg-add-log "Entering the town of %s to heal." town)) + + (zpg-random-clause + (95 + (setq zpg-player-position (1- zpg-player-position))) + (5 + (zpg-add-log "Entering a fight during a retreat.") + (setq + zpg-game-state + (cons 'fighting + (list :name "Enemy" :health 100 :level 1 :turn 'enemy))))))))) + +(defun zpg-handle-healing () + "Handles the 'healing' state. See `zpg-game-state'. + +This state means the players hero is in a town and is slowly healing." + (zpg-with-state + (healing state) + (let ((healing (+ 10 (random 20)))) + (if (>= (+ zpg-player-health healing) zpg-player-max-health) + (progn + (setq zpg-player-health zpg-player-max-health) + (setq zpg-game-state (cons 'in-town nil)) + (zpg-add-log "Fully healed up.")) + (progn + (setq zpg-player-health (+ zpg-player-health healing))))))) + +(defun zpg-handle-xp-donation () + "Handles the 'xp-donation' state. See `zpg-game-state'. + +This state is entered when the hero has 20000 gold by the end of +healing, and so the hero sacrifices them for 5% of level-up experience." + (zpg-with-state + (xp-donation state) + (setq zpg-player-gold (- zpg-player-gold 20000) + zpg-game-state (cons 'in-town '(:no-donation t))) + (zpg-award-exp (/ (zpg-level-up-xp) 20)) + (zpg-add-log "You have sacrificed 20 gold pouches for experience. "))) + +(defun zpg-handle-in-town () + "Handles the 'in-town' state. See `zpg-game-state'." + (zpg-with-state + (in-town state) + ;; Donate, but only once + (cond ((and (>= zpg-player-gold 20000) (not (plist-get state :no-donation))) + (setq zpg-game-state (cons 'xp-donation nil))) + ;; Sell stuff if we have it + ((> (length zpg-player-inventory) 0) + (setq zpg-game-state (cons 'trading nil))) + ;; quest completed. + ((and + (equal (zpg-closest-town zpg-player-position) (caar zpg-cities)) + (plist-get (cdr zpg-current-task) :progress) + (>= (plist-get (cdr zpg-current-task) :progress) + (plist-get (cdr zpg-current-task) :finish-progress))) + (let ((new-gold (cond + ((equal (car zpg-current-task) 'normal) (+ 4000 (random 2000))) + ((equal (car zpg-current-task) 'epic) (+ 40000 (random 20000)))))) + (setq zpg-player-gold (+ zpg-player-gold new-gold)) + (zpg-add-log "Quest complete! Earned %s gold." new-gold) + (setq zpg-current-task '('none)))) + ((not (plist-get (cdr zpg-current-task) :progress)) + ;; TODO: progress-point + (zpg-random-clause + (50 (setq zpg-current-task (cons 'normal (list :progress 0 :finish-progress 100 :progress-point 199)))) + (1 (setq zpg-current-task (cons 'epic (list :progress 0 :finish-progress 1000 :progress-point 199))))) + (zpg-add-log "Got new quest.")) + ;; if we're broke, no items, maybe revel and go out. + (t + (if (> (random 5) 2) + (let* ((tenth (/ zpg-player-gold 10)) + (gold-spent (+ (* tenth 4) (random (max tenth 1))))) + (setq zpg-player-gold (- zpg-player-gold gold-spent) + zpg-game-state (cons 'traveling nil)) + (zpg-add-log "You spent %s gold in town" gold-spent))) + (setq zpg-game-state (cons 'traveling nil)) + (zpg-add-log "Heading out."))))) + +(defun zpg-list-windows (seq window-length fn) + "Applies FN to overlapping subsequences of SEQ of length WINDOW-LENGTH, +and makes a list of the results." + (let ((window '()) + (result '()) + (innerseq (reverse seq))) + + (dotimes (_ window-length) + (setq window (cons (car innerseq) window)) + (setq innerseq (cdr innerseq))) + (setq result (cons (apply fn window) result)) + + (while innerseq + (setf (cdr (last window 2)) nil) ; Remove the last element + (setq window (cons (car innerseq) window) + innerseq (cdr innerseq) + result (cons (apply fn window) result))) + + result)) + +(defun zpg-closest-town (point) + "Returns the town closest to point. A specialized version of `zpg-list-windows'." + (let* ((window '()) + (result '()) + (seq (zpg-town-positions)) + (window-length 2) + (fn (lambda (first second) + (if (and (>= point (car first)) (<= point (car second))) + (if (<= (- point (car first)) (- (car second) point)) + (cdr first) + (cdr second))))) + (innerseq (reverse seq))) + + (let + ((extraresult + (catch :finished + ;; compare with seq because we want the smallest + (if (<= point (caar seq)) + (throw :finished (cdar seq))) + + ;; and now the biggest + (if (>= point (caar innerseq)) + (throw :finished (cdar innerseq))) + + (dotimes (_ window-length) + (setq window (cons (car innerseq) window)) + (setq innerseq (cdr innerseq))) + (setq result (apply fn window)) + + (while (and innerseq (not result)) + (setf (cdr (last window 2)) nil) ; Remove the last element + (setq window (cons (car innerseq) window) + innerseq (cdr innerseq) + result (apply fn window)))))) + (if extraresult + (setq result extraresult))) + + result)) + +(defun zpg-handle-trading () + "Handles the 'trading' state. See `zpg-game-state'. + +In this state, we try to sell all our items." + (zpg-with-state + (trading state) + (let ((item (car zpg-player-inventory))) + (if item + (progn + (setq zpg-player-inventory (cdr zpg-player-inventory) + zpg-player-gold + (+ zpg-player-gold + (let* ((rarity (plist-get (cdr item) :rarity)) + (base-reward (+ 50 (- 100 (random 115)))) + (reward + (cond + ((equal rarity 'rare) + (+ (random 10) (* base-reward 10))) + (t base-reward)))) + (zpg-add-log "You sold %s for %s gold." (car item) reward) + reward)))) + (progn + (setq zpg-game-state (cons 'in-town '(:no-donation t)))))))) + +(defun zpg-handle-death () + "Handles the 'dead' state. See `zpg-game-state'." + (zpg-with-state + (dead state) + (let ((closest-town (zpg-closest-town zpg-player-position))) + (setq zpg-player-position + (plist-get + (alist-get closest-town zpg-cities) :pos) + zpg-game-state (cons 'traveling nil)) + (zpg-add-log "Respawning at %s" closest-town)))) + +(defvar zpg-state-handlers '((traveling . zpg-handle-travel) + (fighting . zpg-handle-fight) + (retreat . zpg-handle-retreat) + (healing . zpg-handle-healing) + (dead . zpg-handle-death) + (in-town . zpg-handle-in-town) + (xp-donation . zpg-handle-xp-donation) + (trading . zpg-handle-trading)) + "Alist that maps the game states to their respective handlers. See +`zpg-game-state'.") + +(defun zpg-level-up-xp () + "Calculates how much experience is needed for a level-up. See +`zpg-player-level' and `zpg-player-exp'. + +Currently, to level up you simply need the amount of experience equal to +the cube of the level." + (* zpg-player-level zpg-player-level zpg-player-level)) + +(defun zpg-do-game-step (&optional skip-rendering) + "Performs an actual step of the game simulation. + +Each step, the current state (`zpg-game-state') is mapped to its +handler (`zpg-state-handlers'), which is then called. The handler is +what performs the actual stepping. + +If in zpg-mode buffer, re-renders it after the step is done, unless +SKIP-RENDERING is non-nil." + (interactive) + (let ((current-state (car zpg-game-state))) + (funcall (alist-get current-state zpg-state-handlers))) + (if (and (equal major-mode 'zpg-mode) (not skip-rendering)) + (zpg-render)) + (setq zpg-statistics-steps (1+ zpg-statistics-steps))) + +(defun zpg-create-status-string () + "Returns a string that describes the current state of the player for +rendering in the UI." + (let ((state-name (car zpg-game-state)) + (state-value (cdr zpg-game-state))) + (cond ((equal state-name 'traveling) "You are currently traveling.\n") + ((equal state-name 'retreat) "You are currently retreating.\n") + ((equal state-name 'dead) "You are dead.\n") + ((equal state-name 'healing) + (format "You are currently healing in %s.\n" + (plist-get state-value :town))) + ((equal state-name 'fighting) + (format "You are currently fighting against %s. \nThey have %s health remaining." + (plist-get state-value :name) + (plist-get state-value :health))) + ((equal state-name 'trading) + (format "You are trading in %s\n" + (zpg-closest-town zpg-player-position))) + ((equal state-name 'in-town) + (format "You are staying in %s\n" + (zpg-closest-town zpg-player-position))) + (t "")))) + +(defun zpg-render (&rest args) + "Clears and renders the ZPG-mode buffer. Arguments are ignored." + (if (not (equal major-mode 'zpg-mode)) + (error "Must be called only in zpg-mode buffers.")) + + (let ((inhibit-read-only t)) + (erase-buffer) + (insert + (format "Health: %s / %s\n" + zpg-player-health + zpg-player-max-health)) + (insert (format "Position: %s\n" zpg-player-position)) + (insert (format "Level: %s\n" zpg-player-level)) + (insert (format "EXP: %s\n" zpg-player-exp)) + (insert (format "Gold: %s\n" zpg-player-gold)) + (insert + (format "Current quest: %s. Progress: %s / %s\n" + (car zpg-current-task) + (plist-get (cdr zpg-current-task) :progress) + (plist-get (cdr zpg-current-task) :finish-progress))) + (insert (zpg-create-status-string) "\n\n") + (insert "Inventory:\n") + (dolist (i zpg-player-inventory) + (insert (format "%s\n" (car i)))) + (insert "\nHere is the current log lol: \n") + (dolist (log (take zpg-log-size zpg-log)) + (insert log "\n")) + (insert "\nSome stats for you:\n") + (insert (format "In total, you killed %s enemies.\n" zpg-statistics-kills)) + (insert (format "In total, you died %s times.\n" zpg-statistics-death)) + (insert (format "A total of %s steps were simulated.\n" zpg-statistics-steps))) + (goto-char (point-min))) + +(defun zpg-fast-forward (steps) + "Performs the specified amount of steps of the game." + (interactive "nHow many steps to fast-forward? ") + (dotimes (i steps) + (zpg-do-game-step t)) + (zpg-render)) + +(defun zpg-toggle-autostep () + "Starts or stops the auto stepping timer. See `zpg-step-timeout'." + (interactive) + (if zpg--step-timer + (progn + (cancel-timer zpg--step-timer) + (setq zpg--step-timer nil) + (message "Disabled auto-stepping.")) + (progn + (setq zpg--step-timer + (run-at-time t zpg-step-timeout 'zpg-do-game-step)) + (message "Enabled auto-stepping.")))) + +(defun zpg-save () + "Saves the current game in `zpg-save-location'. See `zpg-loadables'." + (interactive) + (let ((data '())) + (dolist (item zpg-loadables) + (setq data (cons (list (car item) (symbol-value (cdr item))) data))) + (with-temp-file zpg-save-location + (prin1 data (current-buffer)))) + (message (format "Saved to %s" zpg-save-location))) + +(defun zpg-load () + "Loads the game saved in `zpg-save-location'. See `zpg-loadables'." + (let ((data (with-temp-buffer + (insert-file-contents zpg-save-location) + (read (current-buffer))))) + (dolist (item data) + (set (alist-get (car item) zpg-loadables) (cadr item))))) + +(defvar zpg-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map special-mode-map) + (define-key map (kbd "s") 'zpg-do-game-step) + (define-key map (kbd "S") 'zpg-fast-forward) + (define-key map (kbd "t") 'zpg-toggle-autostep) + map) + "Keymap for `zpg-mode'.") + +(define-derived-mode zpg-mode special-mode + "ZPG" + "A zero player game in Emacs." + :interactive nil + (setq-local revert-buffer-function 'zpg-render) + (add-hook 'window-selection-change-functions 'zpg-render nil t) + (zpg-render)) + +(defun zpg () + "Starts the zpg-mode buffer." + (interactive) + (let ((buffer (get-buffer-create "*zpg*"))) + (pop-to-buffer-same-window buffer) + (zpg-mode))) + +(provide 'zpg) + +;; Local Variables: +;; eval: (add-to-list 'imenu-generic-expression '("Variables" "^\\s-*(\\(defvar-zpg\\)\\s-+\\(\\(?:\\w\\|\\s_\\|\\\\.\\)+\\)" 2)) +;; End: + +;;; zpg.el ends here diff --git a/load/zpg.elc b/load/zpg.elc new file mode 100644 index 0000000..acaf1c3 Binary files /dev/null and b/load/zpg.elc differ