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