[comp.lang.icon] Klondike solitaire, v3.01, part 4/6

naz@hslrswi.hasler.ascom.ch (Norman H. Azadian) (04/21/91)

#! /bin/sh
# This is a shell archive, meaning:
# 1. Remove everything above the #! /bin/sh line.
# 2. Save the resulting text in a file.
# 3. Execute the file with /bin/sh (not csh) to create the files:
#	kloncon.icn
# This archive created: Sun Apr 21 15:04:11 1991
# By:	Norman H. Azadian (Hasler AG)
export PATH; PATH=/bin:$PATH
echo shar: extracting "'kloncon.icn'" '(19868 characters)'
if test -f 'kloncon.icn'
then
	echo shar: will not over-write existing file "'kloncon.icn'"
else
cat << \SHAR_EOF > 'kloncon.icn'
#kloncon.icn	901029	NHA
# Console interface routines for Klondike.
#
# TO FIX:
#
#
# TO DO:
#
# -	develop a visual indication (for isQuiet) for a PC
# -	Click for each card moved in a stack ??
#

link	iolib								#non-ANSI screen output subroutines


# constants
global	rankID, suitID						#card identification chars
global	isANSI								# non-&null for ANSI-compatible
global	isMonochrome						# non-&null when running Black&White
global	output								#function for writing control chars
global	lineCount							#total possible rows on screen
# Video control strings, etc
global	Vnormal, Vreverse, Vblink, Vbold, VclearAll, VclearEOL, VbackSpace,Vbell
global  Vcontrols, ESC						#set of control strings, ESC char
global	visualBell							# non-&null when Vbell is visual
global	color								#list of suit color strings


#	u s e r I n t e r r u p t
# Succeeds iff the user wishes to interrupt the action.
# THIS ROUTINE IS SYSTEM DEPENDENT.
# For DOS, we simply look to see if any keystroke is waiting, discarding
# it/them.  Your system may be different.
procedure userInterrupt ()
static keyWaiting
initial	{
	keyWaiting  :=  if \type(kbhit)  then  "kbhit"
}
	if \isDOS  then
		while keyWaiting()  do  {
			(getch() == "\0")  &  getch()	#eat interrupting keystroke
			return							#success means "interrupt requested"
		}
	else
		(\keyWaiting)  &  keyWaiting()  &  getch()  &  return
	fail									#failure = "interrupt NOT requested"
end											#userInterrupt


#	m y P u t s
# Output all the strings, using iputs() for control strings.
# This slightly cheesy algorithm requires that all control strings
# start with ESC.  This, however, does not apply when \isANSI,
# since this routine isn't called then.
# Note that if isQuiet changes while running (using debug command), then
# Vbell and visualBell may not be optimally set.
procedure myPuts (sl[])
static outstr
local s
initial	{
	outstr := table("Oops!")
	outstr[VclearAll]	:= getval("cl")	 |  (getval("ho") || getval("cd"))
	outstr[VclearEOL]	:= getval("ce")
	outstr[Vnormal]		:= getval("me") || getval("ue")
	outstr[Vbold]		:= getval("md" | "us")
	outstr[Vblink]		:= getval("mb")
	outstr[Vreverse]	:= getval("mr" | "so")
	outstr[VbackSpace]	:= if getval("bs")  then  "\b"  else  getval("le")
	if \isQuiet  then
		outstr[Vbell]	:= (visualBell := getval("vb"))  |  getval("bl")
	else
		outstr[Vbell]	:= getval ("bl")  |  "\^G";
}
	while s := string(get(sl))  do  
		s ?	{
			while writes (tab(upto(ESC)))  do
				iputs (outstr[=(!Vcontrols)])
			writes (tab(0))
		}
	return
end											#myPuts


