Update font, add zpg.el
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
;;; 05-basic.el --- Setup some basics. -*- no-byte-compile: t; lexical-binding: t; -*-
|
;;; 05-basic.el --- Setup some basics. -*- no-byte-compile: t; lexical-binding: t; -*-
|
||||||
|
|
||||||
;; Setup the font
|
;; 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
|
;; I use narrowing, I am a mature emacs user! I do not need the hand-holding
|
||||||
;; (usually).
|
;; (usually).
|
||||||
@@ -121,9 +121,14 @@
|
|||||||
(unless (memq 'vterm features)
|
(unless (memq 'vterm features)
|
||||||
(message "Failed to load vterm. It is probably not installed.")))
|
(message "Failed to load vterm. It is probably not installed.")))
|
||||||
|
|
||||||
;;
|
;; 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-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
|
(register-input-method
|
||||||
"cyrillic-workman" "Russian" 'quail-use-package
|
"cyrillic-workman" "Russian" 'quail-use-package
|
||||||
"RUW@" "Russian (ЙЦУКЕН) input method simulating Workman keyboard"
|
"RUW@" "Russian (ЙЦУКЕН) input method simulating Workman keyboard"
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
;; If you edit it by hand, you could mess it up, so be careful.
|
;; If you edit it by hand, you could mess it up, so be careful.
|
||||||
;; Your init file should contain only one such instance.
|
;; Your init file should contain only one such instance.
|
||||||
;; If there is more than one, they won't work right.
|
;; 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
|
||||||
;; custom-set-faces was added by Custom.
|
;; custom-set-faces was added by Custom.
|
||||||
;; If you edit it by hand, you could mess it up, so be careful.
|
;; If you edit it by hand, you could mess it up, so be careful.
|
||||||
|
|||||||
754
load/zpg.el
Normal file
754
load/zpg.el
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; 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
|
||||||
BIN
load/zpg.elc
Normal file
BIN
load/zpg.elc
Normal file
Binary file not shown.
Reference in New Issue
Block a user