Update font, add zpg.el

This commit is contained in:
2026-01-29 12:50:03 +09:00
parent cbd72df6af
commit 7c234b6a6e
4 changed files with 766 additions and 4 deletions

754
load/zpg.el Normal file
View 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