#	g e t A N S I t y p e
# Determine if this ANSI-compatible screen is monochrome or color.
# This only works on a PC with the ANSI.SYS (or similar) driver installed.
procedure getANSItype ()
local i
	if \isPC  then  {
		i := ord (Peek([16r40, 16r49]))			#BIOS display mode byte
		case i  of  {
			2		:	isMonochrome := 1
			3		:	isMonochrome := &null	#living color
			7		:	isMonochrome := 1
			default	:	stop ("Klondike:  unknown BIOS display mode ", i)
		}
	} else
		runerr (500, "Klondike:  but I thought you were on a PC !")
	return
end											#getANSItype


#	i n i t C o n s t a n t s
# Initialize the program "constants".  These are actually variables that
# are set just once at the beginning of the world.
procedure initConstants (termtype)
local Vred, Vblack							#suit color strings

	lineCount := 25							#assume the best
	case  (map(termtype, &lcase, &ucase))  of  {
		"PC"	:	{ getANSItype ();			isANSI := 1		}
		"MONO"	:	{ isMonochrome := 1;		isANSI := 1 	}
		"COLOR"	:	{ isMonochrome := &null;	isANSI := 1		}
		default	:	{ isMonochrome := 1;		isANSI := &null
						(0 < *termtype)  &  setname (termtype)
						lineCount := getval("li")
		}
	}
	(lineCount < 24)  &
		stop ("klondike: need at least 24-line terminal.  Yours is ", lineCount)

	# Use this function for outputting any string containing control characters,
	# unless you're sure that the screen is ANSI-compatible.
	output  :=  if \isANSI  then  writes  else  myPuts

	# set Video control strings, plus the set of all Video control strings
	ESC				:= "\e"					#escape character
	VclearAll		:= "\e[2J"				#clear screen and home cursor
	VclearEOL		:= "\e[K"				#clear to End Of Line
	Vnormal			:= "\e[0m"
	Vbold			:= "\e[1m"
	Vblink			:= "\e[5m"
	Vreverse		:= "\e[7m"
	if \isANSI  then  {
		VbackSpace	:= "\b"
		Vbell		:= "\^G"
	} else {
		#additional escape required for myPuts()
		VbackSpace	:= "\e\b"
		Vbell		:= "\e\^G"				#ding dong, Avon calling
	}
	Vcontrols 		:= set( [VclearAll, VclearEOL, Vnormal, Vbold,
							Vblink, Vreverse, VbackSpace, Vbell] )

	if \isMonochrome  then  {
		Vred	:= Vnormal
		Vblack	:= Vreverse
	} else {
		Vred	:= "\e[0;47;31m"			# "extra" 0 seems to be necessary
		Vblack	:= "\e[0;47;30m"			# "extra" 0 seems to be necessary
	}

	# Suits are: 1=Hearts, 2=Diamonds, 3=Clubs, 4=Spades
	suitID := if \isPC  then  "\3\4\5\6"  else  "HDCS"
	color  := [Vred, Vred, Vblack, Vblack]
	rankID := "A23456789TJQK"
	return
end											#initConstants


#	i n i t S c r e e n
# Initialize output and write the fixed parts of the screen.
# initConstants() must have been called earlier.
# Note that the screen and its mode must remain unmodified if \invisible.
procedure initScreen ()
local f
static vertical
initial {
	vertical  :=  if \isANSI  then  "\272"  else  "|"
}

	(\invisible)  &  return
	if \isANSI  then  {
		if (\isPC)  then
			if \isMonochrome  then  writes ("\e[=2h")	#25x80 B&W   text mode
			else  writes ("\e[=3h")						#25x80 color text mode
		writes (Vnormal, VclearAll, "\e[=7l")		#clear screen, prevent wrap
	} else {
		if not iputs (getval("is"))  then  {		#terminal Init Ftring
			if f := open (getval("if"), "r") then {	#terminal Init File
				while writes (reads(f))				## should use iputs() ??
				close (f)
			}
		}
		iputs (getval("ti"))						#Termcap Init string
		output (VclearAll)
	}

	every writeStackNumber (1 to 7, Vnormal)
	writeCursor (2, 64)
	if \isANSI  then
		writes ("\311\315\315\315\315SOLITAIRE\315\315\315\315")
	else
		writes ("=====SOLITAIRE====")
	every writeAt (3 to lineCount, 64, vertical)
	outputAt ((if lineCount = 25 then  25  else  1),  66,
				Vbold, "Q", Vnormal, "=Quit  ", Vbold, "H", Vnormal, "=Help")
	return
