(defgroup decimation nil
"Decimation game."
:prefix "decimation-"
:group 'games)
(defcustom decimation-buffer-name
"*Decimation*"
"Buffer name for game output."
:type 'string
:group 'decimation)
(defcustom decimation-help-buffer-name
"*Decimation Help*"
"Buffer name for game help."
:type 'string
:group 'decimation)
(defcustom decimation-grid-width
20
"Number of X squares in game grid."
:type 'number
:group 'decimation)
(defcustom decimation-grid-height
20
"Number of Y squares in game grid."
:type 'number
:group 'decimation)
(defcustom decimation-monsters
9
"Number of monsters to start with."
:type 'number
:group 'decimation)
(defcustom decimation-hero-rationality
100
"Number of rationality points (hit points) the hero starts with."
:type 'number
:group 'decimation)
(defcustom decimation-hero-moves
10
"Number of turns before the hero gets to move."
:type 'number
:group 'decimation)
(defvar decimation-local-hero)
(defvar decimation-local-hero-max-rationality)
(defvar decimation-local-hero-moves)
(defvar decimation-local-grid)
(defvar decimation-local-grid-buffer-pos)
(defvar decimation-local-grid-height)
(defvar decimation-local-grid-offset-height)
(defvar decimation-local-grid-offset-width)
(defvar decimation-local-grid-size)
(defvar decimation-local-grid-space)
(defvar decimation-local-grid-width)
(defvar decimation-local-monsters)
(defvar decimation-local-turn)
(defconst decimation-grid-floor-char ?.)
(defconst decimation-grid-hero-char ?0)
(defconst decimation-face-floor nil)
(defconst decimation-face-hero 'font-lock-function-name-face)
(defconst decimation-face-monster 'font-lock-constant-face)
(defconst decimation-face-monster-can-see 'font-lock-comment-face)
(defconst decimation-face-banner 'font-lock-keyword-face)
(defvar decimation-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [up] (lambda () (interactive) (decimation-hero-move 0 -1)))
(define-key map [down] (lambda () (interactive) (decimation-hero-move 0 1)))
(define-key map [left] (lambda () (interactive) (decimation-hero-move -1 0)))
(define-key map [right] (lambda () (interactive) (decimation-hero-move 1 0)))
(define-key map "1" (lambda () (interactive) (decimation-hero-move -1 1)))
(define-key map "2" (lambda () (interactive) (decimation-hero-move 0 1)))
(define-key map "3" (lambda () (interactive) (decimation-hero-move 1 1)))
(define-key map "4" (lambda () (interactive) (decimation-hero-move -1 0)))
(define-key map "5" (lambda () (interactive) (decimation-hero-move 0 0)))
(define-key map "6" (lambda () (interactive) (decimation-hero-move 1 0)))
(define-key map "7" (lambda () (interactive) (decimation-hero-move -1 -1)))
(define-key map "8" (lambda () (interactive) (decimation-hero-move 0 -1)))
(define-key map "9" (lambda () (interactive) (decimation-hero-move 1 -1)))
(define-key map "+" #'decimation-hero-addition)
(define-key map "-" #'decimation-hero-subtraction)
(define-key map "*" #'decimation-hero-multiplication)
(define-key map "/" #'decimation-hero-devision)
(define-key map "h" #'decimation-help)
(define-key map "n" #'decimation)
(define-key map "q" (lambda () (interactive) (kill-buffer nil)))
map))
(defun decimation-mode ()
"Major mode for playing Decimation."
(kill-all-local-variables)
(setq major-mode 'decimation-mode)
(setq mode-name "Decimation")
(use-local-map decimation-mode-map)
(setq buffer-read-only t))
(defun decimation-grid-pos-to-xy (pos)
"Return X/Y coordinates on grid for POS.
\nReturn nil if position is invalid."
(if (or (not pos)
(< pos 0)
(>= pos (length decimation-local-grid)))
nil
(let* ((y (truncate (/ pos decimation-local-grid-width)))
(x (- pos (* y decimation-local-grid-width))))
(list x y))))
(defun decimation-xy-to-grid-pos (x y)
"Return grid position from X/Y coordinates.
\nReturn nil if coordinates are invalid."
(if (or (< x 0)
(< y 0)
(>= x decimation-local-grid-width)
(>= y decimation-local-grid-height))
nil
(+ (* y decimation-local-grid-width) x)))
(defun decimation-draw-char (pos char)
"Insert CHAR at tile position POS on the game grid,
overwriting the old character.
\nColor properties should already be applied to CHAR."
(let* ((inhibit-read-only t))
(goto-char (aref decimation-local-grid-buffer-pos pos))
(delete-char 1)
(insert char)
(goto-char (point-max))))
(defun decimation-digit-to-char (digit)
"Return ASCII character code of DIGIT."
(progn
(assert (numberp digit))
(assert (and (>= digit 0) (<= digit 9)))
(+ digit 48)))
(defun decimation-grid-pos-in-bounds (pos grid)
"Return t if POS is in GRID boundaries, nil otherwise."
(if (or (not pos)
(< pos 0)
(>= pos (length grid)))
nil
t))
(defun decimation-output-status ()
"Output status (hero's rationality and turn counter) to the screen."
(let ((inhibit-read-only t)
(rationality (first decimation-local-hero)))
(when (< rationality 0)
(setq rationality 0))
(goto-char (point-max))
(forward-line -3)
(goto-char (point-at-bol))
(kill-region (point) (point-at-eol))
(indent-to decimation-local-grid-offset-width)
(insert (format "Rationality: %d" rationality))
(forward-line 1)
(goto-char (point-at-bol))
(kill-region (point) (point-at-eol))
(indent-to decimation-local-grid-offset-width)
(insert (format "Turn: %d" (/ decimation-local-turn 10)))
(goto-char (point-min))))
(defun decimation-hero-status ()
"Return t if hero is alive and some monsters are still left, nil otherwise."
(sit-for 0)
(if (and (> (first decimation-local-hero) 0)
(> (length decimation-local-monsters) 0))
t
(let ((inhibit-read-only t))
(goto-char (point-max))
(goto-char (point-at-bol))
(kill-region (point) (point-at-eol))
(indent-to decimation-local-grid-offset-width)
(if (= (length decimation-local-monsters) 0)
(progn
(insert (propertize "YOU WIN" 'face decimation-face-banner))
(message "Hiro has killed all monsters, you win"))
(progn
(insert (propertize "YOU LOOSE" 'face decimation-face-banner))
(message "Hero has died, you loose")))
(insert " (press 'n' to start a new game)")
(goto-char (point-min))
nil)))
(defun decimation-set-monster (monster new-pos new-type can-see)
"Update a specific MONSTER with a new position POS and an optional NEW-TYPE.
\nIf NEW-POS is nil, the monster is removed (killed).
If NEW-TYPE is nil, the monster type is not changed.
If CAN-SEE is t, the monster's color is changed."
(let (new-monsters
(mnum (first monster))
(old-type (second monster))
(old-pos (third monster))
(new-type (or new-type (second monster))))
(dolist (monster decimation-local-monsters)
(if (= (first monster) mnum)
(if new-pos
(progn
(push (list mnum new-type new-pos) new-monsters)
(decimation-draw-char old-pos (propertize
(char-to-string decimation-grid-floor-char)
'face decimation-face-floor))
(if can-see
(decimation-draw-char new-pos (propertize
(char-to-string (decimation-digit-to-char new-type))
'face decimation-face-monster-can-see))
(decimation-draw-char new-pos (propertize
(char-to-string (decimation-digit-to-char new-type))
'face decimation-face-monster))))
(decimation-draw-char old-pos (propertize
(char-to-string decimation-grid-floor-char)
'face decimation-face-floor)))
(push monster new-monsters)))
(setq decimation-local-monsters (nreverse new-monsters))))
(defun decimation-kill-monster (monster)
"Remove MONSTER from monster list and award the hero with some rationality points."
(decimation-set-monster monster nil nil nil)
(let* ((rationality (/ (+ (first decimation-local-hero) decimation-local-hero-max-rationality) 2))
(increase (- rationality (first decimation-local-hero))))
(setq decimation-local-hero (list rationality (second decimation-local-hero)))
(message "You killed a monster and received %s rationality back" increase)))
(defun decimation-initialize-grid (width height monster-count rationality moves)
"Initialize Decimation game grid.
\nWIDTH is the size of the grid in the horizontal, defaults to `decimation-grid-width'.
HEIGHT is the size of the grid in the vertical, defaults to `decimation-grid-height'.
MONSTER-COUNT is the number of random monsters to create, defaults to `decimation-monsters'.
RATIONALITY is the hero's maximum hit points, defaults to `decimation-rationality'."
(let (grid-free)
(setq
decimation-local-grid-width width
decimation-local-grid-height height
decimation-local-grid-size (* width height)
decimation-local-grid (make-vector decimation-local-grid-size decimation-grid-floor-char)
decimation-local-monsters nil
decimation-local-hero nil
decimation-local-hero-max-rationality rationality
decimation-local-hero-moves moves)
(when (< decimation-local-grid-size (1+ monster-count))
(error "Grid size must be large enough to hold the monsters and the hero"))
(dotimes (x decimation-local-grid-size)
(push x grid-free))
(dotimes (x monster-count)
(let ((type (1+ (mod x 9)))
(pos (nth (random (length grid-free)) grid-free)))
(push (list x type pos) decimation-local-monsters)
(aset decimation-local-grid pos (decimation-digit-to-char type))
(delq pos grid-free)))
(setq decimation-local-monsters (nreverse decimation-local-monsters))
(let ((pos (nth (random (length grid-free)) grid-free)))
(setq decimation-local-hero (list rationality pos))
(aset decimation-local-grid pos decimation-grid-hero-char))))
(defun decimation-hero-move (dx dy)
"Move hero DX in the X axis and DY in the Y axis."
(let* ((hero-pos (second decimation-local-hero))
(hero-xy (decimation-grid-pos-to-xy hero-pos))
(new-pos (decimation-xy-to-grid-pos
(+ (first hero-xy) dx)
(+ (second hero-xy) dy))))
(when new-pos
(cond
((not (decimation-hero-status)))
((not (decimation-grid-pos-in-bounds new-pos decimation-local-grid))
(message "Cannot move hero out of bounds"))
((member new-pos (mapcar 'third decimation-local-monsters))
(message "Cannot move hero onto a monster"))
(t
(aset decimation-local-grid hero-pos decimation-grid-floor-char)
(aset decimation-local-grid new-pos decimation-grid-hero-char)
(decimation-draw-char hero-pos (propertize
(char-to-string decimation-grid-floor-char)
'face decimation-face-floor))
(decimation-draw-char new-pos (propertize
(char-to-string decimation-grid-hero-char)
'face decimation-face-hero))))
(setq decimation-local-hero (list (first decimation-local-hero) new-pos))
(decimation-move-monsters))))
(defun decimation-monster-can-see (monster hero-pos)
"Return t if monster can see hero, nil otherwise."
(let ((monster-xy (decimation-grid-pos-to-xy (third monster)))
(hero-xy (decimation-grid-pos-to-xy hero-pos)))
(if (> (max (abs (- (first monster-xy) (first hero-xy)))
(abs (- (second monster-xy) (second hero-xy))))
(second monster))
nil
t)))
(defun decimation-move-monsters ()
"Move monsters according to game logic."
(let ((move decimation-local-hero-moves))
(while (and (> move 0)
(decimation-hero-status))
(setq move (1- move))
(let (moving-monsters
(hero-pos (second decimation-local-hero)))
(dolist (monster decimation-local-monsters)
(when (= (mod decimation-local-turn (second monster)) 0)
(push monster moving-monsters)))
(dolist (monster moving-monsters)
(let ((x (first (decimation-grid-pos-to-xy (third monster))))
(y (second (decimation-grid-pos-to-xy (third monster)))))
(if (decimation-monster-can-see monster hero-pos)
(let* ((monster-xy (decimation-grid-pos-to-xy hero-pos))
(dx (- x (first monster-xy)))
(dy (- y (second monster-xy)))
(nx (cond
((= dx 0) x)
((> dx 0) (1- x))
(t (1+ x))))
(ny (cond
((= dy 0) y)
((> dy 0) (1- y))
(t (1+ y)))))
(decimation-move-monster monster (decimation-xy-to-grid-pos nx ny) t))
(let (dx
dy
pos)
(while (or (not pos)
(not (decimation-grid-pos-in-bounds pos decimation-local-grid))
(and (= dx 0) (= dy 0)))
(setq dx (1- (random 3))
dy (1- (random 3))
pos (decimation-xy-to-grid-pos (+ x dx) (+ y dy))))
(decimation-move-monster monster (decimation-xy-to-grid-pos (+ x dx) (+ y dy)) nil)))))
(setq decimation-local-turn (1+ decimation-local-turn))
(decimation-output-status)
(decimation-hero-status)))))
(defun decimation-move-monster (monster pos can-see)
"Move monster to POS if it is empty. Attack hero if he is at POS.
\nIf CAN-SEE is non-nil, then monster can see hero."
(cond
((= (second decimation-local-hero) pos)
(decimation-set-monster monster (third monster) nil t)
(let* ((type (second monster))
(damage 0))
(dotimes (x type)
(setq damage (+ damage (random type) 1)))
(message "Monster %d hits you for %d damage" type damage)
(setq decimation-local-hero (list (- (first decimation-local-hero) damage) (second decimation-local-hero)))))
((not (member pos (mapcar 'third decimation-local-monsters)))
(decimation-set-monster monster pos nil can-see))))
(defun decimation-monsters-next-to-hero (monsters hero-pos)
"Return list of monsters that are next to the hero."
(let ((monsters-next)
(hero-xy (decimation-grid-pos-to-xy hero-pos)))
(dolist (monster monsters)
(let ((monster-xy (decimation-grid-pos-to-xy (third monster))))
(when (<= (max (abs (- (first monster-xy) (first hero-xy)))
(abs (- (second monster-xy) (second hero-xy))))
1)
(push monster monsters-next))))
(nreverse monsters-next)))
(defun decimation-hero-attack (funct)
"Hero performs an attack, applying FUNCT to monster type."
(when (decimation-hero-status)
(let ((monster-targets (decimation-monsters-next-to-hero decimation-local-monsters (second decimation-local-hero))))
(if (> (length monster-targets) 0)
(progn
(dolist (monster monster-targets)
(let* ((type (second monster))
(pos (third monster))
(new-type (funcall funct type)))
(if (= new-type 0)
(decimation-kill-monster monster)
(decimation-set-monster monster pos new-type t))))
(decimation-output-status)
(decimation-move-monsters))
(message "There are no monsters close enough to attack")))))
(defun decimation-hero-addition ()
"Hero attacks with addition."
(interactive)
(decimation-hero-attack (lambda (x) (% (+ x 3) 10))))
(defun decimation-hero-subtraction ()
"Hero attacks with subtraction."
(interactive)
(decimation-hero-attack (lambda (x) (% (1- x) 10))))
(defun decimation-hero-multiplication ()
"Hero attacks with multiplication."
(interactive)
(decimation-hero-attack (lambda (x) (% (* x x) 10))))
(defun decimation-hero-devision ()
"Hero attacks with devision."
(interactive)
(decimation-hero-attack (lambda (x) (% (ceiling (/ x 2.0)) 10))))
(defun decimation-draw-screen ()
"Draw game screen from entries in GRID."
(switch-to-buffer (get-buffer-create decimation-buffer-name))
(setq decimation-local-grid-buffer-pos (make-vector decimation-local-grid-size nil))
(let ((inhibit-read-only t))
(erase-buffer)
(dotimes (x decimation-local-grid-offset-height)
(newline))
(dotimes (y decimation-local-grid-height)
(indent-to decimation-local-grid-offset-width)
(dotimes (x decimation-local-grid-width)
(let* ((pos (+ (* y decimation-local-grid-width) x))
(char (aref decimation-local-grid pos)))
(aset decimation-local-grid-buffer-pos pos (point))
(cond
((= char decimation-grid-floor-char)
(insert (propertize (char-to-string char) 'face decimation-face-floor)))
((= char decimation-grid-hero-char)
(insert (propertize (char-to-string char) 'face decimation-face-hero)))
(t
(insert (propertize (char-to-string char) 'face decimation-face-monster))))
(when (= decimation-local-grid-space 2)
(insert " "))))
(newline))
(newline 5)
(decimation-output-status))
(dolist (monster decimation-local-monsters)
(when (decimation-monster-can-see monster (second decimation-local-hero))
(decimation-set-monster monster (third monster) nil t))))
(defun decimation ()
"Play Decimiation game."
(interactive)
(switch-to-buffer (get-buffer-create decimation-buffer-name))
(decimation-mode)
(setq case-fold-search nil
truncate-lines nil
show-trailing-whitespace nil
fill-column (1- (window-width)))
(buffer-disable-undo (current-buffer))
(setq
decimation-local-grid nil
decimation-local-grid-buffer-pos nil
decimation-local-monsters nil
decimation-local-hero nil
decimation-local-hero-moves nil
decimation-local-hero-max-rationality nil
decimation-local-turn 0
decimation-local-grid-space (if (and (>= (window-width) (1- (* decimation-grid-width 2)))
(>= (window-height) (1- (* decimation-grid-height 2))))
2
1)
decimation-local-grid-offset-width (if (= decimation-local-grid-space 2)
(/ (- (1- (window-width)) (1- (* decimation-grid-width 2))) 2)
(/ (- (1- (window-width)) decimation-grid-width) 2))
decimation-local-grid-offset-height (if (= decimation-local-grid-space 2)
(/ (- (1- (window-height)) (1- (* decimation-grid-height 2))) 2)
(/ (- (1- (window-height)) decimation-grid-height) 2)))
(decimation-initialize-grid decimation-grid-width decimation-grid-height decimation-monsters
decimation-hero-rationality decimation-hero-moves)
(decimation-draw-screen))
(defun decimation-help ()
"Print help screen."
(interactive)
(let ((inhibit-read-only t))
(switch-to-buffer (get-buffer-create decimation-help-buffer-name) t)
(erase-buffer)
(insert "\"Decimation is a rogue-like game where you're the number zero and
your goal, in zero's naturally nihilistic fashion, is to wipe out
all the other numbers! But you have to use mathematical
operations in order to make them equal zero...\"
-- Ed Kolis (Author of the original version.)
STORY
You are the number zero.
Being the number zero, you are a nihilist.
This means you want to wipe out all the other impure, barbaric
numbers from the face of the Cartesian plane.
THE GAME WORLD
The game world is very simple: a 20 by 20 grid.
There are no obstacles - just you (the zero) and the
monsters (the other digits from 1 to 9).
THE HERO
As stated above, you are the number zero.
You start with 100 / 100 rationality points which are your HP.
When you are attacked by an adjacent monster, you lose some
rationality.
Should your rationality reach zero or below, you lose the game.
You are slower than all the monsters (you move once every ten
game ticks; see THE MONSTERS for details on monster speeds).
Thus it is important to avoid letting other monsters see you
until you're done dealing with the ones that can already see you.
You have no melee attacks; instead, you have four special attacks
which affect all adjacent monsters with an arithmetic operation.
See COMMANDS for details on commands.
THE MONSTERS
Monsters are the other digits from 1 to 9.
Monsters that can see you will seek after you and attack you.
Monsters that cannot see you will mill about randomly.
Monsters' sight range is equal to their number.
Thus, a one can see you only if you're standing right next to it,
but a nine can see you about halfway across the map.
If a monster can see you, it is displayed in red; otherwise it is
displayed in yellow.
\(The zero and any empty tiles are displayed in brown if they are
in sight range of any monster, and white if they are not.)
Monsters' attack power is XdX, where X is the number of the
monster.
Thus, a one does 1d1 (1) damage per hit, a five does 5d5 (5 to
25) damage per hit, and a nine does 9d9 (9 to 81) damage per hit.
However, lower numbered monsters are pests which move very
quickly.
While the hero moves every 10 game ticks, monsters move and
attack according to their number, so a 5 is twice as fast as the
hero, and a 1 is ten times as fast.
COMMANDS
All commands in Decimation are performed using the numeric
keypad.
The following commands are available:
1, 2, 3, 4, 6, 7, 8, 9: Move in various directions. (Note that
you cannot use 5 or . to sit still and wait for the monsters to
come to you\; you have to at least side-step.)
+: Attack all adjacent monsters with Addition. (See COMBAT for
more details).
-: Attack all adjacent monsters with Subtraction. (See COMBAT for
more details).
*: Attack all adjacent monsters with Multiplication. (See COMBAT
for more details).
/: Attack all adjacent monsters with Division. (See COMBAT for
more details).
n: Start a new game.
q: Quit game.
h: Show this help dialog.
COMBAT
The hero has four attacks with which to attack all adjacent
monsters.
The goal is to transform the numbers so that they equal zero.
Whenever a monster reaches zero, it disappears and the hero
regains half the rationality (HP) that have been lost in
combat (rounded down).
Thus, if the hero is at 75 / 100 HP, and defeats a monster, the
hero's HP will increase by 12 to 87 / 100.
The attacks are as follows (note that if a number reaches 10 or
above, the number is taken \"modulo 10\", that is, all but the last
digit are removed):
Addition (triggered by + key on numpad): Adds three to adjacent
numbers. Not generally useful, but can weaken an eight or nine,
or defeat a seven.
Subtraction (triggered by - key on numpad): Subtracts one from
adjacent numbers. Useful for dealing with small numbers\; will
take too long to wear down larger ones.
Multiplication (triggered by * key on numpad): Multiplies
adjacent numbers by themselves. Does nothing to a five or six,
but is devastating to a nine.
Division (triggered by / key on numpad): Divides adjacent numbers
by two, rounding up. Useful for quickly weakening large numbers.
Monster to player combat is explained in the sections THE HERO
and THE MONSTERS.")
(goto-char (point-min))))
(provide 'decimation)