changeset 0:a9a7ad180c3b version-1

Initial revision
author Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
date Sat, 15 Mar 2014 18:41:03 +0100
parents
children 076450f6a179
files Makefile README board.c board.h compat.h deps.sed docbook-update-source-data.xsl highscore.c highscore.h rantaiwarna.6.xml rantaiwarna.c rantaiwarna.h util.c util.h
diffstat 14 files changed, 2986 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,116 @@
+#
+# Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+PACKAGE =	rantaiwarna
+VERSION =	1
+DISTNAME :=	$(PACKAGE)-$(VERSION)
+
+COMPILE.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(TARGET_ARCH) -c
+# gcc, clang, icc
+MAKEDEPEND.c =	$(CC) -MM $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# Sun/Solaris Studio
+#MAKEDEPEND.c =	$(CC) -xM1 $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# X makedepend
+#MAKEDEPEND.c =	makedepend -f- -Y -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) --
+LINK.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+LINK.o =	$(CC) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+INSTALL :=	install
+INSTALL.exec :=	$(INSTALL) -D -m 0755
+INSTALL.data :=	$(INSTALL) -D -m 0644
+PAX :=		pax
+GZIP :=		gzip
+SED :=		sed
+XSLTPROC :=	xsltproc
+DOCBOOK5_MANPAGES_STYLESHEET =	http://docbook.sourceforge.net/release/xsl-ns/current/manpages/docbook.xsl
+
+define generate-manpage-rule =
+%.$(1): %.$(1).xml
+	$$(XSLTPROC) \
+	    --xinclude \
+	    --stringparam package $$(PACKAGE) \
+	    --stringparam version $$(VERSION)\
+	    docbook-update-source-data.xsl $$< | \
+	    $$(XSLTPROC) \
+	    --xinclude \
+	    $$(DOCBOOK5_MANPAGES_FLAGS) \
+	    --output $$@ \
+	    $$(DOCBOOK5_MANPAGES_STYLESHEET) \
+	    -
+endef
+
+DESTDIR ?=
+prefix ?=	/usr/local
+bindir ?=	$(prefix)/bin
+datadir ?=	$(prefix)/share
+mandir ?=	$(datadir)/man
+
+OBJS =		rantaiwarna.o board.o highscore.o util.o
+MANPAGES =	$(PACKAGE).6
+DOCBOOK5_MANPAGES_FLAGS =	--stringparam man.authors.section.enabled 0 \
+				--stringparam man.copyright.section.enabled 0
+
+.DEFAULT_TARGET = all
+
+.PHONY: all clean clobber dist install
+
+all: $(PACKAGE) $(MANPAGES)
+
+$(PACKAGE): XCPPFLAGS :=
+# Illumos/Solaris
+#$(PACKAGE): XCPPFLAGS := -I/usr/xpg4/include
+$(PACKAGE): XCFLAGS :=
+$(PACKAGE): LDLIBS :=	-lcurses -lm
+# Linux, FreeBSD, OpenBSD
+#$(PACKAGE): LDLIBS :=	-lncursesw -ltinfo -lm
+# Illumos/Solaris, UnixWare, NetBSD
+#$(PACKAGE): LDLIBS :=	-lcurses -lm
+$(PACKAGE): XLDFLAGS :=
+# Illumos/Solaris
+#$(PACKAGE): XLDFLAGS := -L/usr/xpg4/lib -R/usr/xpg4/lib
+$(PACKAGE): $(OBJS)
+	$(LINK.o) $^ $(LDLIBS) -o $@
+
+$(foreach section,1 2 3 4 5 6 7 8 9,$(eval $(call generate-manpage-rule,$(section))))
+
+%.o: %.c
+	$(MAKEDEPEND.c) $< | $(SED) -f deps.sed >$*.d
+	$(COMPILE.c) -o $@ $<
+
+install:
+	$(INSTALL.exec) $(PACKAGE) "$(DESTDIR)$(bindir)/$(PACKAGE)"
+	for manpage in $(MANPAGES); do \
+	    $(INSTALL.data) $${manpage} \
+	        "$(DESTDIR)$(mandir)/man$${manpage##*.}/$${manpage##*/}"; \
+	done
+
+clean:
+	rm -f $(PACKAGE) $(OBJS) $(MANPAGES)
+
+clobber: clean
+	rm -f $(patsubst %.o,%.d,$(OBJS))
+
+dist: clobber
+	$(PAX) -w -x ustar -s ',.*/\..*,,' -s ',./[^/]*\.tar\.gz,,' \
+	    -s ',\./,$(DISTNAME)/,' . | $(GZIP) > $(DISTNAME).tar.gz
+
+-include $(patsubst %.o,%.d,$(OBJS))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,55 @@
+rantaiwarna
+===========
+
+Description
+-----------
+
+rantaiwarna is a tile-matching puzzle game which is also known under the names
+"Chain Shot!" or "SameGame". It is played on a rectangular board which is
+initially filled with elements of several different colors. Two or more
+adjacent elements of the same color may be eliminated, the score resulting from
+the elimination of elements depends on the number of elements eliminated at
+once. The goal of the game is to eliminate as many elements as possible until
+there are no more adjacent elements of the same color left or the board is
+completely cleared.  Vertical gaps resulting from the elimination of elements
+are filled by sliding down elements from above the gap, column gaps are filled
+by sliding columns on the right side of the column gap to the left.
+
+Build Instructions
+------------------
+
+rantaiwarna requires a POSIX:2004 compatible operating system, and needs GNU
+make, GNU or BSD install, the xsltproc tool from libxml2 and an implementation
+of the curses library compatible with X/Open CURSES Issue 4 to be installed.
+It can take advantage of advanced features of recent ncurses versions, in
+particular mouse support, but it also works with the native curses
+implementations of Solaris, NetBSD, and UnixWare.  It has been tested on Linux
+distributions, FreeBSD, Solaris and Illumos-derived distributions, UnixWare,
+and OpenServer.
+
+License
+-------
+
+Except otherwise noted, all files are Copyright (C) 2014 Guido Berhoerster and
+distributed under the following license terms:
+
+Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/board.c	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	_XOPEN_SOURCE	600
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <curses.h>	/* for OK, ERR */
+
+#include "board.h"
+#include "compat.h"
+#include "util.h"
+
+struct stack_element {
+	struct stack_element	*next;
+	int	x;
+	int	y;
+};
+
+struct board_ctx *
+board_create(int height, int width, short colors)
+{
+	struct board_ctx *board;
+
+	board = rantaiwarna_malloc(sizeof (struct board_ctx));
+	board->elements = rantaiwarna_calloc((size_t)(width * height),
+	    sizeof (short));
+	board->seed = (uint32_t)getpid();
+	board->height = height;
+	board->width = width;
+	board->colors = colors;
+	board->status = GAME_OVER;
+	board->score = 0;
+	if (board_generate(board) == ERR) {
+		board_free(board);
+		return (NULL);
+	}
+
+	return (board);
+}
+
+void
+board_free(struct board_ctx *board)
+{
+	free(board->elements);
+	free(board);
+}
+
+int
+board_check_status(struct board_ctx *board)
+{
+	int	status = GAME_OVER | GAME_OVER_CLEARED;
+	int	y;
+	int	x;
+
+	for (y = board->height - 1; y >= 0; y--) {
+		for (x = 0; x < board->width; x++) {
+			if (board->elements[y * board->width + x] != 0) {
+				status &= ~GAME_OVER_CLEARED;
+				if (((y - 1 >= 0) &&
+				    (board->elements[y * board->width + x] ==
+				    board->elements[(y - 1) * board->width +
+				    x])) ||
+				    ((x + 1 < board->width) &&
+				    (board->elements[y * board->width + x] ==
+				    (board->elements[y * board->width + x +
+				    1])))) {
+					status &= ~GAME_OVER;
+					goto out;
+				}
+			}
+		}
+	}
+
+out:
+	board->status = status;
+	return (status);
+}
+
+int
+board_generate(struct board_ctx *board)
+{
+	int	i;
+	int	j;
+
+	/* make up to 1000 attempts to create a board that is playable */
+	for (i = 0; i < 1000; i++) {
+		for (j = 0; j < board->width * board->height; j++) {
+			board->elements[j] =
+			    (short)(rantaiwarna_rand(&board->seed) %
+			    board->colors) + 1;
+		}
+		if (board_check_status(board) & GAME_OVER) {
+			return (ERR);
+		}
+	}
+
+	board->score = 0;
+
+	return (OK);
+}
+
+int
+board_compact(struct board_ctx *board)
+{
+	int	changes = 0;
+	int	x;
+	int	y;
+	int	top;
+	int	left;
+
+	/* close vertical gaps iby moving elements down */
+	for (x = 0; x < board->width; x++) {
+		for (y = top = board->height - 1; y >= 0; y--) {
+			if (board->elements[y * board->width + x] != 0) {
+				board->elements[top * board->width + x] =
+				    board->elements[y * board->width + x];
+				top--;
+				changes++;
+			}
+		}
+		for (y = top; y >= 0; y--) {
+			board->elements[y * board->width + x] = 0;
+		}
+	}
+
+	/* close column gaps by moving columns to the left */
+	for (x = left = 0; x < board->width; x++) {
+		if (board->elements[(board->height - 1) * board->width + x] !=
+		    0) {
+			if (x > left) {
+				for (y = 0; y < board->height; y++) {
+					board->elements[y * board->width +
+					    left] = board->elements[y *
+					    board->width + x];
+				}
+				changes++;
+			}
+			left++;
+		}
+	}
+	for (x = left; x < board->width; x++) {
+		for (y = 0; y < board->height; y++) {
+			board->elements[y * board->width + x] = 0;
+		}
+	}
+
+	return (changes);
+}
+
+int
+board_remove_elements(struct board_ctx *board, int start_y, int start_x)
+{
+	int	removed = 0;
+	int	color;
+	int	x;
+	int	y;
+	int	west;
+	int	east;
+	int	n = 0;
+	int	i;
+	int	save_x;
+	int	save_y;
+	struct stack_element *stack_start;
+	struct stack_element *stack_tmp;
+	struct stack_element *stack_new;
+
+	color = board->elements[start_y * board->width + start_x];
+	if (color == 0) {
+		return (0);
+	}
+
+	stack_new = rantaiwarna_malloc(sizeof (struct stack_element));
+	stack_new->next = NULL;
+	stack_new->y = start_y;
+	stack_new->x = start_x;
+	stack_start = stack_new;
+
+	while (stack_start != NULL) {
+		x = stack_start->x;
+		y = stack_start->y;
+		stack_tmp = stack_start;
+		stack_start = stack_start->next;
+		free(stack_tmp);
+
+		if (board->elements[y * board->width + x] != color) {
+			continue;
+		}
+
+		for (west = x; (west - 1 >= 0) &&
+		    (board->elements[y * board->width + west - 1] == color);
+		    west--);
+		for (east = x; (east + 1 < board->width) &&
+		    (board->elements[y * board->width + east + 1] == color);
+		    east++);
+		for (i = west; i <= east; i++) {
+			/*
+			 * only really start removing elements if there are
+			 * more than two adjacent elements of the same color
+			 */
+			n++;
+			if (n < 2) {
+				save_x = i;
+				save_y = y;
+			} else {
+				if (n == 2) {
+					board->elements[save_y * board->width +
+					    save_x] = 0;
+					removed++;
+				}
+				board->elements[y * board->width + i] = 0;
+				removed++;
+			}
+
+			if ((y - 1 >= 0) &&
+			    (board->elements[(y - 1) * board->width + i] ==
+			    color)) {
+				stack_new = rantaiwarna_malloc(
+				    sizeof (struct stack_element));
+				stack_new->next = stack_start;
+				stack_new->y = y - 1;
+				stack_new->x = i;
+				stack_start = stack_new;
+			}
+			if ((y + 1 < board->height) &&
+			    (board->elements[(y + 1) * board->width + i] ==
+			    color)) {
+				stack_new = rantaiwarna_malloc(
+				    sizeof (struct stack_element));
+				stack_new->next = stack_start;
+				stack_new->y = y + 1;
+				stack_new->x = i;
+				stack_start = stack_new;
+			}
+		}
+	}
+	if (removed > 0) {
+		board_compact(board);
+		board->score += (removed - 1) * (removed - 1);
+		board_check_status(board);
+		if (board->status & GAME_OVER) {
+			if (board->status & GAME_OVER_CLEARED) {
+				board->score += 1000;
+			}
+		}
+	}
+
+	return (removed);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/board.h	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	BOARD_H
+#define	BOARD_H
+
+#include <stdint.h>
+
+enum {
+	GAME_OVER = 1,
+	GAME_OVER_CLEARED = 2
+};
+
+struct board_ctx {
+	short		*elements;
+	uint32_t	seed;
+	int		height;
+	int		width;
+	short		colors;
+	int32_t		score;
+	bool		status;
+};
+
+struct board_ctx * board_create(int, int, short);
+void	board_free(struct board_ctx *);
+int	board_check_status(struct board_ctx *);
+int	board_generate(struct board_ctx *);
+int	board_compact(struct board_ctx *);
+int	board_remove_elements(struct board_ctx *, int, int);
+
+#endif /* BOARD_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat.h	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	COMPAT_H
+#define	COMPAT_H
+
+/*
+ * select C99/POSIX:2004-compatible versions of vsnprintf/snprintf on UnixWare 7
+ */
+#if defined(__SCO_VERSION__) && !defined(__OPENSERVER__)
+#define	vsnprintf	_xvsnprintf
+#define	snprintf	_xsnprintf
+#endif /* defined(__SCO_VERSION__) && !defined(__OPENSERVER__) */
+
+#endif /* COMPAT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/deps.sed	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,26 @@
+/^[^:]\{1,\}:.*\\$/{
+    h
+    s/\([^:]\{1,\}:\).*/\1/
+    x
+    s/[^:]\{1,\}://
+}
+/\\$/,/^$/bgen
+/\\$/,/[^\\]$/{
+:gen
+    s/[[:blank:]]*\\$//
+    s/^[[:blank:]]*//
+    G
+    s/\(.*\)\n\(.*\)/\2 \1/
+}
+/^[^:]\{1,\}:[[:blank:]]*$/d
+/^[^:]\{1,\}\.o:/{
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC][[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC]$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp$//g
+    /^[^:]\{1,\}:[[:blank:]]*$/d
+    s/^\([^:]\{1,\}\)\.o[[:blank:]]*:[[:blank:]]*\(.*\)/\1.d: $(wildcard \2)\
+&/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docbook-update-source-data.xsl	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<xsl:stylesheet
+  version="1.0"
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+  xmlns:db="http://docbook.org/ns/docbook"
+  xmlns="http://docbook.org/ns/docbook"
+  exclude-result-prefixes="xsl db">
+
+  <xsl:param name="package" select="''" />
+  <xsl:param name="version" select="''" />
+
+  <xsl:template match="db:refmeta/db:refmiscinfo[@class = 'source' or
+    @class = 'version']"/>
+
+  <xsl:template match="db:refmeta">
+    <xsl:copy>
+      <xsl:apply-templates/>
+      <refmiscinfo class="source"><xsl:value-of select="$package"/></refmiscinfo>
+      <refmiscinfo class="version"><xsl:value-of select="$version"/></refmiscinfo>
+    </xsl:copy>
+  </xsl:template>
+
+  <xsl:template match="@*|node()">
+    <xsl:copy>
+      <xsl:apply-templates select="@*|node()"/>
+    </xsl:copy>
+  </xsl:template>
+
+</xsl:stylesheet>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/highscore.c	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	_XOPEN_SOURCE	600
+
+#include <stdlib.h>
+#include <pwd.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include <sys/stat.h>
+#include <curses.h>	/* for ERR, OK */
+
+#include "highscore.h"
+#include "compat.h"
+#include "util.h"
+
+#define	HIGHSCORE_FILENAME	".rantaiwarna_hiscore"
+#define	HIGHSCORE_TMP_TMPL	".rantaiwarna_hiscoreXXXXXX"
+
+static struct highscore_entry *
+highscore_entry_new(int16_t width, int16_t height, int16_t colors,
+    int32_t score, time_t time)
+{
+	struct highscore_entry *entry;
+
+	entry = rantaiwarna_malloc(sizeof (struct highscore_entry));
+	entry->next = NULL;
+	entry->width = width;
+	entry->height = height;
+	entry->colors = colors;
+	entry->score = score;
+	localtime_r(&time, &entry->time);
+
+	return (entry);
+}
+
+void
+highscore_entry_free(struct highscore_entry *entry)
+{
+	free(entry);
+}
+
+void
+highscore_free(struct highscore_entry *highscore)
+{
+	struct highscore_entry *next_entry = highscore;
+	struct highscore_entry *entry = NULL;
+
+	while (next_entry != NULL) {
+		entry = next_entry;
+		next_entry = entry->next;
+		highscore_entry_free(entry);
+	}
+}
+
+void
+highscore_update(struct highscore_entry **highscorep, int16_t width,
+    int16_t height, int16_t colors, int32_t score, time_t time)
+{
+	struct highscore_entry *new_entry;
+	struct highscore_entry *entry = *highscorep;
+	int	n = 0;
+
+	if ((entry == NULL) || (entry->score < score)) {
+		/*
+		 * if there are no entries or the current score is greater than
+		 * the first entry, prepend a new entry
+		 */
+		new_entry = highscore_entry_new(width, height, colors, score,
+		    time);
+		new_entry->next = entry;
+		*highscorep = new_entry;
+		n++;
+	} else {
+		/*
+		 * if the current score is higher than the score of one of the
+		 * top ten entries, insert a new entry
+		 */
+		for (n = 0; (entry->next != NULL) && (entry->next->score >
+		    score) && (n < 10); entry = entry->next, n++);
+		new_entry = highscore_entry_new(width, height, colors, score,
+		    time);
+		new_entry->next = entry->next;
+		entry->next = new_entry;
+	}
+	if (entry != NULL) {
+		/* trim any entries outside the top ten */
+		while ((entry->next != NULL) && (n < 10)) {
+			entry = entry->next;
+			n++;
+		}
+		highscore_free(entry->next);
+		entry->next = NULL;
+	}
+}
+
+int
+highscore_load(struct highscore_entry **highscorep, int16_t ref_height,
+    int16_t ref_width, int16_t ref_colors)
+{
+	int		error = ERR;
+	struct highscore_entry *highscore = NULL;
+	char		*home_dir;
+	struct passwd	*pw;
+	int		n;
+	char		*highscore_filename = NULL;
+	FILE		*fp = NULL;
+	char		*scan_fmt = NULL;
+	int16_t		width;
+	int16_t		height;
+	int16_t		colors;
+	int32_t		score;
+	char		timestr[4 + 1 + 2 + 1 + 2 + 1];	/* holds %Y-%m-%d */
+	struct tm	tm;
+	time_t		time;
+
+	home_dir = getenv("HOME");
+	if (home_dir == NULL) {
+		pw = getpwuid(getuid());
+		if (pw != NULL) {
+			home_dir = pw->pw_dir;
+		} else {
+			goto out;
+		}
+	}
+
+	n = snprintf(NULL, 0, "%s/%s", home_dir, HIGHSCORE_FILENAME);
+	if (n < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	highscore_filename = rantaiwarna_malloc((size_t)n + 1);
+	if (snprintf(highscore_filename, (size_t)n + 1,
+	    "%s/%s", home_dir, HIGHSCORE_FILENAME) != n) {
+		rantaiwarna_err(1, NULL);
+	}
+
+	n = snprintf(NULL, 0, "%%" SCNd16 "%%" SCNd16 "%%" SCNd16 "%%" SCNd32
+	    "%%%lus", sizeof (timestr) - 1);
+	if (n < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	scan_fmt = rantaiwarna_malloc((size_t)n + 1);
+	if (snprintf(scan_fmt, (size_t)n + 1, "%%" SCNd16 "%%" SCNd16 "%%"
+	    SCNd16 "%%" SCNd32 "%%%lus", sizeof (timestr) - 1) != n) {
+		rantaiwarna_err(1, NULL);
+	}
+
+	fp = fopen(highscore_filename, "r");
+	if (fp == NULL) {
+		goto out;
+	}
+	while ((n = fscanf(fp, scan_fmt, &width, &height, &colors, &score,
+	    timestr)) != EOF) {
+		if ((n == 5) && ((width == ref_width) &&
+		    (height == ref_height) && (colors == ref_colors))) {
+			memset(&tm, 0, sizeof (struct tm));
+			time = (time_t)0;
+			if (strptime(timestr, "%Y-%m-%d", &tm) != NULL) {
+				time = mktime(&tm);
+				if (time == (time_t)-1) {
+					time = (time_t)0;
+				}
+			}
+			highscore_update(&highscore, width, height, colors,
+			    score, time);
+		}
+	}
+	if (ferror(fp) != 0) {
+		goto out;
+	}
+
+	error = OK;
+
+out:
+	if (fp != NULL) {
+		fclose(fp);
+	}
+
+	free(scan_fmt);
+	free(highscore_filename);
+
+	if (error == ERR) {
+		highscore_free(highscore);
+	} else {
+		highscore_free(*highscorep);
+		*highscorep = highscore;
+	}
+
+	return (error);
+}
+
+int
+highscore_save(struct highscore_entry *highscore, int16_t ref_height,
+    int16_t ref_width, int16_t ref_colors)
+{
+	int		error = ERR;
+	char		*scan_fmt = NULL;
+	char		*home_dir;
+	struct passwd	*pw;
+	int		n;
+	char		*highscore_filename = NULL;
+	char		*tmp_filename = NULL;
+	mode_t		old_mode;
+	FILE		*fp_in = NULL;
+	int		fd_out = -1;
+	FILE		*fp_out = NULL;
+	int16_t		width;
+	int16_t		height;
+	int16_t		colors;
+	int32_t		score;
+	char		timestr[4 + 1 + 2 + 1 + 2 + 1]; /* holds %Y-%m-%d */
+	struct highscore_entry *entry;
+
+	/*
+	 * file format:
+	 * entries consisting of five fields, entries and fields separated by
+	 * whitespace
+	 *
+	 * fields:
+	 * width (int16_t)
+	 * height (int16_t)
+	 * colors (int16_t)
+	 * score (int32_t)
+	 * time string (YYYY-MM-DD)
+	 */
+	if (highscore == NULL) {
+		goto out;
+	}
+
+	n = snprintf(NULL, 0, "%%" SCNd16 "%%" SCNd16 "%%" SCNd16 "%%" SCNd32
+	    "%%%lus", sizeof (timestr) - 1);
+	if (n < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	scan_fmt = rantaiwarna_malloc((size_t)n + 1);
+	if (snprintf(scan_fmt, (size_t)n + 1, "%%" SCNd16 "%%" SCNd16 "%%"
+	    SCNd16 "%%" SCNd32 "%%%lus", sizeof (timestr) - 1) != n) {
+		rantaiwarna_err(1, NULL);
+	}
+
+	home_dir = getenv("HOME");
+	if (home_dir == NULL) {
+		pw = getpwuid(getuid());
+		if (pw != NULL) {
+			home_dir = pw->pw_dir;
+		} else {
+			goto out;
+		}
+	}
+
+	n = snprintf(NULL, 0, "%s/%s", home_dir, HIGHSCORE_FILENAME);
+	if (n < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	highscore_filename = rantaiwarna_malloc((size_t)n + 1);
+	if (snprintf(highscore_filename, (size_t)n + 1, "%s/%s", home_dir,
+	    HIGHSCORE_FILENAME) != n) {
+		rantaiwarna_err(1, NULL);
+	}
+
+	n = snprintf(NULL, 0, "%s/%s", home_dir, HIGHSCORE_TMP_TMPL);
+	if (n < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	tmp_filename = rantaiwarna_malloc((size_t)n + 1);
+	if (snprintf(tmp_filename, (size_t)n + 1, "%s/%s", home_dir,
+	    HIGHSCORE_TMP_TMPL) != n) {
+		rantaiwarna_err(1, NULL);
+	}
+
+	old_mode = umask(077);
+	fd_out = mkstemp(tmp_filename);
+	umask(old_mode);
+	if (fd_out == -1) {
+		goto out;
+	}
+
+	fp_out = fdopen(fd_out, "w");
+	if (fp_out == NULL) {
+		goto out;
+	}
+	/*
+	 * preserve entries which do not match the current combination of width,
+	 * height, and colors
+	 */
+	fp_in = fopen(highscore_filename, "r");
+	if (fp_in != NULL) {
+		while ((n = fscanf(fp_in, scan_fmt, &width, &height, &colors,
+		    &score, timestr)) != EOF) {
+			if ((n == 5) && ((width != ref_width) ||
+			    (height != ref_height) || (colors != ref_colors))) {
+				if (fprintf(fp_out, "%" PRId16 " %" PRId16 " %"
+				    PRId16 " %" PRId32 " %s\n", width, height,
+				    colors, score, timestr) < 0) {
+					goto out;
+				}
+			}
+		}
+	}
+	/*
+	 * append entries for the current combination of width, height, and
+	 * colors
+	 */
+	for (entry = highscore; entry != NULL; entry = entry->next) {
+		if (strftime(timestr, sizeof (timestr), "%Y-%m-%d",
+		    &entry->time) == 0) {
+			snprintf(timestr, sizeof (timestr) - 1, "1970-01-01");
+		}
+		n = snprintf(NULL, 0, "%" PRId16 " %" PRId16 " %" PRId16 " %"
+		    PRId32 " %s\n", entry->width, entry->height, entry->colors,
+		    entry->score, timestr);
+		if (n < 0) {
+			goto out;
+		}
+		if (fprintf(fp_out, "%" PRId16 " %" PRId16 " %" PRId16 " %"
+		    PRId32 " %s\n", entry->width, entry->height,
+		    entry->colors, entry->score, timestr) != n) {
+			goto out;
+		}
+	}
+	if (fflush(fp_out) == EOF) {
+		goto out;
+	}
+	if (fsync(fd_out) == -1) {
+		goto out;
+	}
+
+	error = OK;
+
+out:
+	if (fp_in != NULL) {
+		fclose(fp_in);
+	}
+
+	if (fp_out != NULL) {
+		fclose(fp_out);
+	}
+
+	if (fd_out != -1) {
+		close(fd_out);
+	}
+
+	if (error == ERR) {
+		if (tmp_filename != NULL) {
+			unlink(tmp_filename);
+		}
+	} else {
+		if (rename(tmp_filename, highscore_filename) == -1) {
+			unlink(tmp_filename);
+		}
+	}
+
+	free(highscore_filename);
+	free(tmp_filename);
+	free(scan_fmt);
+
+	return (error);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/highscore.h	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	HIGHSCORE_H
+#define	HIGHSCORE_H
+
+#include <stdint.h>
+#include <time.h>
+
+struct highscore_entry {
+	struct highscore_entry *next;
+	int16_t		width;
+	int16_t		height;
+	int16_t		colors;
+	int32_t		score;
+	struct tm	time;
+};
+
+void	highscore_entry_free(struct highscore_entry *);
+void	highscore_free(struct highscore_entry *);
+void	highscore_update(struct highscore_entry **, int16_t, int16_t, int16_t,
+    int32_t, time_t);
+int	highscore_load(struct highscore_entry **, int16_t, int16_t, int16_t);
+int	highscore_save(struct highscore_entry *, int16_t, int16_t, int16_t);
+
+#endif /* HIGHSCORE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rantaiwarna.6.xml	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,523 @@
+<?xml version="1.0"?>
+<!--
+
+Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-->
+<refentry xmlns="http://docbook.org/ns/docbook" xml:lang="en">
+  <info>
+    <author>
+      <personname>
+        <firstname>Guido</firstname>
+        <surname>Berhoerster</surname>
+      </personname>
+      <email>guido+rantaiwarna@berhoerster.name</email>
+      <personblurb/>
+    </author>
+    <date>12 March, 2014</date>
+  </info>
+  <refmeta>
+    <refentrytitle>rantaiwarna</refentrytitle>
+    <manvolnum>6</manvolnum>
+    <refmiscinfo class="source"/>
+    <refmiscinfo class="version"/>
+    <refmiscinfo class="manual">User Commands</refmiscinfo>
+  </refmeta>
+  <refnamediv>
+    <refname>rantaiwarna</refname>
+    <refpurpose>tile-matching puzzle game</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>rantaiwarna</command>
+      <arg choice="opt">
+        <option>-c</option>
+        <replaceable>colors</replaceable>
+      </arg>
+      <arg choice="opt">
+        <option>-w</option>
+        <replaceable>width</replaceable>
+      </arg>
+      <arg choice="opt">
+        <option>-h</option>
+        <replaceable>height</replaceable>
+      </arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>Description</title>
+    <para><command>rantaiwarna</command> is a tile-matching puzzle game which
+      is also known under the names <quote>Chain Shot!</quote> or
+      <quote>SameGame</quote>. It is played on a rectangular board which is
+      initially filled with elements of several different colors. Two or more
+      adjacent elements of the same color may be eliminated, the score
+      resulting from the elimination of elements depends on the number of
+      elements eliminated at once. The goal of the game is to eliminate as many
+      elements as possible until there are no more adjacent elements of the
+      same color left or the board is completely cleared.  Vertical gaps
+      resulting from the elimination of elements are filled by sliding down
+      elements from above the gap, column gaps are filled by sliding columns on
+      the right side of the column gap to the left.</para>
+  </refsect1>
+  <refsect1>
+    <title>Options</title>
+    <para>The following options are supported:</para>
+    <variablelist>
+      <varlistentry>
+        <term>
+          <option>-c</option>
+          <replaceable>color</replaceable>
+        </term>
+        <listitem>
+          <para>Use the specified number of colors, at least two and at most
+          five.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-w</option>
+          <replaceable>width</replaceable>
+        </term>
+        <listitem>
+          <para>Create a board with the given width, at least five
+            elements.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-h</option>
+          <replaceable>height</replaceable>
+        </term>
+        <listitem>
+          <para>Create a board with the given height, at least five
+            elements.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>Usage</title>
+    <para><command>rantaiwarna</command> makes use of the <citerefentry>
+      <refentrytitle>curses</refentrytitle><manvolnum>3</manvolnum>
+      </citerefentry> library and determines the terminal's capabilities using
+      its <citerefentry><refentrytitle>terminfo</refentrytitle>
+      <manvolnum>5</manvolnum></citerefentry> entry. Certain keyboard and mouse
+      commands may not be available depending on the terminal and the variant of
+      the <citerefentry><refentrytitle>curses</refentrytitle>
+      <manvolnum>3</manvolnum></citerefentry> library
+      <command>rantaiwarna</command> was built against.</para>
+    <refsect2>
+      <title>Game Screen Commands</title>
+      <variablelist>
+        <varlistentry>
+          <term>
+            <keycap function="up"/>,
+            <keycap function="right"/>,
+            <keycap function="down"/>,
+            <keycap function="left"/>
+          </term>
+          <listitem>
+            <para>move the cursor around</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>h</keycap>,
+            <keycap>j</keycap>,
+            <keycap>k</keycap>,
+            <keycap>l</keycap>
+          </term>
+          <listitem>
+            <para>same as arrow keys</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="space"/>
+          </term>
+          <listitem>
+            <para>eliminate elements under the cursor</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="enter"/>
+          </term>
+          <listitem>
+            <para>same as <keycap function="space"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <mousebutton>Left mouse button</mousebutton>
+          </term>
+          <listitem>
+            <para>same as <keycap function="space"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>l</keycap>
+            </keycombo>
+          </term>
+          <listitem>
+            <para>refresh the screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>H</keycap>
+          </term>
+          <listitem>
+            <para>switch to the help screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>i</keycap>
+          </term>
+          <listitem>
+            <para>switch to the highscore screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>n</keycap>
+          </term>
+          <listitem>
+            <para>start a new game</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>q</keycap>
+          </term>
+          <listitem>
+            <para>quit the game</para>
+          </listitem>
+        </varlistentry>
+      </variablelist>
+    </refsect2>
+    <refsect2>
+      <title>Highscore Screen Commands</title>
+      <variablelist>
+        <varlistentry>
+          <term>
+            <keycap function="up"/>,
+            <keycap function="right"/>,
+            <keycap function="down"/>,
+            <keycap function="left"/>
+          </term>
+          <listitem>
+            <para>scroll on line up or down or one character to the left or
+              right</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>h</keycap>,
+            <keycap>j</keycap>,
+            <keycap>k</keycap>,
+            <keycap>l</keycap>
+          </term>
+          <listitem>
+            <para>same as arrow keys</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="space"/>
+          </term>
+          <listitem>
+            <para>scroll down one line</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="enter"/>
+          </term>
+          <listitem>
+            <para>same as <keycap function="space"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="pageup"/>,
+            <keycap function="pagedown"/>
+          </term>
+          <listitem>
+            <para>scroll up or down one screenful</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>b</keycap>,
+            <keycap>f</keycap>
+          </term>
+          <listitem>
+            <para>same as <keycap function="pageup"/> or
+              <keycap function="pagedown"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>b</keycap>
+            </keycombo>,
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>f</keycap>
+            </keycombo>
+          </term>
+          <listitem>
+            <para>same as <keycap function="pageup"/> or
+              <keycap function="pagedown"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="home"/>,
+            <keycap function="end"/>
+          </term>
+          <listitem>
+            <para>go to the first or last line</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>g</keycap>,
+            <keycap>G</keycap>,
+          </term>
+          <listitem>
+            <para>same as <keycap function="home"/> or
+              <keycap function="end"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>l</keycap>
+            </keycombo>
+          </term>
+          <listitem>
+            <para>refresh the screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>a</keycap>
+          </term>
+          <listitem>
+            <para>switch to the game screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>H</keycap>
+          </term>
+          <listitem>
+            <para>switch to the help screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>q</keycap>
+          </term>
+          <listitem>
+            <para>quit the game</para>
+          </listitem>
+        </varlistentry>
+      </variablelist>
+    </refsect2>
+    <refsect2>
+      <title>Help Screen Commands</title>
+      <variablelist>
+        <varlistentry>
+          <term>
+            <keycap function="up"/>,
+            <keycap function="right"/>,
+            <keycap function="down"/>,
+            <keycap function="left"/>
+          </term>
+          <listitem>
+            <para>scroll on line up or down or one character to the left or
+              right</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>h</keycap>,
+            <keycap>j</keycap>,
+            <keycap>k</keycap>,
+            <keycap>l</keycap>
+          </term>
+          <listitem>
+            <para>same as arrow keys</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="space"/>
+          </term>
+          <listitem>
+            <para>scroll down one line</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="enter"/>
+          </term>
+          <listitem>
+            <para>same as <keycap function="space"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="pageup"/>,
+            <keycap function="pagedown"/>
+          </term>
+          <listitem>
+            <para>scroll up or down one screenful</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>b</keycap>,
+            <keycap>f</keycap>
+          </term>
+          <listitem>
+            <para>same as <keycap function="pageup"/> or
+              <keycap function="pagedown"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>b</keycap>
+            </keycombo>,
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>f</keycap>
+            </keycombo>
+          </term>
+          <listitem>
+            <para>same as <keycap function="pageup"/> or
+              <keycap function="pagedown"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap function="home"/>,
+            <keycap function="end"/>
+          </term>
+          <listitem>
+            <para>go to the first or last line</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>g</keycap>,
+            <keycap>G</keycap>,
+          </term>
+          <listitem>
+            <para>same as <keycap function="home"/> or
+              <keycap function="end"/></para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycombo action="simul">
+              <keycap function="control"/>
+              <keycap>l</keycap>
+            </keycombo>
+          </term>
+          <listitem>
+            <para>refresh the screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>a</keycap>
+          </term>
+          <listitem>
+            <para>switch to the game screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>i</keycap>
+          </term>
+          <listitem>
+            <para>switch to the highscore screen</para>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
+          <term>
+            <keycap>q</keycap>
+          </term>
+          <listitem>
+            <para>quit the game</para>
+          </listitem>
+        </varlistentry>
+      </variablelist>
+    </refsect2>
+  </refsect1>
+  <refsect1>
+    <title>Files</title>
+    <variablelist>
+      <varlistentry>
+        <term>
+          <filename>$HOME/.rantaiwarna_hiscore</filename>
+        </term>
+        <listitem>
+          <para>personal highscore file</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>Exit Status</title>
+    <para>The following exit values are returned:</para>
+    <variablelist>
+      <varlistentry>
+        <term>0</term>
+        <listitem>
+          <para>Command successfully executed.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>&gt; 0</term>
+        <listitem>
+          <para>An error has occured.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>Files</title>
+  </refsect1>
+</refentry>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rantaiwarna.c	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,1258 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	_XOPEN_SOURCE	600
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <signal.h>
+#include <errno.h>
+#include <string.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <math.h>
+#include <time.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <curses.h>
+#include <term.h>
+
+#include "rantaiwarna.h"
+#include "compat.h"
+#include "util.h"
+#include "board.h"
+#include "highscore.h"
+
+#define	EXIT_USAGE	2
+#define	HEIGHT_MAX	INT16_MAX
+#define	HEIGHT_MIN	5
+#define	WIDTH_MAX	INT16_MAX
+#define	WIDTH_MIN	5
+#define	DEFAULT_WIDTH	20
+#define	DEFAULT_HEIGHT	10
+#define	COLORS_MAX	6
+#define	DEFAULT_COLORS	4
+#define	ELEMENT_WIDTH	2
+#define	SCORE_WIN_WIDTH	6
+#define	HIGHSCORE_FMT	"%2d) %s %6" PRId32
+#define	TIMESTR_MAX_SIZE 64
+#define	MESSAGE_MAX_SIZE 64
+
+enum {
+	PIPE_R_FD = 0,
+	PIPE_W_FD
+};
+
+enum {
+	GAME_SCREEN,
+	HIGHSCORE_SCREEN,
+	HELP_SCREEN
+};
+
+struct rantaiwarna_ctx {
+	struct board_ctx *board;
+	struct highscore_entry *highscore;
+	WINDOW		*game_header_win;
+	WINDOW		*message_win;
+	WINDOW		*score_win;
+	WINDOW		*board_container_win;
+	WINDOW		*board_win;
+	WINDOW		*highscore_pad;
+	WINDOW		*highscore_header_win;
+	WINDOW		*help_pad;
+	WINDOW		*help_header_win;
+	WINDOW		*input_win;
+	char		message[MESSAGE_MAX_SIZE];
+	char		dummy_timestr[TIMESTR_MAX_SIZE];
+	const chtype	*elements_chs;
+	int		board_cursor_y;
+	int		board_cursor_x;
+	int		highscore_pos_y;
+	int		highscore_pos_x;
+	int		help_pos_y;
+	int		help_pos_x;
+	int		active_screen;
+	bool		use_colors;
+	bool		too_small;
+};
+
+char		*progname;
+bool		curses_init = FALSE;
+static int	signal_pipe_fd[2] = { -1, -1 };
+static const chtype elements_chs[COLORS_MAX] = { ' ', '@', '#', '$', 'X', 'O' };
+static const char *help_strs[] = {
+	"rantaiwarna is a tile-matching puzzle game which is also known under "
+	"the names ",
+	"\"Chain Shot!\" or \"SameGame\". It is played on a rectangular board "
+	"which is ",
+	"initially filled with elements of several different colors. Two or "
+	"more ",
+	"adjacent elements of the same color may be eliminated, the score "
+	"resulting from ",
+	"the elimination of elements depends on the number of elements "
+	"eliminated at ",
+	"once. The goal of the game is to eliminate as many elements as "
+	"possible until ",
+	"there are no more adjacent elements of the same color left or the "
+	"board is ",
+	"completely cleared. Vertical gaps resulting from the elimination of "
+	"elements ",
+	"are filled by sliding down elements from above the gap, column gaps "
+	"are filled ",
+	"by sliding columns on the right side of the column gap to the left.",
+	"",
+	"Game Screen Commands",
+	"",
+	"    Arrow up, right, down, left",
+	"        move the cursor around",
+	"",
+	"    h, j, k, l",
+	"        same as arrow keys",
+	"",
+	"    Space",
+	"        eliminate elements under the cursor",
+	"",
+	"    Enter",
+	"        same as Space",
+	"",
+	"    Left mouse button",
+	"        same as Space",
+	"",
+	"    Ctrl+l",
+	"        refresh the screen",
+	"",
+	"    H",
+	"        switch to the help screen",
+	"",
+	"    i",
+	"        switch to the highscore screen",
+	"",
+	"    n",
+	"        start a new game",
+	"",
+	"    q",
+	"        quit the game",
+	"",
+	"Highscore Screen Commands",
+	"",
+	"    Arrow up, right, down, left",
+	"        scroll on line up or down or one character to the left or"
+	"right",
+	"",
+	"    h, j, k, l",
+	"        same as arrow keys",
+	"",
+	"    Space",
+	"        scroll down one line",
+	"",
+	"    Enter",
+	"        same as Space",
+	"",
+	"    Page up, Page down",
+	"        scroll up or down one screenful",
+	"",
+	"    b, f",
+	"        same as Page up or Page down",
+	"",
+	"    Ctrl+b, Ctrl+f",
+	"        same as Page up or Page down",
+	"",
+	"    Home, End",
+	"        go to the first or last line",
+	"",
+	"    g, G,",
+	"        same as Home or End",
+	"",
+	"    Ctrl+l",
+	"        refresh the screen",
+	"",
+	"    a",
+	"        switch to the game screen",
+	"",
+	"    H",
+	"        switch to the help screen",
+	"",
+	"    q",
+	"        quit the game",
+	"",
+	"Help Screen Commands",
+	"",
+	"    Arrow up, right, down, left",
+	"        scroll on line up or down or one character to the left or "
+	"right",
+	"",
+	"    h, j, k, l",
+	"        same as arrow keys",
+	"",
+	"    Space",
+	"        scroll down one line",
+	"",
+	"    Enter",
+	"        same as Space",
+	"",
+	"    Page up, Page down",
+	"        scroll up or down one screenful",
+	"",
+	"    b, f",
+	"        same as Page up or Page down",
+	"",
+	"    Ctrl+b, Ctrl+f",
+	"        same as Page up or Page down",
+	"",
+	"    Home, End",
+	"        go to the first or last line",
+	"",
+	"    g, G,",
+	"        same as Home or End",
+	"",
+	"    Ctrl+l",
+	"        refresh the screen",
+	"",
+	"    a",
+	"        switch to the game screen",
+	"",
+	"    i",
+	"        switch to the highscore screen",
+	"",
+	"    q",
+	"        quit the game",
+	"",
+	NULL
+};
+
+static void
+on_signal(int signo)
+{
+	int		old_errno = errno;
+	ssize_t		n;
+	sigset_t	sigset;
+
+	/* try to read unread signals from the pipe and add the new one to it */
+	n = read(signal_pipe_fd[PIPE_R_FD], &sigset, sizeof (sigset));
+	if (n == -1 || (size_t)n < sizeof (sigset)) {
+		sigemptyset(&sigset);
+	}
+	sigaddset(&sigset, signo);
+	write(signal_pipe_fd[PIPE_W_FD], &sigset, sizeof (sigset));
+
+	errno = old_errno;
+}
+
+static int
+rantaiwarna_doupdate(struct rantaiwarna_ctx *ctx)
+{
+	/*
+	 * ensure the physical cursor is a sensible place in case it could not
+	 * be made invisible
+	 */
+	if ((ctx->active_screen == GAME_SCREEN) && (ctx->board_win != NULL)) {
+		wmove(ctx->board_win, ctx->board_cursor_y,
+		    ctx->board_cursor_x * ELEMENT_WIDTH);
+		wnoutrefresh(ctx->board_win);
+	}
+	return (doupdate());
+}
+
+static void
+board_win_render_cursor(struct rantaiwarna_ctx *ctx, bool visible)
+{
+	/* highlighting color pairs are base color + COLORS_MAX */
+	short	color = ctx->board->elements[ctx->board_cursor_y *
+	    ctx->board->width + ctx->board_cursor_x] +
+	    (visible ? COLORS_MAX : 0);
+	int	i;
+	chtype	ch;
+
+	for (i = 0; i < ELEMENT_WIDTH; i++) {
+		ch = mvwinch(ctx->board_win, ctx->board_cursor_y,
+		    ctx->board_cursor_x * ELEMENT_WIDTH + i);
+		if (visible) {
+			/* use reverse and underline for highlighting */
+			ch = (ch & ~A_COLOR) | A_REVERSE | A_UNDERLINE;
+		} else {
+			ch &= ~(A_COLOR | A_REVERSE | A_UNDERLINE);
+		}
+		if (ctx->use_colors) {
+			ch |= COLOR_PAIR(color);
+		}
+		mvwaddch(ctx->board_win, ctx->board_cursor_y,
+		    ctx->board_cursor_x * ELEMENT_WIDTH + i, ch);
+	}
+}
+
+static void
+board_win_update_cursor(struct rantaiwarna_ctx *ctx, bool visible)
+{
+	board_win_render_cursor(ctx, visible);
+	touchwin(ctx->board_container_win);
+	wnoutrefresh(ctx->board_win);
+}
+
+static int
+board_win_move_cursor(struct rantaiwarna_ctx *ctx, int y, int x)
+{
+	if ((x < 0) || (x >= ctx->board->width) || (y < 0) ||
+	    (y >= ctx->board->height)) {
+		return (ERR);
+	}
+
+	board_win_update_cursor(ctx, FALSE);
+	ctx->board_cursor_x = x;
+	ctx->board_cursor_y = y;
+	board_win_update_cursor(ctx, TRUE);
+
+	return (OK);
+}
+
+static void
+score_win_render(struct rantaiwarna_ctx *ctx)
+{
+	mvwprintw(ctx->score_win, 0, 0, "%*" PRId32, SCORE_WIN_WIDTH,
+	    ctx->board->score);
+}
+
+static void
+score_win_update(struct rantaiwarna_ctx *ctx)
+{
+	score_win_render(ctx);
+	touchwin(ctx->game_header_win);
+	wnoutrefresh(ctx->score_win);
+}
+
+static void
+set_message(struct rantaiwarna_ctx *ctx, const char *message)
+{
+	snprintf(ctx->message, MESSAGE_MAX_SIZE, "%s", message);
+}
+
+static void
+message_win_render(struct rantaiwarna_ctx *ctx)
+{
+	int	game_header_win_height;
+	int	game_header_win_width;
+	int	message_win_begy;
+	int	message_win_begx;
+
+	getmaxyx(ctx->game_header_win, game_header_win_height,
+	    game_header_win_width);
+	getbegyx(ctx->message_win, message_win_begy, message_win_begx);
+	wclear(ctx->message_win);
+	mvwaddstr(ctx->message_win, 0, (game_header_win_width -
+	    (int)strlen(ctx->message)) / 2 - message_win_begx, ctx->message);
+}
+
+static void
+message_win_update(struct rantaiwarna_ctx *ctx)
+{
+	message_win_render(ctx);
+	touchwin(ctx->game_header_win);
+	wnoutrefresh(ctx->message_win);
+}
+
+static void
+board_win_render(struct rantaiwarna_ctx *ctx)
+{
+	chtype	ch;
+	int	x;
+	int	y;
+	short	color;
+	int	i;
+
+	for (x = 0; x < ctx->board->width; x++) {
+		for (y = 0; y < ctx->board->height; y++) {
+			color = ctx->board->elements[y * ctx->board->width + x];
+			ch = ctx->elements_chs[color];
+			if (ctx->use_colors) {
+				ch |= COLOR_PAIR(color);
+			}
+			for (i = 0; i < ELEMENT_WIDTH; i++) {
+				mvwaddch(ctx->board_win, y,
+				    x * ELEMENT_WIDTH + i, ch);
+			}
+		}
+	}
+}
+
+static void
+board_win_update(struct rantaiwarna_ctx *ctx)
+{
+	board_win_render(ctx);
+	if ((ctx->board->status & GAME_OVER) == 0) {
+		board_win_update_cursor(ctx, TRUE);
+	}
+	touchwin(ctx->board_container_win);
+	wnoutrefresh(ctx->board_win);
+}
+
+static void
+highscore_pad_update(struct rantaiwarna_ctx *ctx)
+{
+	struct highscore_entry *entry;
+	int	i;
+	int32_t	score;
+	char	timestr[TIMESTR_MAX_SIZE];
+	char	*timestrp;
+	int	height;
+	int	width;
+	int	highscore_pad_height;
+	int	highscore_pad_width;
+
+	for (entry = ctx->highscore, i = 0; i < 10; i++) {
+		if (entry != NULL) {
+			score = entry->score;
+			timestrp = timestr;
+			if (strftime(timestr, sizeof (timestr), "%x",
+			    &entry->time) == 0) {
+				timestr[0] = '\0';
+			}
+			entry = entry->next;
+		} else {
+			score = 0;
+			timestrp = ctx->dummy_timestr;
+		}
+		mvwprintw(ctx->highscore_pad, i, 0, HIGHSCORE_FMT, i + 1,
+		    timestrp, score);
+	}
+
+	getmaxyx(stdscr, height, width);
+	getmaxyx(ctx->highscore_pad, highscore_pad_height, highscore_pad_width);
+	if (ctx->highscore_pos_y + height > highscore_pad_height) {
+		ctx->highscore_pos_y = MAX(highscore_pad_height - height, 0);
+	}
+	if (ctx->highscore_pos_x + width > highscore_pad_width) {
+		ctx->highscore_pos_x = MAX(highscore_pad_width - width, 0);
+	}
+}
+
+static int
+highscore_pad_refresh(struct rantaiwarna_ctx *ctx)
+{
+	int	height;
+	int	width;
+	int	highscore_pad_height;
+	int	highscore_pad_width;
+
+	getmaxyx(stdscr, height, width);
+	getmaxyx(ctx->highscore_pad, highscore_pad_height, highscore_pad_width);
+
+	return (pnoutrefresh(ctx->highscore_pad, ctx->highscore_pos_y,
+	    ctx->highscore_pos_x, 1, 0, MIN(height - 1, highscore_pad_height -
+	    1), MIN(width - 1, highscore_pad_width - 1)));
+}
+
+static int
+help_pad_refresh(struct rantaiwarna_ctx *ctx)
+{
+	int	height;
+	int	width;
+	int	help_pad_height;
+	int	help_pad_width;
+
+	getmaxyx(stdscr, height, width);
+	getmaxyx(ctx->help_pad, help_pad_height, help_pad_width);
+
+	return (pnoutrefresh(ctx->help_pad, ctx->help_pos_y, ctx->help_pos_x,
+	    1, 0, MIN(height - 1, help_pad_height - ctx->help_pos_y - 1),
+	    MIN(width - 1, help_pad_width - ctx->help_pos_x - 1)));
+}
+
+static void
+active_screen_switch(struct rantaiwarna_ctx *ctx)
+{
+	clear();
+	wnoutrefresh(stdscr);
+	switch (ctx->active_screen) {
+	case GAME_SCREEN:
+		touchwin(ctx->game_header_win);
+		wnoutrefresh(ctx->game_header_win);
+		touchwin(ctx->board_container_win);
+		wnoutrefresh(ctx->board_container_win);
+		touchwin(ctx->board_win);
+		wnoutrefresh(ctx->board_win);
+		ctx->input_win = ctx->board_win;
+		break;
+	case HIGHSCORE_SCREEN:
+		touchwin(ctx->highscore_header_win);
+		wnoutrefresh(ctx->highscore_header_win);
+		highscore_pad_refresh(ctx);
+		ctx->input_win = stdscr;
+		break;
+	case HELP_SCREEN:
+		touchwin(ctx->help_header_win);
+		wnoutrefresh(ctx->help_header_win);
+		help_pad_refresh(ctx);
+		ctx->input_win = stdscr;
+		break;
+	}
+	flushinp();
+}
+
+static int
+scrolled_win_handle_input(WINDOW *pad, int *pos_yp, int *pos_xp, int c)
+{
+	int	y = *pos_yp;
+	int	x = *pos_xp;
+	int	height;
+	int	width;
+	int	pad_height;
+	int	pad_width;
+
+	getmaxyx(stdscr, height, width);
+	getmaxyx(pad, pad_height, pad_width);
+
+	switch (c) {
+	case 'k':
+	case KEY_UP:
+		/* scroll line up */
+		y--;
+		break;
+	case 'l':
+	case KEY_RIGHT:
+		/* scroll right */
+		x++;
+		break;
+	case 'j':
+	case ' ':
+	case '\n':
+	case '\r':
+	case KEY_ENTER:
+	case KEY_DOWN:
+		/* scroll line down */
+		y++;
+		break;
+	case 'h':
+	case KEY_LEFT:
+		/* scroll left */
+		x--;
+		break;
+	case 'b':
+	case '\002':	/* ^B */
+	case KEY_PPAGE:
+		/* scroll up one screenful */
+		y -= height;
+		break;
+	case 'f':
+	case '\006':	/* ^F */
+	case KEY_NPAGE:
+		/* scroll down one screenful */
+		y += height;
+		break;
+	case 'g':
+	case KEY_HOME:
+		/* go to the first line */
+		y = 0;
+		break;
+	case 'G':
+	case KEY_END:
+		/* go to the line */
+		y = MAX(pad_height - height, y);
+		break;
+	}
+
+	if (y < 0) {
+		y = 0;
+	} else if (y + height > pad_height) {
+		y = MAX(pad_height - height, 0);
+	}
+	if (x < 0) {
+		x = 0;
+	} else if (x + width > pad_width) {
+		x = MAX(pad_width - width, 0);
+	}
+
+	if ((x != *pos_xp) || (y != *pos_yp)) {
+		prefresh(pad, y, x, 1, 0, MIN(height - 1, pad_height - 1),
+		    MIN(width - 1, pad_width - 1));
+		*pos_yp = y;
+		*pos_xp = x;
+	}
+
+	return (OK);
+}
+
+static int
+highscore_handle_input(struct rantaiwarna_ctx *ctx, int c)
+{
+	switch (c) {
+	case 'a':
+		/* switch to game screen */
+		ctx->active_screen = GAME_SCREEN;
+		active_screen_switch(ctx);
+		rantaiwarna_doupdate(ctx);
+		return (OK);
+	case 'H':
+		/* switch to help screen */
+		ctx->active_screen = HELP_SCREEN;
+		active_screen_switch(ctx);
+		rantaiwarna_doupdate(ctx);
+		return (OK);
+	default:
+		/* delegate input handling to scrolled window handler */
+		return (scrolled_win_handle_input(ctx->highscore_pad,
+		    &ctx->highscore_pos_y, &ctx->highscore_pos_x, c));
+	}
+}
+
+static int
+help_handle_input(struct rantaiwarna_ctx *ctx, int c)
+{
+	switch (c) {
+	case 'a':
+		/* switch to game screen */
+		ctx->active_screen = GAME_SCREEN;
+		active_screen_switch(ctx);
+		rantaiwarna_doupdate(ctx);
+		return (OK);
+	case 'i':
+		/* switch to highscore screen */
+		ctx->active_screen = HIGHSCORE_SCREEN;
+		active_screen_switch(ctx);
+		rantaiwarna_doupdate(ctx);
+		return (OK);
+	default:
+		/* delegate input handling to scrolled window handler */
+		return (scrolled_win_handle_input(ctx->help_pad,
+		    &ctx->help_pos_y, &ctx->help_pos_x, c));
+	}
+}
+
+static int
+game_handle_input(struct rantaiwarna_ctx *ctx, int c)
+{
+	int	y = ctx->board_cursor_y;
+	int	x = ctx->board_cursor_x;
+	int	active_screen = ctx->active_screen;
+	int	removed = 0;
+#ifdef NCURSES_MOUSE_VERSION
+	MEVENT	event;
+	int	mx;
+	int	my;
+#endif /* NCURSES_MOUSE_VERSION */
+
+	switch (c) {
+	case 'k':
+	case KEY_UP:
+		/* move cursor up */
+		y--;
+		break;
+	case 'l':
+	case KEY_RIGHT:
+		/* move cursor right */
+		x++;
+		break;
+	case 'j':
+	case KEY_DOWN:
+		/* move cursor down */
+		y++;
+		break;
+	case 'h':
+	case KEY_LEFT:
+		/* move cursor left */
+		x--;
+		break;
+#ifdef NCURSES_MOUSE_VERSION
+	case KEY_MOUSE:
+		if (getmouse(&event) == OK) {
+			mx = event.x;
+			my = event.y;
+
+			/* only handle mouse input inside the board window */
+			if (wmouse_trafo(ctx->board_win, &my, &mx, FALSE)) {
+				mx = (int)(floor(mx / ELEMENT_WIDTH));
+
+				if (event.bstate & BUTTON1_RELEASED) {
+					/* move cursor and remove element */
+					y = my;
+					x = mx;
+					removed =
+					    board_remove_elements(ctx->board,
+					    y, x);
+				} else if (event.bstate &
+				    REPORT_MOUSE_POSITION) {
+					/* move cursor to mouse position */
+					y = my;
+					x = mx;
+				}
+			}
+		}
+		break;
+#endif /* NCURSES_MOUSE_VERSION */
+	case KEY_ENTER:
+	case '\n':
+	case '\r':
+	case ' ':
+		/* remove current element */
+		removed = board_remove_elements(ctx->board, y, x);
+		break;
+	case 'n':
+		/* start a new game */
+		ctx->board_cursor_x = ctx->board_cursor_y = 0;
+		set_message(ctx, "rantaiwarna");
+		if (board_generate(ctx->board) == OK) {
+			score_win_update(ctx);
+			message_win_update(ctx);
+			board_win_update(ctx);
+			rantaiwarna_doupdate(ctx);
+			flash();
+			return (OK);
+		} else {
+			return (ERR);
+		}
+	case 'H':
+		/* switch to help screen */
+		active_screen = HELP_SCREEN;
+		break;
+	case 'i':
+		/* switch to highscore screen */
+		active_screen = HIGHSCORE_SCREEN;
+		break;
+	}
+
+	if (active_screen != ctx->active_screen) {
+		ctx->active_screen = active_screen;
+		active_screen_switch(ctx);
+		rantaiwarna_doupdate(ctx);
+	} else if (removed > 0) {
+		score_win_update(ctx);
+		board_win_update(ctx);
+		if (ctx->board->status & GAME_OVER) {
+			highscore_update(&ctx->highscore,
+			    (int16_t)ctx->board->width,
+			    (int16_t)ctx->board->height,
+			    (int16_t)ctx->board->colors, ctx->board->score,
+			    time(NULL));
+			highscore_save(ctx->highscore, ctx->board->height,
+			    ctx->board->width, ctx->board->colors);
+			board_win_update_cursor(ctx, FALSE);
+			ctx->board_cursor_y = ctx->board_cursor_x = 0;
+			set_message(ctx, "GAME OVER");
+			message_win_update(ctx);
+			highscore_pad_update(ctx);
+			flash();
+		} else if ((y != ctx->board_cursor_y) ||
+		    (x != ctx->board_cursor_x)) {
+			board_win_move_cursor(ctx, y, x);
+		}
+		rantaiwarna_doupdate(ctx);
+	} else if (((ctx->board->status & GAME_OVER) == 0) &&
+	    ((y != ctx->board_cursor_y) || (x != ctx->board_cursor_x))) {
+		if (board_win_move_cursor(ctx, y, x) == OK) {
+			rantaiwarna_doupdate(ctx);
+		}
+	}
+
+	return (OK);
+}
+
+static void
+all_windows_delete(struct rantaiwarna_ctx *ctx)
+{
+	if (ctx == NULL) {
+		return;
+	}
+
+	if (ctx->message_win != NULL) {
+		delwin(ctx->message_win);
+		ctx->message_win = NULL;
+	}
+
+	if (ctx->score_win != NULL) {
+		delwin(ctx->score_win);
+		ctx->score_win = NULL;
+	}
+
+	if (ctx->game_header_win != NULL) {
+		delwin(ctx->game_header_win);
+		ctx->game_header_win = NULL;
+	}
+
+	if (ctx->board_win != NULL) {
+		delwin(ctx->board_win);
+		ctx->board_win = NULL;
+	}
+
+	if (ctx->board_container_win != NULL) {
+		delwin(ctx->board_container_win);
+		ctx->board_container_win = NULL;
+	}
+
+	if (ctx->highscore_header_win != NULL) {
+		delwin(ctx->highscore_header_win);
+		ctx->highscore_header_win = NULL;
+	}
+
+	if (ctx->highscore_pad != NULL) {
+		delwin(ctx->highscore_pad);
+		ctx->highscore_pad = NULL;
+	}
+
+	if (ctx->help_header_win != NULL) {
+		delwin(ctx->help_header_win);
+		ctx->help_header_win = NULL;
+	}
+
+	if (ctx->help_pad != NULL) {
+		delwin(ctx->help_pad);
+		ctx->help_pad = NULL;
+	}
+
+}
+
+static int
+all_windows_create(struct rantaiwarna_ctx *ctx)
+{
+	int		width;
+	int		height;
+	char		score_label[] = "Score: ";
+	int		score_label_len;
+	int		header_min_width;
+	char		highscore_title[] = "Highscore";
+	int		highscore_entry_len;
+	char		help_title[] = "Help";
+	int		help_pad_height;
+	int		help_pad_width;
+	int		i;
+	int		line_len;
+	int		help_width;
+	int		help_height;
+	time_t		dummy_time = (time_t)0;
+
+	getmaxyx(stdscr, height, width);
+
+	/* enforce minimum width, both board and headers must be visble */
+	score_label_len = (int)strlen(score_label);
+	header_min_width = 20 + 1 + score_label_len + SCORE_WIN_WIDTH;
+	header_min_width = MAX(header_min_width, (int)strlen(highscore_title));
+	header_min_width = MAX(header_min_width, (int)strlen(help_title));
+	if ((width < ctx->board->width * ELEMENT_WIDTH + 2) ||
+	    (height < 1 + ctx->board->height + 2) ||
+	    (width < header_min_width)) {
+		ctx->too_small = TRUE;
+		ctx->input_win = stdscr;
+		mvaddstr(0, 0, "terminal size is too small");
+		refresh();
+		return (ERR);
+	} else {
+		ctx->too_small = FALSE;
+	}
+
+	/* set up game screen */
+	ctx->game_header_win = newwin(1, width, 0, 0);
+	if (ctx->game_header_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	wbkgd(ctx->game_header_win, A_UNDERLINE | A_BOLD);
+	mvwaddstr(ctx->game_header_win, 0, width - (score_label_len +
+	    SCORE_WIN_WIDTH), score_label);
+
+	ctx->score_win = derwin(ctx->game_header_win, 1, SCORE_WIN_WIDTH, 0,
+	    width - SCORE_WIN_WIDTH);
+	if (ctx->score_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	score_win_render(ctx);
+
+	ctx->message_win = derwin(ctx->game_header_win, 1, width - (1 +
+	    score_label_len + SCORE_WIN_WIDTH), 0, 0);
+	if (ctx->message_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	message_win_render(ctx);
+
+	ctx->board_container_win = newwin(ctx->board->height + 2,
+	    ctx->board->width * ELEMENT_WIDTH + 2, (height - 1 -
+	    (ctx->board->height + 2)) / 2, (width - (ctx->board->width *
+	    ELEMENT_WIDTH + 2)) / 2);
+	if (ctx->board_container_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	box(ctx->board_container_win, 0, 0);
+
+	ctx->board_win = derwin(ctx->board_container_win, ctx->board->height,
+	    ctx->board->width * ELEMENT_WIDTH, 1, 1);
+	if (ctx->board_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	ctx->input_win = ctx->board_win;
+	wbkgd(ctx->board_win, A_BOLD);
+	wtimeout(ctx->board_win, 100);
+	keypad(ctx->board_win, TRUE);
+	board_win_render(ctx);
+
+	board_win_render_cursor(ctx, TRUE);
+
+	/* set up highscore screen */
+	ctx->highscore_header_win = newwin(1, width, 0, 0);
+	if (ctx->highscore_header_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	wbkgd(ctx->highscore_header_win, A_UNDERLINE | A_BOLD);
+	mvwaddstr(ctx->highscore_header_win, 0, 0, highscore_title);
+
+	if (strftime(ctx->dummy_timestr, sizeof (ctx->dummy_timestr), "%x",
+	    gmtime(&dummy_time)) == 0) {
+		ctx->dummy_timestr[0] = '\0';
+	}
+	highscore_entry_len = snprintf(NULL, 0, HIGHSCORE_FMT, 1,
+	    ctx->dummy_timestr, 0);
+	if (highscore_entry_len < 0) {
+		rantaiwarna_err(1, NULL);
+	}
+	ctx->highscore_pad = newpad(10, highscore_entry_len);
+	if (ctx->highscore_pad == NULL) {
+		rantaiwarna_errx(1, "could not create pad");
+	}
+	highscore_pad_update(ctx);
+
+	/* set up help screen */
+	ctx->help_header_win = newwin(1, width, 0, 0);
+	if (ctx->help_header_win == NULL) {
+		rantaiwarna_errx(1, "could not create window");
+	}
+	wbkgd(ctx->help_header_win, A_UNDERLINE | A_BOLD);
+	mvwaddstr(ctx->help_header_win, 0, 0, help_title);
+
+	for (i = 0, help_width = 1; help_strs[i] != NULL; i++) {
+		line_len = (int)strlen(help_strs[i]);
+		if (line_len > help_width) {
+			help_width = line_len;
+		}
+	}
+	help_height = i;
+	ctx->help_pad = newpad(help_height, help_width);
+	if (ctx->help_pad == NULL) {
+		rantaiwarna_errx(1, "could not create pad");
+	}
+
+	for (i = 0; help_strs[i] != NULL; i++) {
+		mvwaddstr(ctx->help_pad, i, 0, help_strs[i]);
+	}
+	getmaxyx(ctx->help_pad, help_pad_height, help_pad_width);
+	if (ctx->help_pos_y + height - 1 > help_height) {
+		ctx->help_pos_y = MAX(help_height - height - 1, 0);
+	}
+	if (ctx->help_pos_x + width > help_width) {
+		ctx->help_pos_x = MAX(help_width - width, 0);
+	}
+
+	active_screen_switch(ctx);
+
+	return (OK);
+}
+
+static void
+handle_resize(struct rantaiwarna_ctx *ctx)
+{
+	all_windows_delete(ctx);
+	clear();
+	endwin();
+	refresh();
+	if (all_windows_create(ctx) == OK) {
+		rantaiwarna_doupdate(ctx);
+	}
+}
+
+static int
+main_loop(struct rantaiwarna_ctx *ctx)
+{
+	sigset_t	sigset;
+	sigset_t	old_sigset;
+	ssize_t		n;
+	int		saved_errno;
+	int		c;
+
+	while (TRUE) {
+		/*
+		 * deal with pending signals previously received in the signal
+		 * handler, try to read a sigset from the pipe, avoid partial
+		 * reads by blocking all signals during the read operation
+		 */
+		sigfillset(&sigset);
+		sigprocmask(SIG_BLOCK, &sigset, &old_sigset);
+		n = read(signal_pipe_fd[PIPE_R_FD], &sigset, sizeof (sigset));
+		saved_errno = errno;
+		sigprocmask(SIG_SETMASK, &old_sigset, NULL);
+		if (n == -1) {
+			if (saved_errno != EAGAIN) {
+				/* unknown error */
+				return (ERR);
+			}
+		} else if ((size_t)n != sizeof (sigset)) {
+			/* short read, should not happen */
+			return (ERR);
+		} else {
+			if ((sigismember(&sigset, SIGINT) == 1) ||
+			    (sigismember(&sigset, SIGTERM) == 1) ||
+			    (sigismember(&sigset, SIGQUIT) == 1) ||
+			    (sigismember(&sigset, SIGHUP) == 1)) {
+				return (ERR);
+			}
+#ifdef SIGWINCH
+			if (sigismember(&sigset, SIGWINCH) == 1) {
+				/* handle terminal resize */
+				handle_resize(ctx);
+			}
+#endif /* SIGWINCH */
+		}
+
+		/* handle keyboard and mouse input */
+		c = wgetch(ctx->input_win);
+		/* only allow to quit if the terminal is too small */
+		if (ctx->too_small) {
+			if (c == 'q') {
+				return (OK);
+			} else {
+				continue;
+			}
+		} else {
+		/* handle global keys */
+			switch (c) {
+			case ERR:
+				/* no input */
+				continue;
+			case 'q':
+				/* quit */
+				return (OK);
+			case '\f':
+				/* refresh */
+				handle_resize(ctx);
+				continue;
+			default:
+				/*
+				 * delegate input handling to screen-specific
+				 * handler
+				 */
+				switch (ctx->active_screen) {
+				case GAME_SCREEN:
+					if (game_handle_input(ctx, c) == ERR) {
+						return (ERR);
+					}
+					break;
+				case HIGHSCORE_SCREEN:
+					if (highscore_handle_input(ctx, c) ==
+					    ERR) {
+						return (ERR);
+					}
+					break;
+				case HELP_SCREEN:
+					if (help_handle_input(ctx, c) == ERR) {
+						return (ERR);
+					}
+					break;
+				}
+			}
+		}
+	}
+}
+
+int
+main(int argc, char *argv[])
+{
+	int	status = EXIT_FAILURE;
+	struct rantaiwarna_ctx *ctx = NULL;
+	int	c;
+	bool	errflag = FALSE;
+	char	*endptr;
+	long	colors = DEFAULT_COLORS;
+	long	height = DEFAULT_HEIGHT;
+	long	width = DEFAULT_WIDTH;
+	int	flags;
+	struct sigaction sigact;
+
+	setlocale(LC_ALL, "");
+
+	if ((progname = strrchr(argv[0], '/')) == NULL) {
+		progname = argv[0];
+	} else {
+		progname++;
+	}
+
+	while (!errflag && (c = getopt(argc, argv, "c:h:w:")) != -1) {
+		switch (c) {
+		case 'c':
+			errno = 0;
+			colors = strtol(optarg, &endptr, 10);
+			if ((errno != 0) || (*endptr != '\0') || (colors < 2) ||
+			    (colors > COLORS_MAX - 1)) {
+				errflag = TRUE;
+			}
+			break;
+		case 'h':
+			errno = 0;
+			height = strtol(optarg, &endptr, 10);
+			if ((errno != 0) || (*endptr != '\0') ||
+			    (height < HEIGHT_MIN) || (height > HEIGHT_MAX)) {
+				errflag = TRUE;
+			}
+			break;
+		case 'w':
+			errno = 0;
+			width = strtol(optarg, &endptr, 10);
+			if ((errno != 0) || (*endptr != '\0') ||
+			    (width < WIDTH_MIN) || (width > WIDTH_MAX)) {
+				errflag = TRUE;
+			}
+			break;
+		default:
+			errflag = TRUE;
+		}
+	}
+	if (errflag) {
+		fprintf(stderr, "usage: %s [-c colors] [-w width] "
+		    "[-h height]\n", progname);
+		status = EXIT_USAGE;
+		goto out;
+	}
+
+	ctx = rantaiwarna_malloc(sizeof (struct rantaiwarna_ctx));
+	ctx->board = board_create((int)height, (int)width, (short)colors);
+	if (ctx->board->elements == NULL) {
+		goto out;
+	}
+	ctx->active_screen = GAME_SCREEN;
+	ctx->highscore = NULL;
+	ctx->use_colors = FALSE;
+	ctx->too_small = FALSE;
+	ctx->game_header_win = NULL;
+	ctx->message_win = NULL;
+	ctx->score_win = NULL;
+	ctx->board_container_win = NULL;
+	ctx->board_win = NULL;
+	ctx->highscore_pad = NULL;
+	ctx->highscore_header_win = NULL;
+	ctx->help_pad = NULL;
+	ctx->help_header_win = NULL;
+	ctx->input_win = stdscr;
+	ctx->board_cursor_x = ctx->board_cursor_y = 0;
+	ctx->highscore_pos_x = ctx->highscore_pos_y = 0;
+	ctx->help_pos_x = ctx->help_pos_y = 0;
+	ctx->message[0] = '\0';
+	ctx->dummy_timestr[0] = '\0';
+	ctx->elements_chs = elements_chs;
+
+	highscore_load(&ctx->highscore, ctx->board->height, ctx->board->width,
+	    ctx->board->colors);
+
+	/* create pipe for delivering signals to a listener in the main loop */
+	if (pipe(signal_pipe_fd) == -1) {
+		goto out;
+	}
+	flags = fcntl(signal_pipe_fd[PIPE_R_FD], F_GETFL, 0);
+	if ((flags == -1) || (fcntl(signal_pipe_fd[PIPE_R_FD], F_SETFL,
+	    flags | O_NONBLOCK) == -1)) {
+		goto out;
+	}
+	flags = fcntl(signal_pipe_fd[PIPE_W_FD], F_GETFL, 0);
+	if ((flags == -1) || (fcntl(signal_pipe_fd[PIPE_W_FD], F_SETFL,
+	    flags | O_NONBLOCK) == -1)) {
+		goto out;
+	}
+
+	/* set up signal handler */
+	sigact.sa_handler = on_signal;
+	sigact.sa_flags = SA_RESTART;
+	sigemptyset(&sigact.sa_mask);
+	if ((sigaction(SIGINT, &sigact, NULL) < 0) ||
+	    (sigaction(SIGTERM, &sigact, NULL) < 0) ||
+	    (sigaction(SIGQUIT, &sigact, NULL) < 0) ||
+	    (sigaction(SIGHUP, &sigact, NULL) < 0)) {
+		goto out;
+	}
+#ifdef SIGWINCH
+	if (sigaction(SIGWINCH, &sigact, NULL) < 0) {
+		goto out;
+	}
+#endif /* SIGWINCH */
+
+	/* initialize curses */
+	initscr();
+	curses_init = TRUE;
+	cbreak();
+	noecho();
+	intrflush(stdscr, FALSE);
+	nonl();
+	curs_set(0);
+	timeout(100);
+	keypad(stdscr, TRUE);
+#ifdef NCURSES_MOUSE_VERSION
+	mousemask(REPORT_MOUSE_POSITION | ALL_MOUSE_EVENTS, NULL);
+	mouseinterval(0);
+#endif /* NCURSES_MOUSE_VERSION */
+	if (has_colors()) {
+		start_color();
+		if ((COLORS >= 8) && (COLOR_PAIRS >= 12)) {
+			ctx->use_colors = TRUE;
+			/* colors 0 -- COLORS_MAX correspond to elements */
+			init_pair(1, COLOR_RED, COLOR_RED);
+			init_pair(2, COLOR_GREEN, COLOR_GREEN);
+			init_pair(3, COLOR_YELLOW, COLOR_YELLOW);
+			init_pair(4, COLOR_BLUE, COLOR_BLUE);
+			init_pair(5, COLOR_MAGENTA, COLOR_MAGENTA);
+			/*
+			 * colors COLORS_MAX -- (COLORS_MAX + COLORS_MAX)
+			 * are used for the cursor to highlight elements
+			 */
+			init_pair(6, COLOR_WHITE, COLOR_BLACK);
+			init_pair(7, COLOR_WHITE, COLOR_RED);
+			init_pair(8, COLOR_WHITE, COLOR_GREEN);
+			init_pair(9, COLOR_WHITE, COLOR_YELLOW);
+			init_pair(10, COLOR_WHITE, COLOR_BLUE);
+			init_pair(11, COLOR_WHITE, COLOR_MAGENTA);
+		}
+	}
+
+	set_message(ctx, "rantaiwarna");
+	if (board_generate(ctx->board) == ERR) {
+		goto out;
+	}
+
+	if (all_windows_create(ctx) == ERR) {
+		goto out;
+	}
+	rantaiwarna_doupdate(ctx);
+
+	if (main_loop(ctx) == OK) {
+		status = EXIT_SUCCESS;
+	}
+
+out:
+	restore_term();
+	all_windows_delete(ctx);
+
+	if (signal_pipe_fd[PIPE_R_FD] != -1) {
+		close(signal_pipe_fd[PIPE_R_FD]);
+	}
+	if (signal_pipe_fd[PIPE_W_FD] != -1) {
+		close(signal_pipe_fd[PIPE_W_FD]);
+	}
+
+	if (ctx != NULL) {
+		board_free(ctx->board);
+		highscore_free(ctx->highscore);
+	}
+	free(ctx);
+
+	exit(status);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rantaiwarna.h	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	RANTAIWARNA_H
+#define	RANTAIWARNA_H
+
+/*
+ * select C99/POSIX:2004-compatible versions of vsnprintf/snprintf on UnixWare 7
+ */
+#if defined(__SCO_VERSION__) && !defined(__OPENSERVER__)
+#define	vsnprintf	_xvsnprintf
+#define	snprintf	_xsnprintf
+#endif /* defined(__SCO_VERSION__) && !defined(__OPENSERVER__) */
+
+extern char	*progname;
+extern bool	curses_init;
+
+#endif /* RANTAIWARNA_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.c	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	_XOPEN_SOURCE	600
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <curses.h>
+#include <term.h>
+#include <stdarg.h>
+
+#include "util.h"
+#include "compat.h"
+#include "rantaiwarna.h"
+
+void
+restore_term(void) {
+	if (curses_init) {
+		endwin();
+		curses_init = FALSE;
+		curs_set(1);
+	}
+}
+
+void
+rantaiwarna_err(int eval, const char *fmt, ...)
+{
+	int	saved_errno = errno;
+	va_list	args;
+
+	restore_term();
+
+	fprintf(stderr, "%s: ", progname);
+	va_start(args, fmt);
+	if (fmt != NULL) {
+		vfprintf(stderr, fmt, args);
+		fprintf(stderr, ": ");
+	}
+	va_end(args);
+	fprintf(stderr, "%s\n", strerror(saved_errno));
+
+	exit(eval);
+}
+
+void
+rantaiwarna_errx(int eval, const char *fmt, ...)
+{
+	va_list	args;
+
+	restore_term();
+
+	fprintf(stderr, "%s: ", progname);
+	va_start(args, fmt);
+	if (fmt != NULL) {
+		vfprintf(stderr, fmt, args);
+	}
+	va_end(args);
+	fputc('\n', stderr);
+
+	exit(eval);
+}
+
+void *
+rantaiwarna_malloc(size_t size)
+{
+	void	*ptr;
+
+	ptr = malloc(size);
+	if (ptr == NULL) {
+		rantaiwarna_err(1, NULL);
+	}
+	return (ptr);
+}
+
+void *
+rantaiwarna_calloc(size_t nelem, size_t elsize)
+{
+	void	*ptr;
+
+	ptr = calloc(nelem, elsize);
+	if (ptr == NULL) {
+		rantaiwarna_err(1, NULL);
+	}
+	return (ptr);
+}
+
+uint32_t
+rantaiwarna_rand(uint32_t *seed)
+{
+	uint32_t	x = *seed;
+
+	x ^= x << 13;
+	x ^= x >> 17;
+	x ^= x << 5;
+	*seed = x;
+
+	return (x);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.h	Sat Mar 15 18:41:03 2014 +0100
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	UTIL_H
+#define	UTIL_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define	MAX(a, b)	(((a) > (b)) ? (a) : (b))
+#define	MIN(a, b)	(((a) < (b)) ? (a) : (b))
+
+void		restore_term(void);
+void		rantaiwarna_err(int, const char *, ...);
+void		rantaiwarna_errx(int, const char *, ...);
+void *		rantaiwarna_malloc(size_t);
+void *		rantaiwarna_calloc(size_t, size_t);
+uint32_t	rantaiwarna_rand(uint32_t *);
+
+#endif /* UTIL_H */