end											#initScreen


#	t e r m i n a t e S c r e e n
# Put the screen in the correct state prior to program termination.
procedure terminateScreen ()
static resetCooked
initial	{
	resetCooked  :=  if \type(reset_tty)  then  "reset_tty"
}
	if /invisible  then
		if \isANSI  then
			write ("\e[=7h", VclearAll, Vnormal)	#set cursor wrap mode
		else {
			output (VclearAll, Vnormal, getval("te"))
			(/isDOS)  &  (\resetCooked)  &  resetCooked ()
		}
	return
end											#terminateScreen


#	w r i t e C u r s o r
# Position the cursor to row,col.
# Screen origin (top left corner) is row=1 and col=1.
procedure writeCursor (row, col)
static cm
initial {
	(\isANSI)  |  (cm  :=  (getval("cm")  |  (getval("ch") || getval("cv"))))
}
	if /invisible  then
		if \isANSI  then
			writes ("\e[", row, ";", col, "H") 
		else
			iputs (igoto(cm, col, row))
	return
end											#writeCursor


#	w r i t e A t
# Position the cursor to row,col and then write the following string(s).
# Screen origin (top left corner) is row=1 and col=1.
# Note that the standard write() should normally not be used because the newline
# doesn't work when running in raw mode under unix.
procedure writeAt (row, col, s[])
	if /invisible  then  {
		writeCursor (row, col)
		writes! (s)
	}
	return
end											#writeAt


#	o u t p u t A t
# Position the cursor to row,col and then write the following string(s).
# Screen origin (top left corner) is row=1 and col=1.
procedure outputAt (row, col, s[])
	if /invisible  then  {
		writeCursor (row, col)
		output! (s)
	}
	return
end											#outputAt


#	w r i t e S t a c k N u m b e r
# Write the indicated stack number with the specified video attribute.
# Note that this will almost surely alter your current cursor position.
procedure writeStackNumber (num, attr)
	(\invisible)  | outputAt (1, [3,12,21,30,39,48,57][num], attr, num, Vnormal)
	return
end											#writeStackNumber


#	w r i t e F r o n t
# Displays an image of the specified card fronts at the specified spot.
## WARNING: The input cards must be in a list!
# Top left corner of the first card will be placed at the specified position.
# Successive cards are displayed two rows higher (lower position on the screen).
# Cursor need not be in any particular position before this, and is left
# in a random position afterwards.  Video is always left normal (not reversed).
# Cards are 7 columns wide by 5 rows tall.
# We only display the first 2 rows of each card, except for the top card.
# With 25 rows, we can put 12 cards in a stack (assuming we start in row 2).
# But if there are 11 in the stack we can only display 4 rows of the top card.
# If there are 12 cards, we can only display 2 rows of the topmost card.
# With a 24 row screen, the topmost card (a 2) is moved up one line on a full
# stack.  This will obscure the 3 that it is resting on, but the topmost 2 will
# be visible, which is rather more important.
##We can only write a row at a time due to a problem with ANSI col 80 handling.
procedure writeFront (cardlist, row, col)
local card
static vertical, topHorizontal, bottomHorizontal
initial {
	if \isANSI  then  {
		topHorizontal	 := "\332\304\304\304\304\304\277"
		vertical		 := "\263"
		bottomHorizontal := "\300\304\304\304\304\304\331"
	} else {
		topHorizontal	 := "-------"
		vertical		 := "|"
		bottomHorizontal := "-------"
	}
}
	dieIf (lineCount < row, row)
	# output first 2 rows of every card
	# for long stack and short screen the next-to-top card is mostly obscured
	every  card := !cardlist  do  {
		(row = lineCount)  &  (row -:= 1)			#long stack, short screen
		outputAt (row, col, Vreverse, topHorizontal)
		outputAt (row+:=1, col, vertical, color[card.suit], rankID[card.rank],
						suitID[card.suit], Vreverse, "   ", vertical)
		row +:= 1
	}

	# maybe put out some more rows of the top card
	if row <= lineCount  then  {
		outputAt (row, col, Vreverse, vertical, "     ", vertical)
		if (row +:= 1) <= lineCount  then  {
			outputAt (row, col, vertical, "   ", color[card.suit],
					   rankID[card.rank], suitID[card.suit], Vreverse, vertical)
			if (row +:= 1) <= lineCount  then
				writeAt (row, col, bottomHorizontal)	#last row of top card
		}
	}
	output (Vnormal)
	return
