xref: /dragonfly/games/cgram/cgram.c (revision 18d09f18)
1 /* $NetBSD: cgram.c,v 1.11 2021/02/21 22:21:56 rillig Exp $ */
2 
3 /*-
4  * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc.
5  * All rights reserved.
6  *
7  * This code is derived from software contributed to The NetBSD Foundation
8  * by David A. Holland and Roland Illig.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  * 1. Redistributions of source code must retain the above copyright
14  *    notice, this list of conditions and the following disclaimer.
15  * 2. Redistributions in binary form must reproduce the above copyright
16  *    notice, this list of conditions and the following disclaimer in the
17  *    documentation and/or other materials provided with the distribution.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
20  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
23  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29  * POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 #include <assert.h>
33 #include <ctype.h>
34 #include <curses.h>
35 #include <err.h>
36 #include <stdbool.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <string.h>
40 #include <time.h>
41 
42 #include "pathnames.h"
43 
44 ////////////////////////////////////////////////////////////
45 
46 static char
47 ch_toupper(char ch)
48 {
49 	return (char)toupper((unsigned char)ch);
50 }
51 
52 static char
53 ch_tolower(char ch)
54 {
55 	return (char)tolower((unsigned char)ch);
56 }
57 
58 static bool
59 ch_isalpha(char ch)
60 {
61 	return isalpha((unsigned char)ch) != 0;
62 }
63 
64 static bool
65 ch_islower(char ch)
66 {
67 	return islower((unsigned char)ch) != 0;
68 }
69 
70 static bool
71 ch_isupper(char ch)
72 {
73 	return isupper((unsigned char)ch) != 0;
74 }
75 
76 static int
77 imax(int a, int b)
78 {
79 	return a > b ? a : b;
80 }
81 
82 static int
83 imin(int a, int b)
84 {
85 	return a < b ? a : b;
86 }
87 
88 ////////////////////////////////////////////////////////////
89 
90 struct string {
91 	char *s;
92 	size_t len;
93 	size_t cap;
94 };
95 
96 struct stringarray {
97 	struct string *v;
98 	size_t num;
99 };
100 
101 static void
102 string_init(struct string *s)
103 {
104 	s->s = NULL;
105 	s->len = 0;
106 	s->cap = 0;
107 }
108 
109 static void
110 string_add(struct string *s, char ch)
111 {
112 	if (s->len >= s->cap) {
113 		s->cap = 2 * s->cap + 16;
114 		s->s = realloc(s->s, s->cap);
115 		if (s->s == NULL)
116 			errx(1, "Out of memory");
117 	}
118 	s->s[s->len++] = ch;
119 }
120 
121 static void
122 string_finish(struct string *s)
123 {
124 	string_add(s, '\0');
125 	s->len--;
126 }
127 
128 static void
129 stringarray_init(struct stringarray *a)
130 {
131 	a->v = NULL;
132 	a->num = 0;
133 }
134 
135 static void
136 stringarray_cleanup(struct stringarray *a)
137 {
138 	for (size_t i = 0; i < a->num; i++)
139 		free(a->v[i].s);
140 	free(a->v);
141 }
142 
143 static void
144 stringarray_add(struct stringarray *a, struct string *s)
145 {
146 	size_t num = a->num++;
147 	a->v = realloc(a->v, a->num * sizeof a->v[0]);
148 	if (a->v == NULL)
149 		errx(1, "Out of memory");
150 	a->v[num] = *s;
151 }
152 
153 static void
154 stringarray_dup(struct stringarray *dst, const struct stringarray *src)
155 {
156 	assert(dst->num == 0);
157 	for (size_t i = 0; i < src->num; i++) {
158 		struct string str;
159 		string_init(&str);
160 		for (const char *p = src->v[i].s; *p != '\0'; p++)
161 			string_add(&str, *p);
162 		string_finish(&str);
163 		stringarray_add(dst, &str);
164 	}
165 }
166 
167 ////////////////////////////////////////////////////////////
168 
169 static struct stringarray lines;
170 static struct stringarray sollines;
171 static bool hinting;
172 static int extent_x;
173 static int extent_y;
174 static int offset_x;
175 static int offset_y;
176 static int cursor_x;
177 static int cursor_y;
178 
179 static int
180 cur_max_x(void)
181 {
182 	return (int)lines.v[cursor_y].len - 1;
183 }
184 
185 static int
186 cur_max_y(void)
187 {
188 	return extent_y - 1;
189 }
190 
191 static void
192 readquote(void)
193 {
194 	FILE *f = popen(_PATH_FORTUNE, "r");
195 	if (f == NULL)
196 		err(1, "%s", _PATH_FORTUNE);
197 
198 	struct string line;
199 	string_init(&line);
200 
201 	int ch;
202 	while ((ch = fgetc(f)) != EOF) {
203 		if (ch == '\n') {
204 			string_finish(&line);
205 			stringarray_add(&lines, &line);
206 			string_init(&line);
207 		} else if (ch == '\t') {
208 			string_add(&line, ' ');
209 			while (line.len % 8 != 0)
210 				string_add(&line, ' ');
211 		} else if (ch == '\b') {
212 			if (line.len > 0)
213 				line.len--;
214 		} else {
215 			string_add(&line, (char)ch);
216 		}
217 	}
218 
219 	stringarray_dup(&sollines, &lines);
220 
221 	extent_y = (int)lines.num;
222 	for (int i = 0; i < extent_y; i++)
223 		extent_x = imax(extent_x, (int)lines.v[i].len);
224 
225 	pclose(f);
226 }
227 
228 static void
229 encode(void)
230 {
231 	int key[26];
232 
233 	for (int i = 0; i < 26; i++)
234 		key[i] = i;
235 
236 	for (int i = 26; i > 1; i--) {
237 		int c = (int)(random() % i);
238 		int t = key[i - 1];
239 		key[i - 1] = key[c];
240 		key[c] = t;
241 	}
242 
243 	for (int y = 0; y < extent_y; y++) {
244 		for (char *p = lines.v[y].s; *p != '\0'; p++) {
245 			if (ch_islower(*p))
246 				*p = (char)('a' + key[*p - 'a']);
247 			if (ch_isupper(*p))
248 				*p = (char)('A' + key[*p - 'A']);
249 		}
250 	}
251 }
252 
253 static bool
254 substitute(char ch)
255 {
256 	assert(cursor_x >= 0 && cursor_x < extent_x);
257 	assert(cursor_y >= 0 && cursor_y < extent_y);
258 	if (cursor_x > cur_max_x()) {
259 		beep();
260 		return false;
261 	}
262 
263 	char och = lines.v[cursor_y].s[cursor_x];
264 	if (!ch_isalpha(och)) {
265 		beep();
266 		return false;
267 	}
268 
269 	char loch = ch_tolower(och);
270 	char uoch = ch_toupper(och);
271 	char lch = ch_tolower(ch);
272 	char uch = ch_toupper(ch);
273 
274 	for (int y = 0; y < (int)lines.num; y++) {
275 		for (char *p = lines.v[y].s; *p != '\0'; p++) {
276 			if (*p == loch)
277 				*p = lch;
278 			else if (*p == uoch)
279 				*p = uch;
280 			else if (*p == lch)
281 				*p = loch;
282 			else if (*p == uch)
283 				*p = uoch;
284 		}
285 	}
286 	return true;
287 }
288 
289 ////////////////////////////////////////////////////////////
290 
291 static bool
292 is_solved(void)
293 {
294 	for (size_t i = 0; i < lines.num; i++)
295 		if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
296 			return false;
297 	return true;
298 }
299 
300 static void
301 redraw(void)
302 {
303 	erase();
304 
305 	int max_y = imin(LINES - 1, extent_y - offset_y);
306 	for (int y = 0; y < max_y; y++) {
307 		move(y, 0);
308 
309 		int len = (int)lines.v[offset_y + y].len;
310 		int max_x = imin(COLS - 1, len - offset_x);
311 		const char *line = lines.v[offset_y + y].s;
312 		const char *solline = sollines.v[offset_y + y].s;
313 
314 		for (int x = 0; x < max_x; x++) {
315 			char ch = line[offset_x + x];
316 			bool bold = hinting &&
317 			    ch == solline[offset_x + x] &&
318 			    ch_isalpha(ch);
319 
320 			if (bold)
321 				attron(A_BOLD);
322 			addch(ch);
323 			if (bold)
324 				attroff(A_BOLD);
325 		}
326 		clrtoeol();
327 	}
328 
329 	move(LINES - 1, 0);
330 	if (is_solved())
331 		addstr("*solved* ");
332 	addstr("~ to quit, * to cheat, ^pnfb to move");
333 
334 	move(cursor_y - offset_y, cursor_x - offset_x);
335 
336 	refresh();
337 }
338 
339 static void
340 opencurses(void)
341 {
342 	initscr();
343 	cbreak();
344 	noecho();
345 	keypad(stdscr, true);
346 }
347 
348 static void
349 closecurses(void)
350 {
351 	endwin();
352 }
353 
354 ////////////////////////////////////////////////////////////
355 
356 static void
357 saturate_cursor(void)
358 {
359 	cursor_y = imax(cursor_y, 0);
360 	cursor_y = imin(cursor_y, cur_max_y());
361 
362 	assert(cursor_x >= 0);
363 	cursor_x = imin(cursor_x, cur_max_x());
364 }
365 
366 static void
367 scroll_into_view(void)
368 {
369 	if (cursor_x < offset_x)
370 		offset_x = cursor_x;
371 	if (cursor_x > offset_x + COLS - 1)
372 		offset_x = cursor_x - (COLS - 1);
373 
374 	if (cursor_y < offset_y)
375 		offset_y = cursor_y;
376 	if (cursor_y > offset_y + LINES - 2)
377 		offset_y = cursor_y - (LINES - 2);
378 }
379 
380 static void
381 handle_char_input(int ch)
382 {
383 	if (isascii(ch) && ch_isalpha((char)ch)) {
384 		if (substitute((char)ch)) {
385 			if (cursor_x <= cur_max_x())
386 				cursor_x++;
387 			if (cursor_x > cur_max_x() &&
388 			    cursor_y < cur_max_y()) {
389 				cursor_x = 0;
390 				cursor_y++;
391 			}
392 		}
393 	} else if (cursor_x <= cur_max_x() &&
394 	    ch == lines.v[cursor_y].s[cursor_x]) {
395 		cursor_x++;
396 		if (cursor_x > cur_max_x() &&
397 		    cursor_y < cur_max_y()) {
398 			cursor_x = 0;
399 			cursor_y++;
400 		}
401 	} else {
402 		beep();
403 	}
404 }
405 
406 static bool
407 handle_key(void)
408 {
409 	int ch = getch();
410 
411 	switch (ch) {
412 	case 1:			/* ^A */
413 	case KEY_HOME:
414 		cursor_x = 0;
415 		break;
416 	case 2:			/* ^B */
417 	case KEY_LEFT:
418 		if (cursor_x > 0) {
419 			cursor_x--;
420 		} else if (cursor_y > 0) {
421 			cursor_y--;
422 			cursor_x = cur_max_x();
423 		}
424 		break;
425 	case 5:			/* ^E */
426 	case KEY_END:
427 		cursor_x = cur_max_x();
428 		break;
429 	case 6:			/* ^F */
430 	case KEY_RIGHT:
431 		if (cursor_x < cur_max_x()) {
432 			cursor_x++;
433 		} else if (cursor_y < cur_max_y()) {
434 			cursor_y++;
435 			cursor_x = 0;
436 		}
437 		break;
438 	case 12:		/* ^L */
439 		clear();
440 		break;
441 	case 14:		/* ^N */
442 	case KEY_DOWN:
443 		cursor_y++;
444 		break;
445 	case 16:		/* ^P */
446 	case KEY_UP:
447 		cursor_y--;
448 		break;
449 	case KEY_PPAGE:
450 		cursor_y -= LINES - 2;
451 		break;
452 	case KEY_NPAGE:
453 		cursor_y += LINES - 2;
454 		break;
455 	case '*':
456 		hinting = !hinting;
457 		break;
458 	case '~':
459 		return false;
460 	default:
461 		handle_char_input(ch);
462 		break;
463 	}
464 	return true;
465 }
466 
467 static void
468 init(void)
469 {
470 	stringarray_init(&lines);
471 	stringarray_init(&sollines);
472 	srandom((unsigned int)time(NULL));
473 	readquote();
474 	encode();
475 	opencurses();
476 }
477 
478 static void
479 loop(void)
480 {
481 	for (;;) {
482 		redraw();
483 		if (!handle_key())
484 			break;
485 		saturate_cursor();
486 		scroll_into_view();
487 	}
488 }
489 
490 static void
491 clean_up(void)
492 {
493 	closecurses();
494 	stringarray_cleanup(&sollines);
495 	stringarray_cleanup(&lines);
496 }
497 
498 ////////////////////////////////////////////////////////////
499 
500 int
501 main(void)
502 {
503 	init();
504 	loop();
505 	clean_up();
506 }
507