#!/usr/local/bin/stk -f 
;;;; STetris Version 1.1
;;;;     By Harvey J. Stein      hjstein@math.huji.ac.il
;;;;     Copyright (C) 1994 Harvey J. Stein, Tel Aviv, ISRAEL
;;;; 
;;;; Permission to use, copy, and/or distribute this software and its
;;;; documentation for any purpose is hereby granted, provided that
;;;; both the above copyright notice and this permission notice appear
;;;; in all copies and derived works, and that copies and/or derived
;;;; works are used, copied and/or distributed without fees.  Fees for
;;;; distribution or use of this software or derived works may only be
;;;; charged with express written permission of the copyright holder.
;;;; This software is provided ``as is'' without express or implied
;;;; warranty.

;;; This is an implementation of a falling block game.  Just run it.
;;;
;;; The controls are as follows, but are easily modified (see below):
;;;    Move to left : j   or   left arrow
;;;    Move to right: l   or   right arrow
;;;    Rotate right : k   or   down arrow 
;;;    Rotate left  : i   or   up arrow
;;;    Drop quick   : space
;;;    New game     : n
;;;    Pause        : p
;;;    Continue     : c
;;;    Scramble     : s - Scrambles the blocks so that rotate left &
;;;                       rotate right actually transform the shape
;;;                       instead of rotating it.  Only available
;;;                       between games.
;;;    Unscramble   : u - Go back to original configuration
;;;    Help         : h
;;;    Quit         : q
;;;    End game     : e
;;;    Bump up level: b
;;;
;;; ------------- Installation -------------------------------
;;; Should just work fine as is.  If you have xboing, and you have a
;;; /dev/audio device, this game can produce sounds.  To get the
;;; sounds, edit the definition of sounddir (first definition of the
;;; global variables section below).  Make sure it refers to the
;;; directory with your xboing sounds.

;;; To do:
;;;   -Maintain high score file.  Question: How can I protect it?
;;;    (Typically one will make a high score file write only to group
;;;    games & make the game suid games.  But, this can't be done in
;;;    general for shellscripts).
;;;   -Man page.
;;;   -Next piece preview.
;;;   -More sounds.
;;;   -Better way to play sounds than catting to /dev/audio.
;;;   -Make up sounds for game instead of just "borrowing" sounds from
;;;    xboing.
;;;   -Code cleanup - Parameterize the pieces better.  Right now I
;;;    have the number 7 (for the number of pieces) hard wired into
;;;    the code, and the colors of each piece are just stuffed into a
;;;    fcn.  It would be nice to have a global variable (n) for the # of
;;;    blocks to use in the pieces & then to generate all the pieces
;;;    containing n squares.
;;;   -Find better way of playing sounds than catting to /dev/audio
;;;   -Standardize comment style.
;;;   -Write STk program which uses send to play stetris.
;;;   -Need to change name of window before I can write a stetris
;;;    player that uses send...
;;;   -Fix bug where game sometimes ends with last piece overlapping
;;;    another piece.

;;; Changes from v1.0 to v1.1:
;;;   -Got rid of some of the 7s.
;;;   -Added scrambling & help.
;;;   -Didn't fix bug where game sometimes ends with last piece
;;;    overlapping another piece, but made it more rare.
;;;   -Now starts of pieces off screen so that they all appear
;;;    initially as one row.
;;;   -Added buttons for new game, pause, unpause, help, etc.
;;;   -Blank screen during pauses.
;;;   -No need for stetris shellscript (thanks to Erick).
;;;   -Added <b> to increase level by 1.
;;;   -Reduced min-fall-delay from 80 to 60 because it seems to be
;;;    long enough (at least on my 486dx33).  Make it bigger if your
;;;    top level is jerky.