end											#writeFront


#	w r i t e B a c k
# Puts an image of the back of a card at the specified spot on the screen.
procedure writeBack (row, col)
static backLine
local i
initial {
	backLine := list(5)
	if \isANSI  then  {
		backLine[1] := "\332\304\304\304\304\304\277"
		backLine[2] := "\263\332\304\304\304\277\263"
		backLine[3] := "\263\263\040\040\040\263\263"
		backLine[4] := "\263\300\304\304\304\331\263"
		backLine[5] := "\300\304\304\304\304\304\331"
	} else {
		backLine[1] := "-------"
		backLine[2] := "| --- |"
		backLine[3] := "| | | |"
		backLine[4] := "| --- |"
		backLine[5] := "-------"
	}
}
	(\invisible)  |  (every writeAt (row, col, !backLine)  do  row +:= 1)
	return
end											#writeBack


#	w r i t e B l a n k
# Blanks a card-sized area at the specified spot on the screen.
procedure writeBlank (row, col)
	(\invisible)  |  (every writeAt (row + (0 to 4),  col,  "       "))
	return
end											#writeBlank


#	w r i t e S t a c k
# Display the specified stack.  Left end is bottom of stackUp, top of stackDown.
# Stacks start in row 2, column1; with 2 columns between stacks.
# Optimized to avoid re-writing cards that are already on the screen.
# prevs[] holds, for each stack, the total number of visible (up) cards
# on that stack as of the last time writeStack() was called.  This allows
# us to simply draw (or erase) the cards that have been added (or subtracted).
# By special arrangement, this routine can be called with a negative stack
# number!  This is a hint that our idea of what is on the display is actually
# wrong, and therefore the entire stack needs to be re-displayed.  This can
# happen in two situations:  1) in refreshScreen(), the entire screen is cleared
# before calling writeStack();  2) in undo() when undoing a move between
# stacks, the bottom card needs to be changed, although the normal algorithm
# would consider that it is already correctly displayed.  Note that in neither
# case is the stack shrinking, therefore we don't need to worry about erasing
# any cards that were displayed last time.
procedure writeStack (n)
local row, col
static prevs, blankLine, firstRow, lastRow
initial	{
	prevs := [0,0,0,0,0,0,0]					#previous stack height displayed
	blankLine := repl (" ", 7)
	# first and last screen rows for each card in a stack
	firstRow := [2,4,6,8,10,12,14,16,18,20,22,24]
	lastRow  := [6,8,10,12,14,16,18,20,22,24,lineCount,lineCount]
}
	(\invisible)  &  return
	(n < 0)  &  (prevs[n := abs(n)] := 0)		#n < 0  forces complete re-write
	col := (n * 9) - 8							#leftmost column for this stack

	if (*stackUp[n]) <= prevs[n]  then  {
		# the stack just got smaller (or stayed the same)
		# blank out two rows for each card that has been removed
		row := lastRow[prevs[n]] + 1			#<last row used by top card> + 1
		while *stackUp[n] < prevs[n]  do  {
			every  writeAt (row -:= (1 | 1),  col,  blankLine)
			prevs[n] -:= 1						#countdown and update
		}
		dieIf (*stackUp[n] ~= prevs[n], prevs[n])
		# re-write new top card
		if *stackUp[n] = 0  then
			(if *stackDown[n] = 0  then  writeBlank  else  writeBack) (2, col)
		else
			writeFront ([stackUp[n][-1]], firstRow[prevs[n]], col)
	} else {
		# the stack just got bigger -- display new cards
		writeFront (stackUp[n][prevs[n]+1:0], firstRow[prevs[n]+1], col)
		prevs[n] := *stackUp[n]				#remember how much is displayed
	}
	# display the number of hidden cards
	writeAt (4, (7 + col), " 123456???"[1+*stackDown[n]])
	return
