Mercurial > projects > rantaiwarna
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>> 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 */