;;; Helpful for debugging (so that stetris.stk can be reloaded into
;;; the interpreter):
(for-each destroy (winfo 'children *root*))

;;; To avoid inopportune garbage collections:
(cond ((not (symbol-bound? 'heap-expanded)) ; Don't expand after addn'l loads.
       (expand-heap 75000)
       (define heap-expanded #t)))

;;; ------------------- Include files ------------------------

(require "Tk-classes")
(require "unix")
(require "dialog")


;;; ----------- Global variables ---------------------------

;; Sound directory (set for your system, or set to a nonexistent directory to
;; disable sound):
(define sounddir "/usr/games/lib/xboing/sounds")

;;; Sounds (modifiable):
;;; Expects to find (& soundir "/" "game_over.au"), for example.
;;; Sound is played by catting it to /dev/audio
(define soundmap
  '((game-over       "game_over.au")
    (near-end        "looksbad.au")
    (goto-next-level "warp.au")
    (piece-landed    "metal.au")
    (piece-moved     "click.au")
    (three-in-row    "applause.au")
    (four-in-row     "youagod.au")))

;; Keyboard mappings & corresponding actions (modifiable).
(define control-bindings		; Game control actions.
  '(("<q>" (do-exit))			; Always available.
    ("<n>" (do-new-game))
    ("<e>" (do-end-game))
    ("<p>" (do-pause))
    ("<c>" (do-continue-game))
    ("<h>" (do-help))
    ("<b>" (do-increase-level))))

(define game-play-bindings		; Bindings for moving pieces.
  '(("<j>"     (do-left))		; Only available during play.
    ("<Left>"  (do-left))
    ("<l>"     (do-right))
    ("<Right>" (do-right))
    ("<k>"     (do-rotate-right))	; clockwise.
    ("<Down>"  (do-rotate-right))
    ("<i>"     (do-rotate-left))
    ("<Up>"    (do-rotate-left))
    ("<space>" (do-fall))
    ("<5>"     (do-fall))))

(define non-game-play-bindings		; Bindings only available
  '(("<s>" (do-scramble))		; between games.
    ("<u>" (do-unscramble))))

;; Game action descriptions
(define (action-description act)
  (case act
	('do-left         "Move left")
	('do-right        "Move right")
	('do-rotate-left  "Rotate counter-clockwise")
	('do-rotate-right "Rotate clockwise")
	('do-fall         "Fall")
	('do-scramble     "Scramble blocks")
	('do-unscramble   "Unscramble blocks")
	('do-help         "Help")
	('do-exit         "Exit")
	('do-new-game     "New game")
	('do-end-game     "End game")
	('do-pause        "Pause game")
	('do-increase-level "Bump up level by one")
	('do-continue-game "Continue after pause")))

;; block size & playing field size parameters (modifyable).
(define block-width 20)			; Width of a block.
(define block-height 20)		; Height of a block.
(define block-border-width 2)		; Width of block borders.
(define play-cols 9)			; cols # 0-9  = 10 cols.
(define play-rows 29)			; rows # 0-28 = 29 rows.

;; Window shape & size parameters (modifyable).
(define frame-border-width 5)		; Width of frame border for
					; playing field & score box.
(define score-frame-width 150)		; Width of score box (don't
					; make too small!).

;; Game parameters (modifiable).
(define start-fall-delay 750)		; initially, game drops stetris piece
					; one notch every start-fall-delay
					; milliseconds. 
(define level-time (* 40 start-fall-delay)) ; Length of time (in milliseconds)
					    ; that each level lasts.
(define min-fall-delay 60)		; Min amt of time allowable btw piece
					; drops. 
(define delta-reducer .80)		; Each time level goes up, multiply
					; fall-delay by this to get new fall
					; delay.
(define bump-bonus 300)			; When you bump up the level
					; manually, you get bump-bonus
					; pts * the % of time left
					; until the next level.

;;; -------------- Less modifiable parameters --------------------
;; Game parameters (don't touch).
(define winx (* block-width (1+ play-cols))) ; size of playing field
(define winy (* block-width (1+ play-rows)))
(define start-delta-count 0)		; # of steps at game start.
(define delta-count start-delta-count)	; Lapsed time (in steps) of current
					; level.
(define level-number 1)			; Current level number.
(define fall-delay start-fall-delay)	; Current amt of time btw drops (in ms)
(define move-count 1)			; # drops since beginning of game.
(define old-count 1)			; # drops since last piece hit bottom.
(define quit-now #t)			; False causes game to stop.
(define current-piece ())		; Piece that is currently falling.
(define score 0)			; Score.
(define game-over "")			; String to display when game ends.
(define paused-game #f)

(define (ms-left)
  (- level-time (* fall-delay  delta-count)))

(define (time-left)
  (inexact->exact
   (/ (ms-left) 1000)))

(define time-to-speedup (time-left))	; Time left to current level.
(define current-block-colors ())	; Used to store block colors
					; when screen is blanked.

;;; ------------ Start real work ----------------------------
;;; Check sound validity - First check that sounddir exists & that
;;; /dev/audio exists.
(cond ((or
	(not (file-is-directory? sounddir)) ;;; If sounddir doesn't exist.
	(not (file-is-writable? "/dev/audio"))) ;; If /dev/audio doesn't exist.
       (set! soundmap ())))

;;; Now, check that all sounds are readable.  Delete the ones that
;;; aren't.
(set! soundmap
      (let delete-nonexistent ((l soundmap))
	   (cond ((null? l) ())
		 ((file-is-readable? (& sounddir "/" (cadar l)))
		  (cons (car l) (delete-nonexistent (cdr l))))
		 (else (delete-nonexistent (cdr l))))))

(define (reset-vars)
;;; Clears game variables for start of new game.
  (set! delta-count start-delta-count)
  (set! level-number 1)
  (set! fall-delay start-fall-delay)
  (set! old-count 1)
  (set! move-count 1)
  (set! quit-now #f)
  (set! score 0)
  (set! game-over ""))
 
;;; ------------------ Window size setup --------------------------
(wm 'title *root* "STetris")
(wm 'minsize *root* 
    (+ winx score-frame-width)
    (+ winy (* 2 frame-border-width)))
(wm 'maxsize *root* 
    (+ winx score-frame-width)
    (+ winy (* 2 frame-border-width)))

(wm 'geometry *root* (format #f "~Ax~A"
			     (+ winx score-frame-width)
			     (+ winy  (* 2 frame-border-width))))

;;; -------------------- Widget Creation ---------------------------

;;; Playing canvas
(define canvas-frame
  (make <Frame>
	:relief 'ridge
	:border-width frame-border-width))
(pack canvas-frame :side 'left)

(define stetris-canvas
  (make <Canvas>
	:parent canvas-frame
	:height winy
	:width  winx))
(pack stetris-canvas :fill 'both :expand #t)

;;; Statistics frame
(define score-frame
  (make <Frame>
	:relief 'ridge
	:border-width frame-border-width))
(pack score-frame :fill 'both :expand #t :side 'left)

(define filler-1 (make <frame> :parent score-frame))
(define score-title-label 
  (make <label>
	:parent score-frame
	:text "Score"))

(define score-label
  (make <label>
	:parent score-frame
	:text-variable 'score))

(define delay-title-label 
  (make <label>
	:parent score-frame
	:text "Delay"))

(define delay-label
  (make <label>
	:parent score-frame
	:text-variable 'fall-delay))

(define count-title-label 
  (make <label>
	:parent score-frame
	:text "Moves"))

(define count-label
  (make <label>
	:parent score-frame
	:text-variable 'move-count))

(define level-title-label
  (make <label>
	:parent score-frame
	:text "Level"))

(define level-label
  (make <label>
	:parent score-frame
	:text-variable 'level-number))

(define time-to-speedup-title-label 
  (make <label>
	:parent score-frame
	:text "Time to speedup"))

(define time-to-speedup-label
  (make <label>
	:parent score-frame
	:text-variable 'time-to-speedup))

(define game-over-label
  (make <label>
	:parent score-frame
	:text-variable 'game-over))

(define pause-button
  (make <button>
	:parent score-frame
	:text "Pause"
	:command '(do-pause)))

(define continue-button
  (make <button>
	:parent score-frame
	:text "Continue"
	:command '(do-continue-game)))
  
(define newgame-button
  (make <button>
	:parent score-frame
	:text "New Game"
	:command '(do-new-game)))

(define endgame-button
  (make <button>
	:parent score-frame
	:text "End Game"
	:command '(do-end-game)))
  
(define help-button
  (make <button>
	:parent score-frame
	:text "Help"
	:command '(do-help)))

(define quit-button
  (make <button>
	:parent score-frame
	:text "Quit"
	:command '(do-exit)))


(define filler-2 (make <frame> :parent score-frame))
(define filler-3 (make <frame> :parent score-frame))

(pack filler-1 :expand #t :fill 'both)
(pack score-title-label score-label
      delay-title-label delay-label
      count-title-label count-label
      level-title-label level-label
      time-to-speedup-title-label time-to-speedup-label
      game-over-label)

(pack filler-3 :expand #t :fill 'both)
(pack pause-button continue-button newgame-button 
      endgame-button help-button quit-button
      :fill 'x)

;;;(pack filler-2 :expand #t :fill 'both)

;;; -------------- Convert from block coords to screen coords -----------
(define (block-pos-coords x y)
  (list (+ (* x block-width) (/ block-border-width 2))
	(+ (* y block-height)  (/ block-border-width 2))
	(- (* (1+ x) block-width) (/ block-border-width 2))
	(- (* (1+ y) block-height) (/ block-border-width 2))))

;;; --------- Methods for treating rectangles like stetris blocks --------
(define-method fall ((r <Rectangle>))
  (slot-set! r 'coords 
	     (map + (coords r) (list 0 block-height 0 block-height))))

(define-method left ((r <Rectangle>))
  (slot-set! r 'coords
	     (map + (coords r) (list (- block-width) 0 (- block-width) 0))))

(define-method right ((r <Rectangle>))
  (slot-set! r 'coords
	     (map + (coords r) (list block-width 0 block-width 0))))

(define-method up ((r <Rectangle>))
  (slot-set! r 'coords
	     (map + (coords r) (list 0 (- block-height) 0 (- block-height)))))

;;; ------------------- Class stetris-block ----------------------
;;; Instances of this class are basically just rectangles that keep
;;; track of their position in block coordinates instead of screen
;;; coordinates.  There are probably better ways to do this  (such as
;;; making the coordinates virtual slots).  On the other hand, if they
;;; were virtual slots, they would have to scale the coordinates,
;;; which might make things slower.
;;;
;;; Also includes methods for checking that a location is legal
;;; (i.e. - that it isn't already occupied by another block).
;;; Actually, we call a spot legal if it's on the screen & isn't
;;; occupied by another block with the same tag.  Each tetris piece
;;; gets a unique tag which is shared by the blocks which compose it. 
;;;
;;; One good improvement would probably be to remove the testing
;;; against the top of the screen, since blocks should be able to fall
;;; from above the screen.

(define-class <stetris-block> (<Rectangle>)
  ((x :init-keyword :x :accessor x-of :initform 3)
   (y :init-keyword :y :accessor y-of :initform 0)
   (true-color)
   (parent :init-keyword :parent :accessor parent-of)))

(define-method initialize ((self <stetris-block>) initargs)
  (next-method)
  (slot-set! self 'true-color (slot-ref self 'fill))
  (slot-set! self 'width block-border-width)
  (slot-set! self 'coords
	     (block-pos-coords (x-of self) (y-of self))))

(define-method hide ((self <stetris-block>))
  (slot-set! self 'fill 'black))

(define-method show ((self <stetris-block>))
  (slot-set! self 'fill (slot-ref self 'true-color)))

(define-method fall ((self <stetris-block>))
  (set! (y-of self) (1+ (y-of self)))
  (next-method))

(define-method up ((self <stetris-block>))
  (set! (y-of self) (1- (y-of self)))
  (next-method))

(define-method right ((self <stetris-block>))
  (set! (x-of self) (1+ (x-of self)))
  (next-method))

(define-method left ((self <stetris-block>))
  (set! (x-of self) (1- (x-of self)))
  (next-method))

(define-method can-fall? ((self <stetris-block>))
  (ok-spot (x-of self) (1+ (y-of self)) (tags self)))

(define-method can-up? ((self <stetris-block>))
  (ok-spot (x-of self) (1- (y-of self)) (tags self)))

(define-method can-left? ((self <stetris-block>))
  (ok-spot (1- (x-of self)) (y-of self) (tags self)))

(define-method can-right? ((self <stetris-block>))
  (ok-spot (1+ (x-of self)) (y-of self) (tags self)))

(define (ok-spot x y tag)
  (and (eval 
	(cons 'and  (map (lambda (x) (string=? tag (tags x)))
			 (apply
			  find-items
			  `(,stetris-canvas overlapping
			       ,@(block-pos-coords x y))))))
       (onscreen x y)))

(define (onscreen x y)
  (and (>= x 0)
       (<= x play-cols)
;;;       (>= y 0)      ;;; Taken out To allow pieces to drop in from
                        ;;; above the canvas. 
       (<= y play-rows)))

;;; -------------- Class stetris-piece ------------------------
;;; A collection of stetris-blocks.
;;; Class slot descriptions:
;;;   parent - Canvas containing stetris-piece
;;;   blocks - List of blocks composing stetris piece.
;;;   shape  - Integer indicating shape of piece.  Meaning is defined
;;;            by shape-list-slow function.  0 = line, 1 = square, etc.
;;;   tag    - tag for this piece & all the blocks composing it.  It's a
;;;            unique identifier for this piece.
;;;   rotation - Better name would be rotation.
;;;   x        - x coord of piece in game coordinates.
;;;   y        - y coord of piece in game coordinates.
;;;
;;;   Basically, a stetris-piece is a collection of blocks.  A
;;;   stetris-piece has a location, a shape & a rotation.  The
;;;   locations of the blocks are defined by the shape-list function.
;;;   (shape-list shape rotation) function returns a list of
;;;   coordinates.  The coordinates of the blocks composing a
;;;   stetris-piece are computed by adding the location of the stetris
;;;   piece to each of the coordinates returned by shape-list.
;;;   When the user rotates the stetris piece, the rotation slot is
;;;   incremented (or decremented).
;;;
;;;   For (possibly ineffective) speed reasons, we store the
;;;   shapes in a vector & use a macro to access them.
;;;

(define-class <stetris-piece> ()
  ((parent :accessor parent-of :init-keyword :parent)
   (blocks :accessor blocks-of)
   (shape :accessor shape-of :init-keyword :shape :initform 0)
   (tag   :accessor tag-of :init-keyword :tag :initform "")
   (rotation :accessor rotation-of :initform 0 :init-keyword :rotation)
   (x :accessor x-of :initform 0 :init-keyword :x)
   (y :accessor y-of :initform 0 :init-keyword :y)))

(define-method initialize ((self <stetris-piece>) initargs)
  (next-method)
  (set! (blocks-of self)
	(make-blocks (shape-of self)
		     (rotation-of self)
		     (x-of self)
		     (y-of self)
		     (parent-of self)))
  (for-each (lambda (x) (set! (tags x) (tag-of self))) (blocks-of self)))

(define (make-blocks shape rotation x y parent)
  (define (quick-make p)
    (make <stetris-block>
	  :x (+ (car p) x) :y (+ (cadr p) y)
	  :coords '(0 0 0 0)
	  :fill (colors-of shape)
	  :parent parent))
  (map quick-make (shape-list shape rotation)))

;;; Function which returns, for a given shape & rotation, a list of
;;; the positions that the blocks must be in relative to the
;;; stetris-piece.

(define (shape-list-slow shape rotation)
  (case shape
	(0 (case rotation ;; line
		 (0 '( (3 1) (4 1) (5 1) (6 1)))
		 (1 '( (4 0) (4 1) (4 2) (4 3)))))
	(1 (case rotation ;; square
		 (0 '( (3 1) (4 1) (3 2) (4 2)))))
	(2 (case rotation ;; left zig
		 (0 '( (3 1) (4 1) (4 2) (5 2)))
		 (1 '( (4 0) (4 1) (3 1) (3 2)))))
	(3 (case rotation ;; right zig
		 (0 '( (3 2) (4 2) (4 1) (5 1)))
		 (1 '( (4 1) (4 2) (5 2) (5 3)))))
	(4 (case rotation ;; T
		 (0 '( (3 1) (4 1) (5 1) (4 0)))
		 (1 '( (4 0) (4 1) (4 2) (5 1)))
		 (2 '( (3 1) (4 1) (5 1) (4 2)))
		 (3 '( (4 0) (4 1) (4 2) (3 1)))))
	(5 (case rotation ;; right L
		 (0 '( (3 1) (3 2) (3 3) (4 3)))
		 (1 '( (3 1) (4 1) (5 1) (3 2)))
		 (2 '( (4 1) (5 1) (5 2) (5 3)))
		 (3 '( (3 3) (4 3) (5 3) (5 2)))))
	(6 (case rotation ;; left L
		 (0 '( (5 0) (5 1) (5 2) (4 2)))
		 (1 '( (3 2) (4 2) (5 2) (3 1)))
		 (2 '( (3 0) (3 1) (3 2) (4 0)))
		 (3 '( (3 0) (4 0) (5 0) (5 1)))))))
		 
;;; given a shape, returns the number of rotations that that shape can
;;; go through.
(define (num-rotations-slow shape)
  (case shape
	(0 2)
	(1 1)
	(2 2)
	(3 2)
	(4 4)
	(5 4)
	(6 4)))

;;; We initialize a vector to contain the number of rotations of each
;;; shape & use a macro to access it.  I was hoping for speed
;;; benefits, but I don't know if it really helps.
(define num-rotations-vect (make-vector 7))

(dotimes (shape (vector-length num-rotations-vect))
	 (vector-set! num-rotations-vect shape
		      (num-rotations-slow shape)))

(define-macro (num-rotations shape)
  `(vector-ref num-rotations-vect ,shape))

;;; The same applies here for the shape-list.  We store the shapes in
;;; a vector of vectors, and use a macro for access, hoping that this
;;; will speed access.
(define shape-list-vect (make-vector 7))

(dotimes (shape (vector-length shape-list-vect))
	 (vector-set! shape-list-vect shape 
		      (make-vector (num-rotations shape))))

(define-macro (shape-list shape rotation)
  `(vector-ref
    (vector-ref shape-list-vect ,shape)
    ,rotation))

(define (set-standard-shape-vect!)
  (dotimes (shape (vector-length shape-list-vect))
	   (dotimes (pos (num-rotations shape))
		    (vector-set! (vector-ref shape-list-vect shape)
				 pos
				 (shape-list-slow shape pos)))))

(define (delete-list-el l i)
;;; Removes element i from list l
  (cond ((<= i 0) (cdr l))
	(else (cons (car l) (delete-list-el (cdr l) (- i 1))))))

(define (scramble)
;;; Scrambles the blocks so that rotate left & rotate right actually
;;; transform the shape instead of rotating it.  Call this function
;;; before playing to play a variant of stetris.
  (let ((l ()))
    (dotimes (shape (vector-length shape-list-vect))
	     (dotimes (pos (num-rotations shape))
		      (set! l
			    (cons 
			     (vector-ref (vector-ref shape-list-vect shape)
					 pos)
			     l))))
    (dotimes (shape (vector-length shape-list-vect))
	     (dotimes (pos (num-rotations shape))
		      (let ((i (random (length l))))
			(vector-set! (vector-ref shape-list-vect shape)
				     pos
				     (list-ref l i))
			(set! l (delete-list-el l i)))))))
		      
  

;;; Specifies the color that each shape has.
(define (colors-of shape)
  (case shape
	(0 "red")
	(1 "green")
	(2 "blue")
	(3 "yellow")
	(4 "purple")
	(5 "orange")
	(6 "cyan")))


(define-method quick-change ((self <stetris-piece>))
;;; Repositions the blocks of a stetris piece according to it's shape &
;;; rotation.  Basically just does this by force - setting each blocks
;;; position according to shape-list.
  (let ((x (x-of self))
	(y (y-of self)))
    (for-each
     (lambda (b p)
       (slot-set! b 'x (+ x (car p)))
       (slot-set! b 'y (+ y (cadr p)))
       (slot-set! b 'coords (block-pos-coords
			     (+ x (car p)) (+ y (cadr p)))))
     (blocks-of self)
     (shape-list (shape-of self) (rotation-of self)))))


(define (ok-spots p x y tag)
;;; p is a list of coordinate offsets from point (x y).  This routine
;;; returns true iff each coordinate in p + (x y) is a good
;;; postion for the block with the specified tag.  Basically, just
;;; makes sure that each block would be on the screen & not on top of
;;; any other blocks.  The tag is needed so that we ignore the pieces
;;; blocks themselves when checking that locations are unoccupied.
  (cond ((null? p) #t)
	(else
	 (and (ok-spot (+ x (caar p)) (+ y (cadar p)) tag)
	      (ok-spots (cdr p) x y tag)))))

(define (ok-spots-by-type shape rotation x y tag)
;;; Same as ok-spots, except takes a shape & a rotation instead of a
;;; list of coordinate offsets.  A convenient wrapper for ok-spots.
  (ok-spots (shape-list (shape-of self) (rotation-of self))
	    x y tag))
  
(define-method ok-new-spot ((self <stetris-piece>))
;;; Same as ok-spots, except gets all its arguments from a
;;; stetris-piece.  Another convenient wrapper for ok-spots.
  (ok-spots (shape-list (shape-of self) (rotation-of self))
	    (x-of self) (y-of self) (tag-of self)))

(define-method incr-rotation ((self <stetris-piece>) incr)
;;; Sets block to next rotation.
  (slot-set! self 'rotation (modulo (+ (rotation-of self) incr)
				    (num-rotations (shape-of self))))
  (if (ok-new-spot self)
      (quick-change self)
    (slot-set! self 'rotation (modulo (- (rotation-of self) incr)
				      (num-rotations (shape-of self))))))

(define-method fall ((t <stetris-piece>))
;;; Drops piece t one row (if possible).  Returns true iff the piece
;;; was able to move down.
  (cond ((can-fall? t)
	 (slot-set! t 'y (1+ (y-of t)))	   
	 (for-each fall (blocks-of t))
	 #t)
	(else
	 #f)))

(define-method can-fall? ((t <stetris-piece>))
;;; Returns true iff t can move down one row.
  (ok-spots (shape-list (shape-of t) (rotation-of t))
	    (x-of t) (1+ (y-of t)) (tag-of t)))
    
(define-method up ((t <stetris-piece>))
;;; Moves t up one row (if possible).  Returns true iff t was able to
;;; move up.
  (cond ((can-up? t)
	 (slot-set! t 'y (1- (y-of t)))	   
	 (for-each up (blocks-of t))
	 #t)
	(else
	 #f)))

(define-method can-up? ((t <stetris-piece>))
;;; Returns true iff t can move up one row.
  (ok-spots (shape-list (shape-of t) (rotation-of t))
	    (x-of t) (1- (y-of t)) (tag-of t)))

(define-method left ((t <stetris-piece>))
;;; Moves t left one column (if possible).  Returns true iff t was
;;; able to move left.
  (cond ((can-left? t)
	 (slot-set! t 'x (1- (x-of t)))	   
	 (for-each left (blocks-of t))
	 #t)
	(else
	 #f)))

(define-method can-left? ((t <stetris-piece>))
;;; Returns true iff t can move left one column.
  (ok-spots (shape-list (shape-of t) (rotation-of t))
	    (1- (x-of t)) (y-of t) (tag-of t)))

(define-method right ((t <stetris-piece>))
;;; Moves t right one column (if possible).  Returns true iff t was
;;; able to move right.
  (cond ((can-right? t)
	 (slot-set! t 'x (1+ (x-of t)))	   
	 (for-each right (blocks-of t))
	 #t)
	(else
	 #f)))

(define-method can-right? ((t <stetris-piece>))
;;; Returns true iff t can move right one column.
  (ok-spots (shape-list (shape-of t) (rotation-of t))
	    (1+ (x-of t)) (y-of t) (tag-of t)))

(define (new-game)
;;; Starts new game by clearing the screen, resetting global counts,
;;; etc.  We bind the piece moving actions here (and unbind them when
;;; the game stops) so that the user can only move pieces during game
;;; play.
  (set! quit-now #t)
  (after (* 2 fall-delay)
	 '(begin
	   (reset-vars)
	   (for-each destroy (find-items stetris-canvas 'all))
	   (set! current-piece (make-new-stetris-piece))
	   (bind-action-list game-play-bindings)
	   (update-screen))))

(define (continue-game)
;;; Continues game after a pause.
  (bind-action-list game-play-bindings)
  (cond (quit-now
	 (set! quit-now #f)
	 (update-screen))))

(define (play-sound soundfile)
;;; Plays specified sound (very crude for now - just cats it to /dev/audio).
  (! (format #f "cat ~A >/dev/audio&" soundfile)))

(define (game-sound sound)
;;; Plays specified game sound (specified by a symbol in the soundmap
;;; assoc list).
  (let ((soundfilepair (assq sound soundmap)))
    (if soundfilepair
	(play-sound (& sounddir "/" (cadr soundfilepair))))))

(define (fini)
;;; Called when the game is over.
  (cancel-movement-bindings)
  (set! game-over "game over")
  (set! quit-now #t)
  (game-sound 'game-over))

(define maybe-play-looks-bad
;;; Play the looks bad sound only when a piece stops within 8 rows
;;; from the top, and don't play it again until after the top 20 rows
;;; have been cleared.
  (let ((play #t))
    (lambda ()
      (cond ((and play 
		  (< (y-of current-piece) 8))
	     (game-sound 'near-end)
	     (set! play #f))
	    ((> (y-of current-piece) 20)
	     (set! play #t))))))

(define (update-score-value delay count)
  (set! score 
	(+ score
	   (inexact->exact 
	    (max
	     (/ 30000 (* delay count)
	     1))))))

(define (update-delay)
  (set! delta-count (1+ delta-count))
  (set! time-to-speedup (time-left))
  (cond ((> (* fall-delay  delta-count) level-time)
	 (increase-level))))

(define (increase-level)
  (let ((new-fall-delay
	 (max (inexact->exact (* delta-reducer fall-delay))
	      min-fall-delay)))
    (cond ((< new-fall-delay fall-delay)
	   (set! fall-delay new-fall-delay)
	   (set! delta-count 0)
	   (set! level-number (1+ level-number))
	   (set! time-to-speedup (time-left))
	   (game-sound 'goto-next-level)
	   #t)
	  (else #f))))

(define (update-screen)
;;; This is the game play function.  It makes sure that the pieces
;;; fall one row every fall-delay milliseconds, updates the screen,
;;; etc.
  (cond ((not quit-now)
	 (after fall-delay '(update-screen))
	 (cond ((not (fall current-piece))
;;;		(game-sound 'piece-landed)
		(maybe-play-looks-bad)
		(update "idletasks")
		(clear-filled-rows)
		(set! current-piece 
		      (make-new-stetris-piece))
		(update-score-value fall-delay 
				    (- move-count old-count))
		(set! old-count move-count)
		(cond ((not (can-fall? current-piece))
		       (fini)))))
;;;	 (game-sound 'piece-moved)
	 (set! move-count (1+ move-count))
	 (update-delay))))


(define make-new-stetris-piece
;;; Called every time a new piece is needed.
  (let ((count 0)
	(shape 0))
    (lambda ()
      (set! shape (random (vector-length shape-list-vect)))
      (set! count (1+ count))
      (make <stetris-piece> 
	    :parent stetris-canvas
	    :coords '(0 0 0 0)
	    :x (center-position shape) :y -2
	    :shape shape
	    :tag (number->string count)))))

(define (center-position shape)
;;; Proper x coord to use to get shape to appear in center of screen.
;;; I could recompute the piece offsets so that all pieces appear
;;; centered for the same stetris-piece coordinate, but that's too
;;; much work...
  (case shape
	(0 0)
	(1 1)
	(2 0)
	(3 0)
	(4 0)
	(5 1)
	(6 0)))

;;;;;; ----------------- Game Control Functions -----------------------

;;; Functions for keyboard control of pieces

(define (do-left)
  (left current-piece)
  (update "idletasks"))

(define (do-right)
  (right current-piece)
  (update "idletasks"))

(define (do-fall)
  (while (fall current-piece)
    (update "idletasks")))

(define (do-rotate-right)
  (incr-rotation current-piece 1)
  (update "idletasks"))

(define (do-rotate-left)
  (incr-rotation current-piece -1)
  (update "idletasks"))

;;; Game control functions.

(define (do-exit)
  (destroy *root*))

(define (do-new-game)
  (new-game))

(define (do-end-game)
  (fini))

(define (hide-game)
  (for-each hide (find-items stetris-canvas 'all))
  (slot-set! stetris-canvas 'background 'black))

(define (show-game)
  (for-each show (find-items stetris-canvas 'all))
  (slot-set! stetris-canvas 'background "#ffe4c4"))

(define (do-pause)
  (cond ((not quit-now)
	 (set! paused-game #t)
	 (cancel-movement-bindings)
	 (hide-game)
	 (set! quit-now #t))))

(define (do-continue-game)
  (cond (paused-game
	 (bind-action-list game-play-bindings)
	 (show-game)
	 (set! paused-game #f)
	 (continue-game))))

;;; Between game functions
(define (do-help)
  (stk::make-dialog :title "stetris help"
		    :text (help-text)
		    :buttons '( ("Ok" (lambda () ())))))

(define (do-scramble)
  (cond (quit-now
	 (scramble))))

(define (do-unscramble)
  (cond (quit-now
	 (set-standard-shape-vect!))))


(define (do-increase-level)
  (let ((tl (max 0 (ms-left))))
    (cond ((increase-level)
	   (set! score (+ score (inexact->exact 
				 (* bump-bonus 
				    (/ tl level-time)))))))))
					       

;;; ---------- Functions for binding actions to keys -----------------
(define (bind-action-list l)
  (for-each (lambda (x)
	 (bind 'all (car x) (cadr x)))
       l))

(define (cancel-bindings l)
  (bind-action-list (map (lambda (x) (list (car x) ()))
			 l)))
  
(define (cancel-movement-bindings)
  (cancel-bindings game-play-bindings))

;;; ----- Dead block maintenance routines.

(define (clear-filled-rows)
;;; Hairy function which clears all filled rows.  It explicitly
;;; garbage collects before & after doing all work since this is the
;;; only decent time for such.  When run with 75000 cells then there
;;; is no need for gc's (and thus no pauses) when blocks are falling.
  (define (row-of block) (caar block))
  (define (block-of block) (cadar block))
  (gc)
  (let ((curr-row (make-vector (1+ play-cols)))
	(curr-row-size -1)
	(curr-row-num 0)
	(amt-to-fall 0))
    (do ((blocks (sort (map (lambda (b) (list (y-of b) b))
			    (find-items stetris-canvas 'all))
		       (lambda (x y) (> (car x) (car y))))
		 (cdr blocks)))
	((null? blocks))
	(cond ((not (= curr-row-num (row-of blocks)))
	       (cond ((= curr-row-size play-cols) ; delete row
		      (dotimes (j (1+ curr-row-size))
			       (destroy (vector-ref curr-row j)))
		      (set! amt-to-fall (1+ amt-to-fall))
		      ))
	       (set! curr-row-size -1)
	       (set! curr-row-num (row-of blocks))))
	(dotimes (j amt-to-fall)
		 (fall (block-of blocks))
		 (update "idletasks"))
	(set! curr-row-size (1+ curr-row-size))
	(vector-set! curr-row curr-row-size (block-of blocks)))
    (set! score (+ score (* amt-to-fall 10)))
    (if (= amt-to-fall 3) (game-sound 'three-in-row))
    (if (= amt-to-fall 4) (game-sound 'four-in-row))
    (gc)))


(define (check-blocks)
;;; This function useful when the above function wasn't working.
  (for-each (lambda (b) (format #t "~A\n" b))
	    (sort (map (lambda (b) (list (y-of b) (x-of b) b))
		       (find-items stetris-canvas 'all))
		  (lambda (x y) (or (> (car x) (car y))
				    (and (= (car x) (car y))
					 (< (cadr x) (cadr y))))))))


;;; ----------------- Help Text ---------------------------

(define (help-text)
;; Constructs help string for help window.
  (define (pad-to len str)
    (define (pad-to-aux len l)
      (cond ((null? l)   (string->list (make-string len #\space)))
	    ((<= len 0) ())
	    (else (cons (car l)
			(pad-to-aux (1- len) (cdr l))))))
    (list->string (pad-to-aux len (string->list str))))

  (define (help-strings l)
    (map (lambda (x) (format #f "~A\t~A\n" 
			     (pad-to 12 (car x))
			     (action-description (caadr x))))
	 l))
  (apply & `(
	     "
Welcome to stetris - A falling block game reminiscent of another
falling block game whose name we won't mention :).

The game controls are as follows:\n"
	    "\n     Game control:\n"
	    ,@(help-strings control-bindings)
	    "\n     Movement control:\n"
	    ,@(help-strings game-play-bindings)
	    "\n     Other (only available between games):\n"
	    ,@(help-strings non-game-play-bindings))))



;;; ----------------- Bind the keys  --------------------

(bind-action-list control-bindings)
(bind-action-list non-game-play-bindings)

;;; ----------------- Set up some global vars -----------------
(set-standard-shape-vect!)

(gc) ; Get a gc in before starting.
 
;;; regexp for finding variable c:
;;;[ 	()
;;;]c[ 	()
;;;]