end											#writeStack


#	w r i t e P i l e
# Displays an image of the specified ace pile, face up (or blank if empty)
procedure writePile (n)
static pileRow, pileCol
initial {
	pileRow := [3,3,9,9]
	pileCol := [66,74,66,74]
}
	if /invisible  then
		if  0 = pile[n]  then
			writeBlank (pileRow[n], pileCol[n])
		else
			writeFront ([card(n,pile[n])], pileRow[n], pileCol[n])
	return
end											#writePile


#	w r i t e D e c k D o w n
# Displays an image of deckDown (the face-down deck) in the proper spot.
# Avoids re-displaying blank or card back that is already on the screen.
# Parameter is non-null to force displaying something (assuming not invisible).
procedure writeDeckDown (forget)
static p
initial	{
	forget := 1								#assume the screen is blank
}
	if /invisible  then  {
		(\forget)  &  (p := writeDeckDown)	#anything but writeBlank | writeBack
		(p ~===:= (if 0 < *deckDown  then  writeBack else writeBlank)) (20, 74)
		writeAt (19, 76, right(*deckDown, 2))	#display card count in deckDown
	}
	return
end											#writeDeckDown


#	w r i t e D e c k U p
# Displays an image of deckUp (the face-up deck) in the proper spot.
procedure writeDeckUp ()
	if /invisible  then  {
		writeFront ([deckUp[1]], 20, 66)  |  writeBlank (20, 66)
		writeAt (19, 68, right(*deckUp, 2))		#write number of cards in deckUp
	}
	return
end											#writeDeckUp


#	c o u n t C o n t r o l s
# Returns a count of video control sequences in the proffered string.
# This is not a very general algorithm, being valid only for our V constants.
procedure countControls (s)
local count									#of [invisible] video control chars
local seq									#control sequence found
	count := 0
	every  find ((seq := (!Vcontrols)), s)  do  count +:= *seq
	return count
end											#countControls


#	w r i t e I n f o
# Displays a new short string (up to 16 printing characters) centered in the
# officially approved information area of the screen.
# All known video attributes (in set "Vcontrols") are specially handled.
# An empty string results in clearing the area.
# We always revert to normal text mode after outputting the string.
procedure writeInfo (s)
	(\invisible)  |  outputAt (15, 65, Vnormal, VclearEOL,
							trim(center(s, 16+countControls(s)), ' '), Vnormal)
	return
end											#writeInfo


#	c l i c k
# Make a quick sound to accompany card transfers, if possible and not Quiet.
procedure click ()
local x
	if (not \isQuiet)  &  (\isPC)  then  {
		x := InPort (16r61)
		OutPort (16r61, 3)
		OutPort (16r61, x)
	}
	return
end											#click


#	b e e p
# The short beep produced under isPC is not as annoying as the normal beeeeep.
# This always puts out something, although it might be a visual indication.
procedure beep ()
local x
	if (\isPC)  then  {						#no visual indication yet for a PC
		x := InPort (16r61)
		every  | OutPort (16r61, 3) \ 22
		OutPort (16r61, x)
	} else
		output (Vbell)
	return
end											#beep


