From 7c234b6a6e038bbe95d4e4ea65822208a2835363 Mon Sep 17 00:00:00 2001 From: Vladislav Slobodenyuk Date: Thu, 29 Jan 2026 12:50:03 +0900 Subject: [PATCH] Update font, add zpg.el --- .config.d/05-basic.el | 11 +- custom.el | 5 +- load/zpg.el | 754 ++++++++++++++++++++++++++++++++++++++++++ load/zpg.elc | Bin 0 -> 25275 bytes 4 files changed, 766 insertions(+), 4 deletions(-) create mode 100644 load/zpg.el create mode 100644 load/zpg.elc 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 0000000000000000000000000000000000000000..acaf1c3d9a8f4599c95648e472a2527d7967dfe9 GIT binary patch literal 25275 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 oXK+U|_b}&GPP)