xref: /titanic_51/usr/src/lib/libshell/common/scripts/gnaw.sh (revision b0e1d44ee433c64fb17d070b16d4886b9595451f)
1#!/usr/bin/ksh93
2
3#
4# CDDL HEADER START
5#
6# The contents of this file are subject to the terms of the
7# Common Development and Distribution License (the "License").
8# You may not use this file except in compliance with the License.
9#
10# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
11# or http://www.opensolaris.org/os/licensing.
12# See the License for the specific language governing permissions
13# and limitations under the License.
14#
15# When distributing Covered Code, include this CDDL HEADER in each
16# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
17# If applicable, add the following below this CDDL HEADER, with the
18# fields enclosed by brackets "[]" replaced with your own identifying
19# information: Portions Copyright [yyyy] [name of copyright owner]
20#
21# CDDL HEADER END
22#
23
24#
25# Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved.
26#
27
28#
29# gnaw - a simple ksh93 technology demo
30#
31# Note that this script has been written with the main idea to show
32# many of ksh93's new features (comparing to ksh88/bash) and not
33# as an example of efficient&&clean script code (much of the code
34# could be done more efficient using compound variables, this script
35# focus is the usage of associative arrays).
36#
37
38# Solaris needs /usr/xpg6/bin:/usr/xpg4/bin because the tools in /usr/bin are not POSIX-conformant
39export PATH=/usr/xpg6/bin:/usr/xpg4/bin:/bin:/usr/bin
40
41# Make sure all math stuff runs in the "C" locale to avoid problems
42# with alternative # radix point representations (e.g. ',' instead of
43# '.' in de_DE.*-locales). This needs to be set _before_ any
44# floating-point constants are defined in this script).
45if [[ "${LC_ALL}" != "" ]] ; then
46    export \
47        LC_MONETARY="${LC_ALL}" \
48        LC_MESSAGES="${LC_ALL}" \
49        LC_COLLATE="${LC_ALL}" \
50        LC_CTYPE="${LC_ALL}"
51        unset LC_ALL
52fi
53export LC_NUMERIC=C
54
55function print_setcursorpos
56{
57    print -n -- "${vtcode[cup_${1}_${2}]}"
58}
59
60function beep
61{
62    ${quiet} || print -n -- "${vtcode["bel"]}"
63}
64
65function fatal_error
66{
67    print -u2 "${progname}: $*"
68    exit 1
69}
70
71# Get terminal size and put values into a compound variable with the integer
72# members "columns" and "lines"
73function get_term_size
74{
75	nameref rect=$1
76
77	rect.columns=${ tput cols ; } || return 1
78	rect.lines=${ tput lines ; }  || return 1
79
80	return 0
81}
82
83function print_levelmap
84{
85    integer screen_y_offset=$1
86    integer start_y_pos=$2 # start at this line in the map
87    integer max_numlines=$3 # maximum lines we're allowed to render
88    integer x
89    integer y
90    typeset line=""
91
92    print_setcursorpos 0 ${screen_y_offset}
93
94    for (( y=start_y_pos; (y-start_y_pos) < max_numlines && y < levelmap["max_y"] ; y++ )) ; do
95        line=""
96        for (( x=0 ; x < levelmap["max_x"] ; x++ )) ; do
97            line+="${levelmap["${x}_${y}"]}"
98        done
99
100        print -- "${line} "
101    done
102
103    # print lines filled with spaces for each line not filled
104    # by the level map
105    line="${vtcode["spaceline"]:0:${levelmap["max_x"]}}"
106    for (( ; (y-start_y_pos) < max_numlines ; y++ )) ; do
107        print -- "${line} "
108    done
109    return 0
110}
111
112function level_completed
113{
114    integer i
115    typeset dummy
116    typeset render_buffer="$(
117    print -n -- "${vtcode["clear"]}"
118    cat <<ENDOFTEXT
119
120 #       ######  #    #  ######  #
121 #       #       #    #  #       #
122 #       #####   #    #  #####   #
123 #       #       #    #  #       #
124 #       #        #  #   #       #
125 ######  ######    ##    ######  ######
126
127             (Good job)
128
129     #####    ####   #    #  ######
130     #    #  #    #  ##   #  #
131     #    #  #    #  # #  #  #####
132     #    #  #    #  #  # #  #
133     #    #  #    #  #   ##  #
134     #####    ####   #    #  ######
135
136
137ENDOFTEXT
138
139    printf "    SCORE: --> %s <--\n" "${player["score"]}"
140    printf "    LIVES: --> %s <--\n" "${player["lives"]}"
141    )"
142    print -- "${render_buffer}${end_of_frame}"
143
144    # wait five seconds and swallow any user input
145    for (( i=0 ; i < 50 ; i++ )) ; do
146        read -r -t 0.1 -n 1 dummy
147    done
148
149    print "Press any key to continue...${end_of_frame}"
150    # wait five secs or for a key
151    read -r -t 5 -n 1 dummy
152    return 0
153}
154
155function game_over
156{
157    typeset dummy
158    typeset render_buffer="$(
159    print -n -- "${vtcode["clear"]}"
160    cat <<ENDOFTEXT
161
162  ####     ##    #    #  ######
163 #    #   #  #   ##  ##  #
164 #       #    #  # ## #  #####
165 #  ###  ######  #    #  #
166 #    #  #    #  #    #  #
167  ####   #    #  #    #  ######
168
169            (LOSER!)
170
171  ####   #    #  ######  #####
172 #    #  #    #  #       #    #
173 #    #  #    #  #####   #    #
174 #    #  #    #  #       #####
175 #    #   #  #   #       #   #
176  ####     ##    ######  #    #
177
178ENDOFTEXT
179
180    printf "\n    SCORE: --> %s <--\n" "${player["score"]}"
181    )"
182    print -r -- "${render_buffer}${end_of_frame}"
183
184    # wait five seconds and swallow any user input
185    for (( i=0 ; i < 50 ; i++ )) ; do
186        read -r -t 0.1 -n 1 dummy
187    done
188
189    print "Press any key to continue...${end_of_frame}"
190    # wait five secs or for a key
191    read -r -t 5 -n 1 dummy
192    return 0
193}
194
195function run_logo
196{
197    typeset render_buffer="$(
198    cat <<ENDOFTEXT
199
200 #####  #     #    #    #     #   ###
201#     # ##    #   # #   #  #  #   ###
202#       # #   #  #   #  #  #  #   ###
203#  #### #  #  # #     # #  #  #    #
204#     # #   # # ####### #  #  #
205#     # #    ## #     # #  #  #   ###
206 #####  #     # #     #  ## ##    ###
207ENDOFTEXT
208    )"
209    print -- "${vtcode["clear"]}${render_buffer}"
210
211    # wait two seconds and swallow any user input
212    for (( i=0 ; i < 20 ; i++ )) ; do
213        read -r -t 0.1 -n 1 dummy
214    done
215
216    print "\n   (The KornShell 93 maze game)"
217
218    attract_mode
219    return 0
220}
221
222function attract_mode
223{
224(
225    # Now present some info, line-by-line in an endless loop
226    # until the user presses a key (we turn the "magic" return
227    # code for that)
228    integer -r magic_return_code=69
229    typeset line
230    IFS='' ; # Make sure we do not swallow whitespaces
231
232    while true ; do
233        (
234            redirect 5<&0
235
236        (cat <<ENDOFTEXT
237
238
239
240
241
242         ################
243     ########################
244   ############################
245  #######     ######     #######
246  ######     ######     ########
247  #######     ######     #######
248  ##############################
249  ##############################
250  ##############################
251  ##############################
252  ##############################
253  #########  ########  #########
254  #  ####      ####      ####  #
255
256
257
258
259
260
261           Written by
262
263          Roland Mainz
264    (roland.mainz@nrubsig.org)
265
266
267
268
269
270
271           ##############         
272      ########################    
273   #################**############
274  ################################
275 ############################     
276 ######################           
277 ################                 
278 ######################           
279 ############################     
280  ################################
281   ############################## 
282      ########################    
283           ##############    
284
285
286
287
288
289
290
291             High scores:
292  
293        * 'chin'      8200 pt
294        * 'gisburn'   7900 pt
295        * 'tpenta'    5520 pt
296        * 'kupfer'    5510 pt
297        * 'noname'    5000 pt
298        * 'noname'    4000 pt
299        * 'livad'     3120 pt
300        * 'noname'    3000 pt
301        * 'noname'    2000 pt
302        * 'noname'    1000 pt
303  
304ENDOFTEXT
305
306        # clear screen, line-by-line
307        for (( i=0 ; i < termsize.lines ; i++ )) ; do print "" ; done
308        ) | (while read -r line ; do
309                read -r -t 0.3 -n 1 c <&5
310                [[ "$c" != "" ]] && exit ${magic_return_code}
311                print -- "${line}"
312            done)
313        (( $? == magic_return_code )) && exit ${magic_return_code}
314        )
315        (( $? == magic_return_code )) && return 0
316
317        sleep 2
318    done
319)
320}
321
322function run_menu
323{
324    integer numlevels=0
325    integer selected_level=0
326    typeset l
327
328    # built list of available levels based on the "function levelmap_.*"
329    # built into this script
330    typeset -f | egrep "^function.*levelmap_.*" | sed 's/^function //' |
331    while read -r l ; do
332        levellist[numlevels]="$l"
333        numlevels+=1
334    done
335
336    # swallow any queued user input (e.g. drain stdin)
337    read -r -t 0.1 -n 100 dummy
338
339    while true ; do
340        # menu loop with timeout (which switches to "attract mode")
341        while true ; do
342            print -n -- "${vtcode["clear"]}"
343
344    cat <<ENDOFTEXT
345>======================================\   
346>  /-\     .--.                        |
347> | OO|   / _.-' .-.   .-.  .-.   .-.  |
348> |   |   \  '-. '-'   '-'  '-'   '-'  |
349> ^^^^^    '--'                        |
350>======\       /================\  .-. |
351>      |       |                |  '-' |
352 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
353ENDOFTEXT
354            print "    GNAW - the ksh93 maze game"
355            print "\n\tMenu:"
356
357            print "\t - [L]evels:"
358            for (( i=0 ; i < numlevels ; i++ )) ; do
359                printf "\t    %s %s \n" "$( (( i == selected_level )) && print -n "*" || print -n " ")" "${levellist[i]##levelmap_}"
360            done
361
362            print  "\t - Rendering options:"
363            printf "\t    [%s] Use [U]nicode\n" "$( (( game_use_unicode == 1 )) && print -n "x" || print -n "_" )"
364            printf "\t    [%s] Use [C]olors\n"  "$( (( game_use_colors  == 1 )) && print -n "x" || print -n "_" )"
365
366            print "\t - [S]tart - [Q]uit"
367
368            # wait 30 secs (before we switch to "attract mode")
369            c="" ; read -r -t 30 -n 1 c
370            case "$c" in
371                'l') (( selected_level=(selected_level+numlevels+1) % numlevels )) ;;
372                'L') (( selected_level=(selected_level+numlevels-1) % numlevels )) ;;
373                ~(Fi)s)
374                    (( game_use_colors == 1 )) && print -- "${vtcode["bg_black"]}"
375                    case "${game_use_colors}${game_use_unicode}" in
376                        "00") main_loop "${levellist[selected_level]}" ;;
377                        "01") main_loop "${levellist[selected_level]}" | map_filter 0 1 ;;
378                        "10") main_loop "${levellist[selected_level]}" | map_filter 1 0 ;;
379                        "11") main_loop "${levellist[selected_level]}" | map_filter 1 1 ;;
380                    esac
381                    print -- "${vtcode["vtreset"]}"
382                    ;;
383                ~(Fi)q | $'\E')
384                    # make sure we do not exit on a cursor key (e.g. <esc>[A,B,C,D)
385                    read -r -t 0.01 -n 1 c
386                    if [[ "$c" == "[" ]] ; then
387                        # this was a cursor key sequence, just eat the 3rd charcater
388                        read -r -t 0.01 -n 1 c
389                    else
390                        exit 0
391                    fi
392                    ;;
393                ~(Fi)u) (( game_use_unicode=(game_use_unicode+2+1) % 2)) ;;
394                ~(Fi)c) (( game_use_colors=(game_use_colors+2+1) % 2))   ;;
395                "") break ;; # timeout, switch to attract mode
396                *) beep ;;
397            esac
398        done
399
400        print -n -- "${vtcode["clear"]}"
401        attract_mode
402    done
403    return 0
404}
405
406function levelmap_stripes
407{
408cat <<ENDOFLEVEL
409###################################
410#.......    ...............    P  #
411#########..#################..### #
412#########..#################..### #
413#.......    ..    ..............# #
414###############  ################ #
415###############  ################ #
416#............. M  ..............# #
417##..#####################  ###### #
418##..#####################  ###### #
419#.......  ...........    .......# #
420########  ############  ######### #
421#   ####  ############  ######### #
422# #..................     ......# #
423# ############################### # 
424#                                 #
425###################################
426ENDOFLEVEL
427    return 0
428}
429
430function levelmap_livad
431{
432cat <<ENDOFLEVEL
433#####################################################
434#                                                   #
435# ##############  ###############  ################ #
436# #............       P             ..............# #
437#  .#############################################.# #
438# #.#..........                     ............#.  #
439# #.#.##########  ###############  ############.#.# #
440# #...#........                     ..........#...# #
441# #...#.#####################################.#.#.# #
442# #...#.#......                     ........#...#.# #
443# #.#.#...######  #########################.#.#.#.# #
444#  .#.....#....          M          ......#...#.#.# #
445# #.#.#...#######################  ########.#.#.#.# #
446# #...#.#......                     ........#...#.# #
447# #...#.########  ###############  ##########.#.#.# #
448# #...#........                     ..........#...# #
449# #.#.#########################################.#.# #
450# #.#..........                     ............#.  #
451#  .############  ###############  ##############.# #
452# #............                     ..............# #
453# ################################################# #
454#                                                   #
455#####################################################
456ENDOFLEVEL
457    return 0
458}
459
460function levelmap_classic1
461{
462cat <<ENDOFLEVEL
463#########################
464#.P.........#...........#
465#.####.####.#.####.####.#
466#.#  #.#  #.#.#  #.#  #.#
467#.#  #.#  #.#.#  #.#  #.#
468#.####.####.#.####.####.#
469#.......................#
470#.####.#.#######.#.####.#
471#.#  #.#.#     #.#.#  #.#
472#.####.#.#######.#.####.#
473#......#....#....#......#
474######.####.#.####.######
475###### #         # ######
476###### # ##   ## # ######
477###### # #     # # ######
478#        #  M  #        #
479###### # ####### # ######
480###### #         # ######
481###### # ####### # ######
482###### # #     # # ######
483######.#.#######.#.######
484#...........#...........#
485#.###.###...#...###.###.#
486#...#...............#...#
487###.#....#######....#.###
488# #.#..#.#     #.#..#.# #
489###....#.#######.#....###
490#......#....#....#......#
491#.#########.#.#########.#
492#.......................#
493#########################
494ENDOFLEVEL
495    return 0
496}
497
498function levelmap_classic2
499{
500cat <<ENDOFLEVEL
501#######################
502#.P...#.........#.....#
503#.###.#.#######.#.###.#
504#.....................#
505###.#.####.#.####.#.###
506###.#......#......#.###
507###.###.#######.###.###
508###.................###
509###.###.### ###.###.###
510###.#...#M    #...#.###
511###.#.#.#######.#.#.###
512#.....#.........#.....#
513###.#####..#..#####.###
514###........#........###
515###.###.#######.###.###
516#.....................#
517#.###.####.#.####.###.#
518#.###.#....#....#.###.#
519#.###.#.#######.#.###.#
520#.....................#
521#######################
522ENDOFLEVEL
523    return 0
524}
525
526function levelmap_easy
527{
528cat <<ENDOFLEVEL
529##################
530# .............. #
531# . ######       #
532# . #  M #       #
533# . #    #       #
534# . ### ##       #
535# .   #          #
536# .   ###        #
537# .              #
538# ..........     #
539# .......... P   #
540##################
541ENDOFLEVEL
542    return 0
543}
544
545function levelmap_sunsolaristext
546{
547cat <<ENDOFLEVEL
548################################################
549# .####   .    #  #....#                       #
550# #       #    #  #....#                       #
551#  ####   #    #  #.#..#          M            #
552#      #  #    #  #..#.#                       #
553# #    #  #    #  #...##                       #
554#  ####    ####   #....#                       #
555#                                              #
556#  ####    ####  #       ##    #####  #  ####  #
557# #       #.  .# #      #  #   #....# # #      #
558#  ####   #    # #      # P #  #....# #  ####  #
559#      #  #    ###      #.#### #.###  #      # #
560# #   .#  #.  ..        #    # #...#  # #    # #
561#  ####    ####  ###### .    #  ....# #  ####. #
562################################################
563ENDOFLEVEL
564    return 0
565}
566
567function read_levelmap
568{
569    typeset map="$( $1 )"
570
571    integer y=0
572    integer x=0
573    integer maxx=0
574    integer numdots=0
575    typeset line
576    typeset c
577
578    while read -r line ; do
579        for (( x=0 ; x < ${#line} ; x++ )) ; do
580            c="${line:x:1}"
581
582            case $c in
583                ".") numdots+=1 ;;
584                "M")
585		    # log start position of monsters
586                    levelmap["monsterstartpos_x"]="$x"
587                    levelmap["monsterstartpos_y"]="$y"
588                    c=" "
589                    ;;
590                "P")
591		    # log start position of player
592                    levelmap["playerstartpos_x"]="$x"
593                    levelmap["playerstartpos_y"]="$y"
594                    c=" "
595                    ;;
596            esac
597
598            levelmap["${x}_${y}"]="$c"
599        done
600        (( maxx=x , y++ ))
601    done <<<"${map}"
602
603    levelmap["max_x"]=${maxx}
604    levelmap["max_y"]=${y}
605    levelmap["numdots"]=${numdots}
606
607    # consistency checks
608    if [[ "${levelmap["monsterstartpos_x"]}" == "" ]] ; then
609        fatal_error "read_levelmap: monsterstartpos_x is empty."
610    fi
611    if [[ "${levelmap["playerstartpos_x"]}" == "" ]] ; then
612        fatal_error "read_levelmap: playerstartpos_x is empty."
613    fi
614
615    return 0
616}
617
618function player.set
619{
620    case "${.sh.subscript}" in
621        pos_y)
622            if [[ "${levelmap["${player["pos_x"]}_${.sh.value}"]}" == "#" ]] ; then
623                .sh.value=${player["pos_y"]}
624                beep
625            fi
626            ;;
627
628        pos_x)
629            if [[ "${levelmap["${.sh.value}_${player["pos_y"]}"]}" == "#" ]] ; then
630                .sh.value=${player["pos_x"]}
631                beep
632            fi
633            ;;
634    esac
635    return 0
636}
637
638function monster.set
639{
640    case "${.sh.subscript}" in
641        *_pos_y)
642            if [[ "${levelmap["${monster[${currmonster}_"pos_x"]}_${.sh.value}"]}" == "#" ]] ; then
643                .sh.value=${monster[${currmonster}_"pos_y"]}
644                # turn homing off when the monster hit a wall
645                monster[${currmonster}_"homing"]=0
646            fi
647            ;;
648
649        *_pos_x)
650            if [[ "${levelmap["${.sh.value}_${monster[${currmonster}_"pos_y"]}"]}" == "#" ]] ; then
651                .sh.value=${monster[${currmonster}_"pos_x"]}
652                # turn homing off when the monster hit a wall
653                monster[${currmonster}_"homing"]=0
654            fi
655            ;;
656    esac
657    return 0
658}
659
660function render_game
661{
662    # render_buffer is some kind of "background buffer" to "double buffer"
663    # all output and combine it in one write to reduce flickering in the
664    # terminal
665    typeset render_buffer="$(
666        integer screen_y_offset=1
667        integer start_y_pos=0
668        integer render_num_lines=${levelmap["max_y"]}
669
670        if (( (termsize.lines-3) < levelmap["max_y"] )) ; then
671            (( start_y_pos=player["pos_y"] / 2))
672            (( render_num_lines=termsize.lines-5))
673        fi
674
675        #print -n -- "${vtcode["clear"]}"
676        print_setcursorpos 0 0
677
678        # print score (note the " " around "%d" are neccesary to clean up cruft
679        # when we overwrite the level
680        printf "SCORE: %05d  DOTS: %.3d  LIVES: %2.d " "${player["score"]}" "${levelmap["numdots"]}" "${player["lives"]}"
681        print_levelmap ${screen_y_offset} ${start_y_pos} ${render_num_lines}
682
683        # render player
684        print_setcursorpos ${player["pos_x"]} $((player["pos_y"]+screen_y_offset-start_y_pos))
685        print -n "@"
686
687        # render monsters
688        for currmonster in ${monsterlist} ; do
689            (( m_pos_x=monster[${currmonster}_"pos_x"] ))
690            (( m_pos_y=monster[${currmonster}_"pos_y"]+screen_y_offset-start_y_pos ))
691
692            if (( m_pos_y >= screen_y_offset && m_pos_y < render_num_lines )) ; then
693                print_setcursorpos ${m_pos_x} ${m_pos_y}
694                print -n "x"
695            fi
696        done
697
698        # status block
699        print_setcursorpos 0 $((render_num_lines+screen_y_offset))
700        emptyline="                                                            "
701        print -n " >> ${player["message"]} <<${emptyline:0:${#emptyline}-${#player["message"]}}"
702    )"
703    print -r -- "${render_buffer}${end_of_frame}"
704#    print "renderbuffersize=$(print "${render_buffer}" | wc -c) ${end_of_frame}"
705    return 0
706}
707
708function main_loop
709{
710    float   sleep_per_cycle=0.2
711    float   seconds_before_read
712    integer num_cycles=0
713    float   rs
714
715    print -n -- "${vtcode["clear"]}"
716
717    read_levelmap "$1"
718
719    # player init
720    player["pos_x"]=${levelmap["playerstartpos_x"]}
721    player["pos_y"]=${levelmap["playerstartpos_y"]}
722    player["score"]=0         # player score
723    player["lives"]=5         # number of lives
724    player["invulnerable"]=10 # cycles how long the player remains invulnerable
725    player["message"]="Go..."
726
727    monsterlist="maw claw jitterbug tentacle grendel"
728
729    for currmonster in ${monsterlist} ; do
730        monster[${currmonster}_"pos_x"]=${levelmap["monsterstartpos_x"]}
731        monster[${currmonster}_"pos_y"]=${levelmap["monsterstartpos_y"]}
732        monster[${currmonster}_"xstep"]=0
733        monster[${currmonster}_"ystep"]=0
734        monster[${currmonster}_"homing"]=0
735    done
736
737    # main game cycle loop
738    while true ; do
739        num_cycles+=1
740        seconds_before_read=${SECONDS}
741        c="" ; read -r -t ${sleep_per_cycle} -n 1 c
742
743        if [[ "$c" != "" ]] ; then
744            # special case handling for cursor keys which are usually composed
745            # of three characters (e.g. "<ESC>[D"). If only <ESC> is hit we
746            # quicky exit
747            if [[ "$c" == $'\E' ]] ; then
748                read -r -t 0.1 -n 1 c
749                if [[ "$c" != "[" ]] ; then
750                    return 0
751                fi
752
753                # we assume the user is using the cursor keys, this |read|
754                # should fetch the 3rd byte of the three-character sequence
755                # for the cursor keys
756                read -r -t 0.1 -n 1 c
757            fi
758
759            # if the user hit a key the "read" above was interrupted
760            # and didn't wait exactly |sleep_per_cycle| seconds.
761            # We wait here some moments (|rs|="remaining seconds") to
762            # avoid that the game gets "faster" when more user input
763            # is given.
764            (( rs=sleep_per_cycle-(SECONDS-seconds_before_read) ))
765            (( rs > 0.001 )) && sleep ${rs}
766
767            player["message"]=""
768
769            case "$c" in
770                j|D|4) (( player["pos_x"]-=1 )) ;;
771                k|C|6) (( player["pos_x"]+=1 )) ;;
772                i|A|8) (( player["pos_y"]-=1 )) ;;
773                m|B|2) (( player["pos_y"]+=1 )) ;;
774
775                q) return 0 ;;
776            esac
777
778            if [[ "${levelmap["${player["pos_x"]}_${player["pos_y"]}"]}" == "." ]] ; then
779                levelmap["${player["pos_x"]}_${player["pos_y"]}"]=" "
780                (( levelmap["numdots"]-=1 ))
781
782                (( player["score"]+=10 ))
783                player["message"]='GNAW!!'
784
785                if (( levelmap["numdots"] <= 0 )) ; then
786                    level_completed
787                    return 0
788                fi
789            fi
790        fi
791
792        # generic player status change
793        if (( player["invulnerable"] > 0 )) ; then
794            (( player["invulnerable"]-=1 ))
795        fi
796        if (( player["lives"] <= 0 )) ; then
797            game_over
798            return 0
799        fi
800
801        # move monsters
802        for currmonster in ${monsterlist} ; do
803            # make monster as half as slow then the others when it is following the user
804            if (( monster[${currmonster}_"homing"] > 0 )) ; then
805                (( (num_cycles%2) > 0 )) && continue
806            fi
807
808            if [[ ${monster[${currmonster}_"pos_x"]} == ${player["pos_x"]} ]] ; then
809                if (( (monster[${currmonster}_"pos_y"]-player["pos_y"]) > 0 )) ; then
810                    (( monster[${currmonster}_"xstep"]=+0 , monster[${currmonster}_"ystep"]=-1 ))
811                else
812                    (( monster[${currmonster}_"xstep"]=+0 , monster[${currmonster}_"ystep"]=+1 ))
813                fi
814                monster[${currmonster}_"homing"]=1
815                if (( player["invulnerable"] <= 0 )) ; then
816                    player["message"]="Attention: ${currmonster} is chasing you"
817                fi
818            elif (( monster[${currmonster}_"pos_y"] == player["pos_y"] )) ; then
819                if (( (monster[${currmonster}_"pos_x"]-player["pos_x"]) > 0 )) ; then
820                    (( monster[${currmonster}_"xstep"]=-1 , monster[${currmonster}_"ystep"]=-0 ))
821                else
822                    (( monster[${currmonster}_"xstep"]=+1 , monster[${currmonster}_"ystep"]=+0 ))
823                fi
824                monster[${currmonster}_"homing"]=1
825                if (( player["invulnerable"] <= 0 )) ; then
826                    player["message"]="Attention: ${currmonster} is chasing you"
827                fi
828            else
829                if (( monster[${currmonster}_"homing"] == 0 )) ; then
830                    case $((SECONDS % 6 + RANDOM % 4)) in
831                        0) (( monster[${currmonster}_"xstep"]=+0 , monster[${currmonster}_"ystep"]=+0 )) ;;
832                        2) (( monster[${currmonster}_"xstep"]=+0 , monster[${currmonster}_"ystep"]=+1 )) ;;
833                        3) (( monster[${currmonster}_"xstep"]=+1 , monster[${currmonster}_"ystep"]=+0 )) ;;
834                        5) (( monster[${currmonster}_"xstep"]=+0 , monster[${currmonster}_"ystep"]=-1 )) ;;
835                        6) (( monster[${currmonster}_"xstep"]=-1 , monster[${currmonster}_"ystep"]=+0 )) ;;
836                    esac
837                fi
838            fi
839
840            (( monster[${currmonster}_"pos_x"]=monster[${currmonster}_"pos_x"]+monster[${currmonster}_"xstep"] ))
841            (( monster[${currmonster}_"pos_y"]=monster[${currmonster}_"pos_y"]+monster[${currmonster}_"ystep"] ))
842
843            # check if a monster hit the player
844            if (( player["invulnerable"] <= 0 )) ; then
845                if (( monster[${currmonster}_"pos_x"] == player["pos_x"] && \
846                      monster[${currmonster}_"pos_y"] == player["pos_y"] )) ; then
847                     # if player was hit by a monster take one life and
848                     # make him invulnerable for 10 cycles to avoid that
849                     # the next cycle steals more lives
850                     player["message"]="Ouuuchhhh"
851                     player["invulnerable"]=10
852                     (( player["lives"]-=1 ))
853
854                     beep ; beep ; sleep 0.2 ; beep ; beep
855                fi
856            fi
857        done
858
859        render_game
860    done
861    return 0
862}
863
864function map_filter
865{
866    typeset ch_player ch_monster ch_wall var
867
868    if (( $1 == 1 )) ; then
869        ch_player="${vtcode["fg_yellow"]}"
870        ch_monster="${vtcode["fg_red"]}"
871        ch_wall="${vtcode["fg_blue"]}"
872    else
873        ch_player=""
874        ch_monster=""
875        ch_wall=""
876    fi
877
878    if (( $2 == 1 )) ; then
879        # unicode map
880        ch_player+="$(printf '\u[24d2]')"
881        ch_monster+="$(printf '\u[2605]')"
882        ch_wall+="$(printf '\u[25a6]')"
883    else
884        # ascii map
885        ch_player+="@"
886        ch_monster+="x"
887        ch_wall+="#"
888    fi
889
890    # note that this filter currently defeats the "double-buffering"
891    while IFS='' read -r -d "${end_of_frame}" var ; do
892        var="${var// /${vtcode["fg_grey"]} }"
893        var="${var//\./${vtcode["fg_lightred"]}.}"
894        var="${var//@/${ch_player}}"
895        var="${var//x/${ch_monster}}"
896        var="${var//#/${ch_wall}}"
897
898        print -r -- "${var}"
899    done
900    return 0
901}
902
903function exit_trap
904{
905    # restore stty settings
906    stty ${saved_stty}
907
908    print "bye."
909    return 0
910}
911
912function usage
913{
914    OPTIND=0
915    getopts -a "${progname}" "${gnaw_usage}" OPT '-?'
916    exit 2
917}
918
919# program start
920# make sure we use the ksh93 "cat" builtin which supports the "-u" option
921builtin basename
922builtin cat
923builtin wc
924
925typeset progname="${ basename "${0}" ; }"
926
927# terminal size rect
928compound termsize=(
929    integer columns=-1
930    integer lines=-1
931)
932
933# global variables
934typeset quiet=false
935
936typeset -A levelmap
937typeset -A player
938typeset -A monster
939# global rendering options
940integer game_use_colors=0
941integer game_use_unicode=0
942
943typeset -r gnaw_usage=$'+
944[-?\n@(#)\$Id: gnaw (Roland Mainz) 2009-05-09 \$\n]
945[-author?Roland Mainz <roland.mainz@nrubsig.org>]
946[+NAME?gnaw - maze game written in ksh93]
947[+DESCRIPTION?\bgnaw\b is a maze game.
948        The player maneuvers a yellow "@" sign to navigate a maze while eating
949        small dots. A level is finished when all the dots are eaten. Five monsters
950        (maw, claw, jitterbug, tentacle and grendel) also wander the maze in an attempt
951        to catch the "@". Each level begins with all ghosts in their home, and "@" near
952        the bottom of the maze. The monsters are released from the home one by one at the
953        start of each level and start their rentless hunt after the player.]
954[q:quiet?Disable use of terminal bell.]
955[+SEE ALSO?\bksh93\b(1)]
956'
957
958while getopts -a "${progname}" "${gnaw_usage}" OPT ; do
959#    printmsg "## OPT=|${OPT}|, OPTARG=|${OPTARG}|"
960    case ${OPT} in
961        q)    quiet=true  ;;
962        +q)   quiet=false ;;
963        *)    usage ;;
964    esac
965done
966shift $((OPTIND-1))
967
968# save stty values and register the exit trap which restores these values on exit
969saved_stty="$(stty -g)"
970trap exit_trap EXIT
971
972print "Loading..."
973
974# set stty values, "-icanon min 1 time 0 -inpck" should improve input latency,
975# "-echo" turns the terminal echo off
976stty -icanon min 1 time 0 -inpck -echo
977
978get_term_size termsize || fatal_error "Could not get terminal size."
979
980# prechecks
981(( termsize.columns < 60 )) && fatal_error "Terminal width must be larger than 60 columns (currently ${termsize.columns})."
982
983typeset -A vtcode
984# color values taken from http://frexx.de/xterm-256-notes/, other
985# codes from http://vt100.net/docs/vt100-tm/
986vtcode=(
987    ["bg_black"]="$(print -n "\E[40m")"
988    ["fg_black"]="$(print -n "\E[30m")"
989    ["fg_red"]="$(print -n "\E[31m")"
990    ["fg_lightred"]="$(print -n "\E[1;31m")"
991    ["fg_green"]="$(print -n "\E[32m")"
992    ["fg_lightgreen"]="$(print -n "\E[1;32m")"
993    ["fg_yellow"]="$(print -n "\E[33m")"
994    ["fg_lightyellow"]="$(print -n "\E[1;33m")"
995    ["fg_blue"]="$(print -n "\E[34m")"
996    ["fg_lightblue"]="$(print -n "\E[1;34m")"
997    ["fg_grey"]="$(print -n "\E[1;37m")"
998    ["fg_white"]="$(print -n "\E[37m")"
999
1000    # misc other vt stuff
1001    ["vtreset"]="$(tput reset)"
1002    ["clear"]="$(tput clear)"
1003    ["bel"]="$(tput bel)"
1004    ["spaceline"]="$(for (( i=0 ; i < termsize.columns ; i++ )) ; do print -n " " ; done)"
1005)
1006
1007# character used to as marker that a single frame ends at this point - this
1008# is used by the "double buffering" code to make sure the "read" builtin
1009# can read a whole "frame" instead of reading stuff line-by-line
1010typeset -r end_of_frame=$'\t'
1011
1012# get terminal sequence to move cursor to position x,y
1013# (see http://vt100.net/docs/vt100-ug/chapter3.html#CPR)
1014case ${TERM} in
1015    xterm | xterm-color | vt100 | vt220 | dtterm | sun | sun-color)
1016        cup="$(infocmp -1 | \
1017	       egrep '^[[:space:]]*cup=' | \
1018	       sed -e 's/.*cup=//' \
1019	           -e 's/%[%id]*p1[%id]*/%2\\\$d/g' \
1020		   -e 's/%[%id]*p2[%id]*/%1\\\$d/g' \
1021		   -e 's/,$//')"
1022        for (( x=0 ; x < termsize.columns ; x++ )) ; do
1023            for (( y=0 ; y < termsize.lines ; y++ )) ; do
1024                vtcode[cup_${x}_${y}]="$(printf "${cup}" $((x + 1)) $((y + 1)) )"
1025            done
1026        done
1027        ;;
1028    *)
1029        printf "# Unrecognised terminal type '%s', fetching %dx%d items from terminfo database, please wait...\n" "${TERM}" "${termsize.columns}" "${termsize.lines}"
1030        for (( x=0 ; x < termsize.columns ; x++ )) ; do
1031            for (( y=0 ; y < termsize.lines ; y++ )) ; do
1032                vtcode[cup_${x}_${y}]="$(tput cup ${y} ${x})"
1033            done
1034        done
1035        ;;
1036esac
1037
1038print -- "${vtcode["vtreset"]}"
1039
1040run_logo
1041run_menu
1042
1043exit 0
1044# EOF.
1045