#	c o m p l a i n
# Let the boob know he done something wrong, with a dash of humor.
# Complaint can be specified, otherwise a generic one will be selected for you.
# Phrases can be up to 16 visible characters long.
# Additionally there may be embedded recognized video control sequences.
# Complaint will always be bold, and will blink if isQuiet.
procedure complain (complaint)
static phrases
local f, s
initial {
	phrases := ["INVALID"]					#irreducible minimum
	if \phraseFile  then  {
		if f := open((directory || phraseFile), "r")  then  {
			while put (phrases,
						trim(center(s, 16+countControls(s := read(f))), ' '))
			close (f)
			(1 < *phrases)  &  pop(phrases)	#success -- remove ours
		}
	} else {
		phrases |||:= [
"cut that out!",	"oops!",			"be nice!", 		"get real",
"be serious",		"What??",			"huh?",				"idiot alert",
"dummy!",			"Hey man!",			"giggle giggle",	"engage brain",
"NOW what?",		"yuk yuk",			"funky dude",		"wake up, man",
"yeah, real funny",	"totally awesome",	"boffo",			"boogie boogie",
"Bozo!",			"watch it!",		"oi vey",			"~@?$!&*=(*%^!*",
"forty lashes!",	"way cool, dude",	"ha ha ha",			"see an analyst",
"go fly a kite",	"wish upon a star",	"basket case",		"braindamaged",
"dummkopf",			"holy cow",			"reject",			"try harder",
"think again",		"watch your hands",	"We're not amused",	"adjust fingers",
"gimme a break",	"R U 4 real ?",		"that's a laugh",	"Ho Ho Ho",
"space case",		"space cadet",		"call AA",			"oh yeah?",
"dum de dum dum",	"try thinking",		"sigh",				"get a life"
		]
	}
	(\debugging)  &  writeInfo (*phrases || " phrases")
}
	/complaint := ?phrases
	#Note that isQuiet can change while running if debugging is enabled (-Z)
	if (\isQuiet)  &  (not \visualBell)  then
		writeInfo (Vbold || Vblink || complaint)
	else {
		writeInfo (Vbold || complaint)
		beep()
	}
	return
end											#complain


#	g e t C m d C h a r
# Returns an upper-case command character, echoed to current cursor position.
# Fails if character wasn't "normal" and complaint was made.
# For ESC, abort information is written, and ESC is returned.
# Normal calling sequence (from within a command procedure) is thus:
#		until (s := getCmdChar ())
#		(s == ESC)  &  fail
# Under DOS, F1 [function key 1] is specially treated as a request for Help.
procedure getCmdChar ()
local s
	s := getch ()							#get command character
	if s == "\0"  then  {					#non-ASCII character
		s := getch ()						#check keyboard scan code
		if (\isDOS)  &  (s == ";")  then  {
			s := "h"						#jigger F1 to look like "Help"
		} else {
			complain ()
			fail
		}
	}
	s := map (s, &lcase, &ucase)
	if s == ESC  then
		writeInfo (Vbold || "Cmd Aborted.")
	else
		writes (s)							#echo the command character
	return  s
end											#getCmdChar


#	r e f r e s h S c r e e n
# Re-write entire screen.
procedure refreshScreen ()
	if /invisible  then  {
		initScreen ()
		every writeStack (-1 to -7 by -1)
		every writePile (1 to 4)
		writeDeckDown (1)
		writeDeckUp ()
	}
	return
end											#refreshScreen
SHAR_EOF
if test 19868 -ne "`wc -c < 'kloncon.icn'`"
then
	echo shar: error transmitting "'kloncon.icn'" '(should have been 19868 characters)'
fi
fi # end of overwriting check
#	End of shell archive
exit 0
-- 
PAPER:  Norman Azadian; Ascom AG; Belpstrasse 23; 3000 Berne 14; Switzerland
INTERNET:  naz%hslrswi.uucp@uunet.uu.net
UUCP:   ...{uunet,ukc,mcvax,...}!chx400!hslrswi!naz
VOICE:  +41 31 63 2178            BITNET: naz%hslrswi.UUCP@cernvax.BITNET