xref: /netbsd/games/cgram/cgram.c (revision 288a712f)
1 /* $NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 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 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 <sys/cdefs.h>
33 #if defined(__RCSID) && !defined(lint)
34 __RCSID("$NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 rillig Exp $");
35 #endif
36 
37 #include <assert.h>
38 #include <ctype.h>
39 #include <curses.h>
40 #include <err.h>
41 #include <stdbool.h>
42 #include <stdio.h>
43 #include <stdlib.h>
44 #include <string.h>
45 #include <time.h>
46 
47 #include "pathnames.h"
48 
49 
50 static bool
ch_islower(char ch)51 ch_islower(char ch)
52 {
53 	return ch >= 'a' && ch <= 'z';
54 }
55 
56 static bool
ch_isupper(char ch)57 ch_isupper(char ch)
58 {
59 	return ch >= 'A' && ch <= 'Z';
60 }
61 
62 static bool
ch_isalpha(char ch)63 ch_isalpha(char ch)
64 {
65 	return ch_islower(ch) || ch_isupper(ch);
66 }
67 
68 static char
ch_toupper(char ch)69 ch_toupper(char ch)
70 {
71 	return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
72 }
73 
74 static char
ch_tolower(char ch)75 ch_tolower(char ch)
76 {
77 	return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
78 }
79 
80 static int
imax(int a,int b)81 imax(int a, int b)
82 {
83 	return a > b ? a : b;
84 }
85 
86 static int
imin(int a,int b)87 imin(int a, int b)
88 {
89 	return a < b ? a : b;
90 }
91 
92 ////////////////////////////////////////////////////////////
93 
94 struct string {
95 	char *s;
96 	size_t len;
97 	size_t cap;
98 };
99 
100 struct stringarray {
101 	struct string *v;
102 	size_t num;
103 };
104 
105 static void
string_init(struct string * s)106 string_init(struct string *s)
107 {
108 	s->s = NULL;
109 	s->len = 0;
110 	s->cap = 0;
111 }
112 
113 static void
string_add(struct string * s,char ch)114 string_add(struct string *s, char ch)
115 {
116 	if (s->len >= s->cap) {
117 		s->cap = 2 * s->cap + 16;
118 		s->s = realloc(s->s, s->cap);
119 		if (s->s == NULL)
120 			errx(1, "Out of memory");
121 	}
122 	s->s[s->len++] = ch;
123 }
124 
125 static void
string_finish(struct string * s)126 string_finish(struct string *s)
127 {
128 	string_add(s, '\0');
129 	s->len--;
130 }
131 
132 static void
stringarray_init(struct stringarray * a)133 stringarray_init(struct stringarray *a)
134 {
135 	a->v = NULL;
136 	a->num = 0;
137 }
138 
139 static void
stringarray_done(struct stringarray * a)140 stringarray_done(struct stringarray *a)
141 {
142 	for (size_t i = 0; i < a->num; i++)
143 		free(a->v[i].s);
144 	free(a->v);
145 }
146 
147 static void
stringarray_add(struct stringarray * a,struct string * s)148 stringarray_add(struct stringarray *a, struct string *s)
149 {
150 	size_t num = a->num++;
151 	if (reallocarr(&a->v, a->num, sizeof(a->v[0])) != 0)
152 		errx(1, "Out of memory");
153 	a->v[num] = *s;
154 }
155 
156 static void
stringarray_dup(struct stringarray * dst,const struct stringarray * src)157 stringarray_dup(struct stringarray *dst, const struct stringarray *src)
158 {
159 	assert(dst->num == 0);
160 	for (size_t i = 0; i < src->num; i++) {
161 		struct string str;
162 		string_init(&str);
163 		for (const char *p = src->v[i].s; *p != '\0'; p++)
164 			string_add(&str, *p);
165 		string_finish(&str);
166 		stringarray_add(dst, &str);
167 	}
168 }
169 
170 ////////////////////////////////////////////////////////////
171 
172 static struct stringarray lines;
173 static struct stringarray sollines;
174 static bool hinting;
175 static int extent_x;
176 static int extent_y;
177 static int offset_x;
178 static int offset_y;
179 static int cursor_x;
180 static int cursor_y;
181 
182 static int
cur_max_x(void)183 cur_max_x(void)
184 {
185 	return (int)lines.v[cursor_y].len;
186 }
187 
188 static int
cur_max_y(void)189 cur_max_y(void)
190 {
191 	return extent_y - 1;
192 }
193 
194 static char
char_left_of_cursor(void)195 char_left_of_cursor(void)
196 {
197 	if (cursor_x > 0)
198 		return lines.v[cursor_y].s[cursor_x - 1];
199 	assert(cursor_y > 0);
200 	return '\n'; /* eol of previous line */
201 }
202 
203 static char
char_at_cursor(void)204 char_at_cursor(void)
205 {
206 	if (cursor_x == cur_max_x())
207 		return '\n';
208 	return lines.v[cursor_y].s[cursor_x];
209 }
210 
211 static void
getquote(FILE * f)212 getquote(FILE *f)
213 {
214 	struct string line;
215 	string_init(&line);
216 
217 	int ch;
218 	while ((ch = fgetc(f)) != EOF) {
219 		if (ch == '\n') {
220 			string_finish(&line);
221 			stringarray_add(&lines, &line);
222 			string_init(&line);
223 		} else if (ch == '\t') {
224 			string_add(&line, ' ');
225 			while (line.len % 8 != 0)
226 				string_add(&line, ' ');
227 		} else if (ch == '\b') {
228 			if (line.len > 0)
229 				line.len--;
230 		} else {
231 			string_add(&line, (char)ch);
232 		}
233 	}
234 
235 	stringarray_dup(&sollines, &lines);
236 
237 	extent_y = (int)lines.num;
238 	for (int i = 0; i < extent_y; i++)
239 		extent_x = imax(extent_x, (int)lines.v[i].len);
240 }
241 
242 static void
readfile(const char * name)243 readfile(const char *name)
244 {
245 	FILE *f = fopen(name, "r");
246 	if (f == NULL)
247 		err(1, "%s", name);
248 
249 	getquote(f);
250 
251 	if (fclose(f) != 0)
252 		err(1, "%s", name);
253 }
254 
255 
256 static void
readquote(void)257 readquote(void)
258 {
259 	FILE *f = popen(_PATH_FORTUNE, "r");
260 	if (f == NULL)
261 		err(1, "%s", _PATH_FORTUNE);
262 
263 	getquote(f);
264 
265 	if (pclose(f) != 0)
266 		exit(1); /* error message must come from child process */
267 }
268 
269 static void
encode(void)270 encode(void)
271 {
272 	int key[26];
273 
274 	for (int i = 0; i < 26; i++)
275 		key[i] = i;
276 
277 	for (int i = 26; i > 1; i--) {
278 		int c = (int)(random() % i);
279 		int t = key[i - 1];
280 		key[i - 1] = key[c];
281 		key[c] = t;
282 	}
283 
284 	for (int y = 0; y < extent_y; y++) {
285 		for (char *p = lines.v[y].s; *p != '\0'; p++) {
286 			if (ch_islower(*p))
287 				*p = (char)('a' + key[*p - 'a']);
288 			if (ch_isupper(*p))
289 				*p = (char)('A' + key[*p - 'A']);
290 		}
291 	}
292 }
293 
294 static void
substitute(char a,char b)295 substitute(char a, char b)
296 {
297 	char la = ch_tolower(a);
298 	char ua = ch_toupper(a);
299 	char lb = ch_tolower(b);
300 	char ub = ch_toupper(b);
301 
302 	for (int y = 0; y < (int)lines.num; y++) {
303 		for (char *p = lines.v[y].s; *p != '\0'; p++) {
304 			if (*p == la)
305 				*p = lb;
306 			else if (*p == ua)
307 				*p = ub;
308 			else if (*p == lb)
309 				*p = la;
310 			else if (*p == ub)
311 				*p = ua;
312 		}
313 	}
314 }
315 
316 static bool
is_solved(void)317 is_solved(void)
318 {
319 	for (size_t i = 0; i < lines.num; i++)
320 		if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
321 			return false;
322 	return true;
323 }
324 
325 ////////////////////////////////////////////////////////////
326 
327 static void
redraw(void)328 redraw(void)
329 {
330 	erase();
331 
332 	int max_y = imin(LINES - 1, extent_y - offset_y);
333 	for (int y = 0; y < max_y; y++) {
334 		move(y, 0);
335 
336 		int len = (int)lines.v[offset_y + y].len;
337 		int max_x = imin(COLS - 1, len - offset_x);
338 		const char *line = lines.v[offset_y + y].s;
339 		const char *solline = sollines.v[offset_y + y].s;
340 
341 		for (int x = 0; x < max_x; x++) {
342 			char ch = line[offset_x + x];
343 			bool bold = hinting &&
344 			    (ch == solline[offset_x + x] || !ch_isalpha(ch));
345 
346 			if (bold)
347 				attron(A_BOLD);
348 			addch(ch);
349 			if (bold)
350 				attroff(A_BOLD);
351 		}
352 		clrtoeol();
353 	}
354 
355 	move(LINES - 1, 0);
356 	addstr("~ to quit, * to cheat, ^pnfb to move");
357 
358 	if (is_solved()) {
359 		if (extent_y + 1 - offset_y < LINES - 2)
360 			move(extent_y + 1 - offset_y, 0);
361 		else
362 			addch(' ');
363 		attron(A_BOLD | A_STANDOUT);
364 		addstr("*solved*");
365 		attroff(A_BOLD | A_STANDOUT);
366 	}
367 
368 	move(cursor_y - offset_y, cursor_x - offset_x);
369 
370 	refresh();
371 }
372 
373 ////////////////////////////////////////////////////////////
374 
375 static void
saturate_cursor(void)376 saturate_cursor(void)
377 {
378 	cursor_y = imax(cursor_y, 0);
379 	cursor_y = imin(cursor_y, cur_max_y());
380 
381 	assert(cursor_x >= 0);
382 	cursor_x = imin(cursor_x, cur_max_x());
383 }
384 
385 static void
scroll_into_view(void)386 scroll_into_view(void)
387 {
388 	if (cursor_x < offset_x)
389 		offset_x = cursor_x;
390 	if (cursor_x > offset_x + COLS - 1)
391 		offset_x = cursor_x - (COLS - 1);
392 
393 	if (cursor_y < offset_y)
394 		offset_y = cursor_y;
395 	if (cursor_y > offset_y + LINES - 2)
396 		offset_y = cursor_y - (LINES - 2);
397 }
398 
399 static bool
can_go_left(void)400 can_go_left(void)
401 {
402 	return cursor_y > 0 ||
403 	    (cursor_y == 0 && cursor_x > 0);
404 }
405 
406 static bool
can_go_right(void)407 can_go_right(void)
408 {
409 	return cursor_y < cur_max_y() ||
410 	    (cursor_y == cur_max_y() && cursor_x < cur_max_x());
411 }
412 
413 static void
go_to_prev_line(void)414 go_to_prev_line(void)
415 {
416 	cursor_y--;
417 	cursor_x = cur_max_x();
418 }
419 
420 static void
go_to_next_line(void)421 go_to_next_line(void)
422 {
423 	cursor_x = 0;
424 	cursor_y++;
425 }
426 
427 static void
go_left(void)428 go_left(void)
429 {
430 	if (cursor_x > 0)
431 		cursor_x--;
432 	else if (cursor_y > 0)
433 		go_to_prev_line();
434 }
435 
436 static void
go_right(void)437 go_right(void)
438 {
439 	if (cursor_x < cur_max_x())
440 		cursor_x++;
441 	else if (cursor_y < cur_max_y())
442 		go_to_next_line();
443 }
444 
445 static void
go_to_prev_word(void)446 go_to_prev_word(void)
447 {
448 	while (can_go_left() && !ch_isalpha(char_left_of_cursor()))
449 		go_left();
450 
451 	while (can_go_left() && ch_isalpha(char_left_of_cursor()))
452 		go_left();
453 }
454 
455 static void
go_to_next_word(void)456 go_to_next_word(void)
457 {
458 	while (can_go_right() && ch_isalpha(char_at_cursor()))
459 		go_right();
460 
461 	while (can_go_right() && !ch_isalpha(char_at_cursor()))
462 		go_right();
463 }
464 
465 static bool
can_substitute_here(int ch)466 can_substitute_here(int ch)
467 {
468 	return isascii(ch) &&
469 	    ch_isalpha((char)ch) &&
470 	    cursor_x < cur_max_x() &&
471 	    ch_isalpha(char_at_cursor());
472 }
473 
474 static void
handle_char_input(int ch)475 handle_char_input(int ch)
476 {
477 	if (ch == char_at_cursor())
478 		go_right();
479 	else if (can_substitute_here(ch)) {
480 		substitute(char_at_cursor(), (char)ch);
481 		go_right();
482 	} else
483 		beep();
484 }
485 
486 static bool
handle_key(void)487 handle_key(void)
488 {
489 	int ch = getch();
490 
491 #define CTRL(letter) (letter - 64)
492 	switch (ch) {
493 	case CTRL('A'):
494 	case KEY_BEG:
495 	case KEY_HOME:
496 		cursor_x = 0;
497 		break;
498 	case CTRL('B'):
499 	case KEY_LEFT:
500 		go_left();
501 		break;
502 	case CTRL('E'):
503 	case KEY_END:
504 		cursor_x = cur_max_x();
505 		break;
506 	case CTRL('F'):
507 	case KEY_RIGHT:
508 		go_right();
509 		break;
510 	case '\t':
511 		go_to_next_word();
512 		break;
513 	case KEY_BTAB:
514 		go_to_prev_word();
515 		break;
516 	case '\n':
517 		go_to_next_line();
518 		break;
519 	case CTRL('L'):
520 		clear();
521 		break;
522 	case CTRL('N'):
523 	case KEY_DOWN:
524 		cursor_y++;
525 		break;
526 	case CTRL('P'):
527 	case KEY_UP:
528 		cursor_y--;
529 		break;
530 	case KEY_PPAGE:
531 		cursor_y -= LINES - 2;
532 		break;
533 	case KEY_NPAGE:
534 		cursor_y += LINES - 2;
535 		break;
536 	case '*':
537 		hinting = !hinting;
538 		break;
539 	case '~':
540 		return false;
541 	case KEY_RESIZE:
542 		break;
543 	default:
544 		handle_char_input(ch);
545 		break;
546 	}
547 	return true;
548 }
549 
550 static void
init(const char * filename)551 init(const char *filename)
552 {
553 	stringarray_init(&lines);
554 	stringarray_init(&sollines);
555 	srandom((unsigned int)time(NULL));
556 	if (filename != NULL) {
557 	    readfile(filename);
558 	} else {
559 	    readquote();
560 	}
561 	encode();
562 
563 	initscr();
564 	cbreak();
565 	noecho();
566 	keypad(stdscr, true);
567 }
568 
569 static void
loop(void)570 loop(void)
571 {
572 	for (;;) {
573 		redraw();
574 		if (!handle_key())
575 			break;
576 		saturate_cursor();
577 		scroll_into_view();
578 	}
579 }
580 
581 static void
done(void)582 done(void)
583 {
584 	move(LINES - 1, 0);
585 	clrtoeol();
586 	refresh();
587 
588 	endwin();
589 
590 	stringarray_done(&sollines);
591 	stringarray_done(&lines);
592 }
593 
594 
595 static void __dead
usage(void)596 usage(void)
597 {
598 
599 	fprintf(stderr, "usage: %s [file]\n", getprogname());
600 	exit(1);
601 }
602 
603 int
main(int argc,char * argv[])604 main(int argc, char *argv[])
605 {
606 
607 	setprogname(argv[0]);
608 	if (argc != 1 && argc != 2)
609 		usage();
610 
611 	init(argc > 1 ? argv[1] : NULL);
612 	loop();
613 	done();
614 	return 0;
615 }
616