From 199883ef02031e78e9b9fce199d2b0f244d9f3fd Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Sun, 3 Jun 2018 14:33:33 -0700 Subject: [PATCH 01/19] disable debugger. allow OOM process to crash and be restarted --- service/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/Makefile b/service/Makefile index 23c21d0..8d5ef06 100644 --- a/service/Makefile +++ b/service/Makefile @@ -3,7 +3,7 @@ SHELL=/bin/bash all: mkdir -p bin - buildapp --eval '(declaim (optimize (speed 3)))' --eval '(load #p"~/quicklisp/setup.lisp")' --eval '(load #p"tetris.asd")' --eval "(ql:quickload 'tetris-ai-rest)" --entry tetris-ai-rest:main --output bin/tetris-ai-rest + buildapp --eval '(declaim (optimize (speed 3)))' --eval '(load #p"~/quicklisp/setup.lisp")' --eval '(load #p"tetris.asd")' --eval "(ql:quickload 'tetris-ai-rest)" --eval '(disable-debugger)' --entry tetris-ai-rest:main --output bin/tetris-ai-rest clean: rm -r bin From 031cb1340fc3671a805e8a494fa9581ca2afdafe Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Wed, 20 Jun 2018 22:11:52 -0700 Subject: [PATCH 02/19] move out buildapp invocation into own script to make long-line more readable --- service/Makefile | 4 +--- service/build.sh | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100755 service/build.sh diff --git a/service/Makefile b/service/Makefile index 8d5ef06..6921165 100644 --- a/service/Makefile +++ b/service/Makefile @@ -1,9 +1,7 @@ SHELL=/bin/bash - all: mkdir -p bin - buildapp --eval '(declaim (optimize (speed 3)))' --eval '(load #p"~/quicklisp/setup.lisp")' --eval '(load #p"tetris.asd")' --eval "(ql:quickload 'tetris-ai-rest)" --eval '(disable-debugger)' --entry tetris-ai-rest:main --output bin/tetris-ai-rest - + ./build.sh bin/tetris-ai-rest clean: rm -r bin diff --git a/service/build.sh b/service/build.sh new file mode 100755 index 0000000..746f7a5 --- /dev/null +++ b/service/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash -x + +set -euo pipefail + +OUT=${1:-tetris-ai-rest} + +buildapp --eval '(declaim (optimize (speed 3)))' \ + --eval '(load #p"~/quicklisp/setup.lisp")' \ + --eval '(load #p"tetris.asd")' \ + --eval "(ql:quickload 'tetris-ai-rest)" \ + --eval "(disable-debugger)" \ + --entry tetris-ai-rest:main \ + --output ${OUT} From 6cab87b7cf60b818965540ce2b6a0dfdaa1d8845 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 00:26:55 -0700 Subject: [PATCH 03/19] add infarray data structure --- service/infarray.lisp | 63 +++++++++++++++++++++++++++++++++ service/test/infarray-test.lisp | 49 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 service/infarray.lisp create mode 100644 service/test/infarray-test.lisp diff --git a/service/infarray.lisp b/service/infarray.lisp new file mode 100644 index 0000000..d9fe454 --- /dev/null +++ b/service/infarray.lisp @@ -0,0 +1,63 @@ +(defpackage #:tetris-ai-rest/infarray + (:use :cl) + (:export + #:infarray-new + #:infarray-nth + #:infarray-push) + (:documentation "an infinite array that only remembers the last n elements")) + +(in-package #:tetris-ai-rest/infarray) + +(defstruct infarray + max-elements + page-max-elements + page + page-len + page-last + page-base-idx + forgotten-pages-count) + +(defun infarray-new (element-type &key (max-elements 10000000)) + (assert (>= max-elements 2)) + (let ((page-max-elements (floor max-elements 2))) + (make-infarray + :max-elements max-elements + :page-max-elements page-max-elements + :page (make-array page-max-elements :element-type element-type) + :page-last (make-array page-max-elements :element-type element-type) + :page-base-idx 0 + :page-len 0 + :forgotten-pages-count 0))) + +(defun infarray-nth (infarray nth) + (with-slots (page page-base-idx page-len page-last page-max-elements forgotten-pages-count) + infarray + (let ((idx (- nth page-base-idx))) + (if (>= idx 0) + (if (< idx page-len) + (aref page idx) + (values nil :out-of-bounds)) + (if (and (>= (+ idx page-max-elements) 0) + (> forgotten-pages-count 0)) + (aref page-last (+ idx page-max-elements)) + (if (< idx 0) + (values nil :out-of-bounds) + (values nil :forgotten))))))) + +(defun infarray-push (infarray elt) + (with-slots (page page-base-idx page-len page-last page-max-elements + forgotten-pages-count) + infarray + (if (< page-len page-max-elements) + (progn (setf (aref page page-len) elt) + (incf page-len)) + (let ((new-page page-last) (old-page page)) + (setf page new-page + page-last old-page) + (incf page-base-idx page-max-elements) + (incf forgotten-pages-count) + (infarray-push infarray elt))))) + +(defun infarray-length (infarray) + (with-slots (page-len page-max-elements forgotten-pages-count) infarray + (+ page-len (* forgotten-pages-count page-max-elements)))) diff --git a/service/test/infarray-test.lisp b/service/test/infarray-test.lisp new file mode 100644 index 0000000..e7fc2f4 --- /dev/null +++ b/service/test/infarray-test.lisp @@ -0,0 +1,49 @@ +(defpackage #:tetris-ai-infarray-test + ;; (:use :cl :lisp-unit) + (:use :cl) + (:import-from #:stefil + #:is) + (:import-from #:tetris-ai-rest/infarray + #:infarray-new + #:infarray-nth + #:infarray-length + #:infarray-push) + (:export #:run-tests)) + +(in-package #:tetris-ai-infarray-test) + +(stefil:deftest infarray-test nil + (let* ((infarr (infarray-new (type-of 11) :max-elements 10))) + (labels ((is-nth-ok (nth elt-exp) + (multiple-value-bind (elt err) + (infarray-nth infarr nth) + (is (eq elt elt-exp)) + (is (null err)))) + (is-nth-err (nth err-exp) + (multiple-value-bind (elt err) (infarray-nth infarr nth) + (is (eq err err-exp)) + (is (null elt))))) + + (is (eq 0 (infarray-length infarr))) + (is-nth-err 0 :out-of-bounds) + (is-nth-err -1 :out-of-bounds) + (infarray-push infarr 0) + (is (eq 1 (infarray-length infarr))) + (is-nth-ok 0 0) + (is-nth-err -1 :out-of-bounds) + + (loop for i from 1 below 10 + do (infarray-push infarr i) + do (is (eq (1+ i) (infarray-length infarr))) + do (loop for ii upto i do + (is-nth-ok ii ii))) + + (loop for i from 10 below 100 + do (infarray-push infarr i) + do (is (eq (1+ i) (infarray-length infarr))) + do (loop for ii upto i do + (if (< (- i ii) 10) + (is-nth-ok ii ii) + (is-nth-err ii :forgotten))))))) + +(infarray-test) From 2459eae3e1bb8e2c426be3c25975c6423b0a839e Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 00:27:33 -0700 Subject: [PATCH 04/19] use infarray to avoid oom --- service/server.lisp | 64 +++++++++++++++++++++++---------------------- service/tetris.asd | 4 ++- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/service/server.lisp b/service/server.lisp index bcef432..78f17ec 100644 --- a/service/server.lisp +++ b/service/server.lisp @@ -27,7 +27,12 @@ #:service-config #:grid-height-width #:config-grid-height-width - #:game-serialize-state)) + #:game-serialize-state) + (:import-from #:tetris-ai-rest/infarray + #:infarray-new + #:infarray-nth + #:infarray-length + #:infarray-push)) (in-package #:tetris-ai-rest) @@ -42,6 +47,7 @@ (max-move-catchup-wait-secs 10) (ai-depth 3) (default-ai-move-delay-millis 5) + (max-remembered-moves 10000000);; 10 million (log-filename "tetris-ai-rest.log")) (defstruct service @@ -53,9 +59,7 @@ (defstruct game-execution game - (moves (make-array 0 :adjustable t - :fill-pointer t - :element-type 'tetris-ai:game-move)) + (moves (infarray-new 'tetris-ai:game-move)) last-recorded-state running-p @@ -158,31 +162,26 @@ The capturing behavior is based on wrapping `ppcre:register-groups-bind' (defun game-exc-move (game-exc move-no &aux moves) (setf moves (game-execution-moves game-exc)) - (cond - ((< move-no (length moves)) ;; test this first, even if redundant - (values hunchentoot:+HTTP-OK+ (aref moves move-no))) - - ((not (game-execution-running-p game-exc)) - (values hunchentoot:+HTTP-REQUESTED-RANGE-NOT-SATISFIABLE+ - '(:error "requested move outside of range of completed game"))) - - (t - (loop with - max-move-catchup-wait-secs = (config-max-move-catchup-wait-secs - (service-config *service*)) - for i below max-move-catchup-wait-secs - as behind = (>= move-no (length moves)) - while behind - do (progn - (vom:debug "catching up from ~D to ~D (~D secs left)~%" - (length moves) move-no (- max-move-catchup-wait-secs i)) - (sleep 1)) - finally - (return - (if behind - (values hunchentoot:+HTTP-SERVICE-UNAVAILABLE+ - '(:error "reached timeout catching up to requested move" )) - (values hunchentoot:+HTTP-OK+ (aref moves move-no)))))))) + (multiple-value-bind (move err) (infarray-nth moves move-no) + (cond + ((null err) (values hunchentoot:+HTTP-OK+ move)) + ((eq err :forgotten) '(:error "the requested move has been forgotten by the service")) + ((not (game-execution-running-p game-exc)) + (assert (eq err :out-of-bounds)) + (values hunchentoot:+HTTP-REQUESTED-RANGE-NOT-SATISFIABLE+ + '(:error "requested move outside of range of completed game"))) + (t (loop + ;; blocking wait to allow ai thread to catch up... + with max-move-catchup-wait-secs = (config-max-move-catchup-wait-secs + (service-config *service*)) + for i below max-move-catchup-wait-secs + do (vom:debug "catching up from ~D to ~D (~D secs left)~%" + (infarray-length moves) move-no (- max-move-catchup-wait-secs i)) + thereis (multiple-value-bind (move err) (infarray-nth moves move-no) + (and (null err) (values hunchentoot:+HTTP-OK+ move))) + finally + (return (values hunchentoot:+HTTP-SERVICE-UNAVAILABLE+ + '(:error "reached timeout catching up to requested move")))))))) (define-regexp-route game-move-handler ("^/games/([0-9]+)/moves/([0-9]+)$" (#'parse-integer game-no) (#'parse-integer move-no)) @@ -231,7 +230,7 @@ until either the game is lost, or `max-moves' is reached" (progn (unless (zerop ai-move-delay-secs) (sleep ai-move-delay-secs)) - (vector-push-extend native moves))) + (infarray-push moves native))) if (zerop (mod i last-recorded-state-check-multiple)) do (setf (game-execution-last-recorded-state game-exc) @@ -247,7 +246,8 @@ until either the game is lost, or `max-moves' is reached" (unless (service-running-p *service*) (error "service not running")) - (with-slots (ai-depth grid-height-width default-ai-move-delay-millis ai-weights-file) + (with-slots (ai-depth grid-height-width default-ai-move-delay-millis + ai-weights-file max-remembered-moves) (service-config *service*) (let* ((game (destructuring-bind (height . width) grid-height-width @@ -258,6 +258,8 @@ until either the game is lost, or `max-moves' is reached" :ai-depth ai-depth))) (game-exc (apply 'make-game-execution :game game + :moves (infarray-new 'tetris-ai:game-move + :max-elements max-remembered-moves) :last-recorded-state (game-serialize-state game 0) (append make-game-exc-extra-args diff --git a/service/tetris.asd b/service/tetris.asd index 8c754e2..ccd238a 100644 --- a/service/tetris.asd +++ b/service/tetris.asd @@ -13,7 +13,9 @@ :description "A restful service on top of tetris-ai" :license "GPLv3" :author "Ernesto Alfonso " - :components ((:file "server") + :components ( + (:file "infarray") + (:file "server") (:file "ws") (:file "util") (:file "main")) From 383a6936170c09021af3fa0306b46e37fd58c76f Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 00:28:25 -0700 Subject: [PATCH 05/19] add flag to control max-remember-moves --- service/main.lisp | 1 + 1 file changed, 1 insertion(+) diff --git a/service/main.lisp b/service/main.lisp index 4027761..d3a489a 100644 --- a/service/main.lisp +++ b/service/main.lisp @@ -12,6 +12,7 @@ "dimensions of the grid, in the form of HxW, e.g. 19x10") (("ai-depth" #\d) :type integer :optional t :documentation "libtetris ai depth") (("default-ai-move-delay-millis" #\m) :type integer :optional t :documentation "delay between ai moves") + (("max-remembered-moves" #\M) :type integer :optional t :documentation "max moves to remember") (("log-filename" #\l) :type string :optional t :documentation "filename where to log connections") (("verbose" #\v) :type boolean :optional t :documentation "verbose logging") (("help" #\h) :type boolean :optional t :documentation "display help") From bd9103bc6d7d5442f5bdd57c32fd7ab44c6e0c97 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 08:50:12 -0700 Subject: [PATCH 06/19] fix not resetting page-len on rotation --- service/infarray.lisp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/infarray.lisp b/service/infarray.lisp index d9fe454..9001dbb 100644 --- a/service/infarray.lisp +++ b/service/infarray.lisp @@ -53,7 +53,8 @@ (incf page-len)) (let ((new-page page-last) (old-page page)) (setf page new-page - page-last old-page) + page-last old-page + page-len 0) (incf page-base-idx page-max-elements) (incf forgotten-pages-count) (infarray-push infarray elt))))) From f3abe445ae622a2b60c963773bd422d1ff7a8574 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 16:09:28 -0700 Subject: [PATCH 07/19] fix not returning error code as text --- service/ws.lisp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/ws.lisp b/service/ws.lisp index e6f1e35..14fd591 100644 --- a/service/ws.lisp +++ b/service/ws.lisp @@ -61,7 +61,7 @@ (game-exc (exc-resource/game-exc res))) (multiple-value-bind (ret-code data) (game-exc-move game-exc move-no) (if (not (= 200 ret-code)) - (clws:write-to-client-binary client (- ret-code)) + (clws:write-to-client-text client (write-to-string (- ret-code))) (let* ((game-move data) (packed (tetris-ai:game-move-pack game-move))) (clws:write-to-client-text client (write-to-string packed))))))) From 76872d9654ff003b8a926a51046777f868253138 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 16:23:01 -0700 Subject: [PATCH 08/19] properly pass promise result to accept function --- service/www/js/tetris_client.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/service/www/js/tetris_client.js b/service/www/js/tetris_client.js index 2b47aa1..b32d4c3 100755 --- a/service/www/js/tetris_client.js +++ b/service/www/js/tetris_client.js @@ -443,16 +443,18 @@ Game.prototype.fetchCallback = function(move) { Game.prototype.fetch = function() { // send request to fetch the next block and the AI best move var game = this; + var fetch; if (this.ws != null) { - return new Promise(function(resolve, reject) { + fetch = new Promise(function(resolve, reject) { game.ws.resolve = resolve; game.ws.reject = reject; game.ws.send(game.moveNo); }); } else { var uri = "/games/" + this.gameNo + "/moves/" + this.moveNo; - return serverRequest(uri).then(fetchCallback, gameOver); + fetch = serverRequest(uri); } + return fetch.then(this.fetchCallback.bind(this)); }; Game.prototype.init = function(gameNo) { @@ -539,7 +541,7 @@ Game.prototype.initWs = function(ws_url){ answer.r = (packed >> 8) & 0xff; answer.x = (packed >> 0) & 0xff; state.fetchCallback(answer); - state.ws.resolve(); + state.ws.resolve(answer); }); state.ws.addEventListener('open', function(event) { console.log("ws connection opened.."); From 5ea34d8fd8fd377072bbfa0f7e079bf157a65e5f Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 16:24:16 -0700 Subject: [PATCH 09/19] fix not checking for error status codes --- service/www/js/tetris_client.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/service/www/js/tetris_client.js b/service/www/js/tetris_client.js index b32d4c3..e5e7027 100755 --- a/service/www/js/tetris_client.js +++ b/service/www/js/tetris_client.js @@ -535,13 +535,16 @@ Game.prototype.initWs = function(ws_url){ state.ws = new WebSocket(state.ws_url); state.ws.addEventListener('message', function(event) { var packed = event.data; - // if (packed<0) {state.ws.reject();} - var answer = state.answer; - answer.m = (packed >> 16) & 0xff; - answer.r = (packed >> 8) & 0xff; - answer.x = (packed >> 0) & 0xff; - state.fetchCallback(answer); - state.ws.resolve(answer); + if (packed<0) { + var status_code = -packed; + state.ws.reject("bad status code from server: "+status_code); + } else { + var answer = state.answer; + answer.m = (packed >> 16) & 0xff; + answer.r = (packed >> 8) & 0xff; + answer.x = (packed >> 0) & 0xff; + state.ws.resolve(answer); + } }); state.ws.addEventListener('open', function(event) { console.log("ws connection opened.."); From faa6636fa7b67ee91249161f9de1a839bbdfb352 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 16:29:44 -0700 Subject: [PATCH 10/19] remove redundant var --- service/www/js/tetris_client.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/www/js/tetris_client.js b/service/www/js/tetris_client.js index e5e7027..c8a9be8 100755 --- a/service/www/js/tetris_client.js +++ b/service/www/js/tetris_client.js @@ -691,9 +691,8 @@ Game.prototype.gameOver = function() { Game.prototype.fetchPlanExecuteLoop = function() { // a recursive promise to continuously fetch, plan, execute - var game = this; this.fetch() - .then(this.ui.paintTo.bind(this.ui, game.b, ON)) // add active block to the UI + .then(this.ui.paintTo.bind(this.ui, this.b, ON)) // add active block to the UI .then(this.planExecute.bind(this)) .then(this.fetchPlanExecuteLoop.bind(this)); }; From badd64f3def41163909f427df2fc61f514fa5643 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 16:29:55 -0700 Subject: [PATCH 11/19] fix not returning promise from function this was causing rejections in fetchPlanExecuteLoop to go uncaught by handleError --- service/www/js/tetris_client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/www/js/tetris_client.js b/service/www/js/tetris_client.js index c8a9be8..ffc63aa 100755 --- a/service/www/js/tetris_client.js +++ b/service/www/js/tetris_client.js @@ -691,7 +691,7 @@ Game.prototype.gameOver = function() { Game.prototype.fetchPlanExecuteLoop = function() { // a recursive promise to continuously fetch, plan, execute - this.fetch() + return this.fetch() .then(this.ui.paintTo.bind(this.ui, this.b, ON)) // add active block to the UI .then(this.planExecute.bind(this)) .then(this.fetchPlanExecuteLoop.bind(this)); From 2f887f72c6cab9e399c50dd350bed6e102c7bdeb Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Thu, 21 Jun 2018 23:14:11 -0700 Subject: [PATCH 12/19] use hash-table-count, not size --- service/server.lisp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/server.lisp b/service/server.lisp index 78f17ec..f417b8e 100644 --- a/service/server.lisp +++ b/service/server.lisp @@ -266,7 +266,7 @@ until either the game is lost, or `max-moves' is reached" (list :ai-move-delay-secs (/ default-ai-move-delay-millis 1000))))) (exc-table (service-game-executions *service*)) - (game-no (HASH-TABLE-SIZE exc-table))) + (game-no (HASH-TABLE-COUNT exc-table))) (assert (service-game-executions *service*)) From 8bffc2c4b72b39823092b6103db80ff6a97d70d5 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Fri, 22 Jun 2018 09:45:52 -0700 Subject: [PATCH 13/19] fix for-loop initial declaration --- src/ai.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ai.c b/src/ai.c index 7e57d41..82569c6 100644 --- a/src/ai.c +++ b/src/ai.c @@ -40,7 +40,8 @@ double* load_weights ( char* file ) { memset(seen, 0, sizeof(seen)); char feat_name[21]; - for ( int i = 0; i < FEAT_COUNT; i++ ) { + int i; + for ( i = 0; i < FEAT_COUNT; i++ ) { double wi; if (fscanf(fh, "%20s\t%lf", feat_name, &wi) != 2) { sprintf(err, "found %d weights in %s but wanted %d\n", i, file, FEAT_COUNT ); From a03562bb9b29441145d919827f4d882c3a09a1fa Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Sun, 24 Jun 2018 17:05:47 -0700 Subject: [PATCH 14/19] simplify implementation, use a circular array --- service/infarray.lisp | 59 +++++++++++++------------------------------ 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/service/infarray.lisp b/service/infarray.lisp index 9001dbb..3a85db8 100644 --- a/service/infarray.lisp +++ b/service/infarray.lisp @@ -9,56 +9,33 @@ (in-package #:tetris-ai-rest/infarray) (defstruct infarray - max-elements - page-max-elements page - page-len - page-last - page-base-idx - forgotten-pages-count) + len + page-len) (defun infarray-new (element-type &key (max-elements 10000000)) (assert (>= max-elements 2)) - (let ((page-max-elements (floor max-elements 2))) - (make-infarray - :max-elements max-elements - :page-max-elements page-max-elements - :page (make-array page-max-elements :element-type element-type) - :page-last (make-array page-max-elements :element-type element-type) - :page-base-idx 0 - :page-len 0 - :forgotten-pages-count 0))) + (make-infarray + :page-len max-elements + :page (make-array max-elements :element-type element-type) + :len 0)) (defun infarray-nth (infarray nth) - (with-slots (page page-base-idx page-len page-last page-max-elements forgotten-pages-count) + (with-slots (page len page-len) infarray - (let ((idx (- nth page-base-idx))) - (if (>= idx 0) - (if (< idx page-len) - (aref page idx) - (values nil :out-of-bounds)) - (if (and (>= (+ idx page-max-elements) 0) - (> forgotten-pages-count 0)) - (aref page-last (+ idx page-max-elements)) - (if (< idx 0) - (values nil :out-of-bounds) - (values nil :forgotten))))))) + (if (or (< nth 0) (>= nth len)) + (values nil :out-of-bounds) + (let ((oldest-idx (- len page-len))) + (if (< nth oldest-idx) + (values nil :forgotten) + (values (aref page (mod nth page-len)))))))) (defun infarray-push (infarray elt) - (with-slots (page page-base-idx page-len page-last page-max-elements - forgotten-pages-count) + (with-slots (page page-len len) infarray - (if (< page-len page-max-elements) - (progn (setf (aref page page-len) elt) - (incf page-len)) - (let ((new-page page-last) (old-page page)) - (setf page new-page - page-last old-page - page-len 0) - (incf page-base-idx page-max-elements) - (incf forgotten-pages-count) - (infarray-push infarray elt))))) + (let ((idx (mod len page-len))) + (setf (aref page idx) elt) + (incf len)))) (defun infarray-length (infarray) - (with-slots (page-len page-max-elements forgotten-pages-count) infarray - (+ page-len (* forgotten-pages-count page-max-elements)))) + (infarray-len infarray)) From 8a74cf22a54204f17285b67c2ab82bec3ebede89 Mon Sep 17 00:00:00 2001 From: Ernesto Alfonso Date: Mon, 25 Jun 2018 03:31:59 -0700 Subject: [PATCH 15/19] explicitly do gc periodically to try to address oom based on suggestion in an sbcl possible bug report --- service/infarray.lisp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/infarray.lisp b/service/infarray.lisp index 3a85db8..1a3ad1d 100644 --- a/service/infarray.lisp +++ b/service/infarray.lisp @@ -35,6 +35,8 @@ infarray (let ((idx (mod len page-len))) (setf (aref page idx) elt) + (when (zerop idx) + (sb-ext:gc :full t)) (incf len)))) (defun infarray-length (infarray) From 89854d2fd26a8d469587ac8a2efaf1edf372a4f6 Mon Sep 17 00:00:00 2001 From: Ernesto Date: Sat, 28 Dec 2019 23:08:30 -0800 Subject: [PATCH 16/19] add old patch to preserve history --- old-patch | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 old-patch diff --git a/old-patch b/old-patch new file mode 100644 index 0000000..21e2ed2 --- /dev/null +++ b/old-patch @@ -0,0 +1,26 @@ +diff --git a/Makefile b/Makefile +index d68b2a9..c18c1cd 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,5 +1,5 @@ + CC=gcc +-CFLAGS=-Wall -W -Werror -Wextra -DNDEBUG -Ofast #-g -funroll-loops --param max-unroll-times=200 -fno-inline-functions #-fno-inline #-fno-omit-frame-pointer # -fverbose-asm -fpic ++CFLAGS=-Wall -W -Werror -Wextra -DNDEBUG -Ofast -funroll-all-loops --param max-unroll-times=200 #-g -funroll-loops --param max-unroll-times=200 -fno-inline-functions #-fno-inline #-fno-omit-frame-pointer # -fverbose-asm -fpic + DEPS=tetris.h tetri_ai.h + OBJ=ai.o game.o grid.o block.o shape.o evolution.o tetris-ncurses.o tetris-play.o + +diff --git a/tetris.c b/tetris.c +index bf59ed0..10b64af 100644 +--- a/tetris.c ++++ b/tetris.c +@@ -26,8 +26,8 @@ int main(int argc, char** argv) + ui_play(); + // ai_play(3, 1); + }else if (!strcmp(argv[1], "ai")) { +- int max_moves = 5000; +- int depth = 3; ++ int max_moves = 1000; ++ int depth = 4; + int show_grid = 0; + ai_run(max_moves, depth, show_grid); + }else if (!strcmp(opt, "evolve")) { From 539b32c3c9ac23fde46518f2abccb4e11b6fd7e6 Mon Sep 17 00:00:00 2001 From: Ernesto Date: Sat, 28 Dec 2019 23:09:03 -0800 Subject: [PATCH 17/19] Revert "add old patch to preserve history" This reverts commit 89854d2fd26a8d469587ac8a2efaf1edf372a4f6. --- old-patch | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 old-patch diff --git a/old-patch b/old-patch deleted file mode 100644 index 21e2ed2..0000000 --- a/old-patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/Makefile b/Makefile -index d68b2a9..c18c1cd 100644 ---- a/Makefile -+++ b/Makefile -@@ -1,5 +1,5 @@ - CC=gcc --CFLAGS=-Wall -W -Werror -Wextra -DNDEBUG -Ofast #-g -funroll-loops --param max-unroll-times=200 -fno-inline-functions #-fno-inline #-fno-omit-frame-pointer # -fverbose-asm -fpic -+CFLAGS=-Wall -W -Werror -Wextra -DNDEBUG -Ofast -funroll-all-loops --param max-unroll-times=200 #-g -funroll-loops --param max-unroll-times=200 -fno-inline-functions #-fno-inline #-fno-omit-frame-pointer # -fverbose-asm -fpic - DEPS=tetris.h tetri_ai.h - OBJ=ai.o game.o grid.o block.o shape.o evolution.o tetris-ncurses.o tetris-play.o - -diff --git a/tetris.c b/tetris.c -index bf59ed0..10b64af 100644 ---- a/tetris.c -+++ b/tetris.c -@@ -26,8 +26,8 @@ int main(int argc, char** argv) - ui_play(); - // ai_play(3, 1); - }else if (!strcmp(argv[1], "ai")) { -- int max_moves = 5000; -- int depth = 3; -+ int max_moves = 1000; -+ int depth = 4; - int show_grid = 0; - ai_run(max_moves, depth, show_grid); - }else if (!strcmp(opt, "evolve")) { From b94f683045b1156ad7e10eae750b659c05cb91ec Mon Sep 17 00:00:00 2001 From: Ernesto Date: Sat, 28 Dec 2019 23:35:55 -0800 Subject: [PATCH 18/19] add old js client code from 2014 --- service/front/tetris_client.js | 702 +++++++++++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100755 service/front/tetris_client.js diff --git a/service/front/tetris_client.js b/service/front/tetris_client.js new file mode 100755 index 0000000..18b067a --- /dev/null +++ b/service/front/tetris_client.js @@ -0,0 +1,702 @@ +/* + @licstart The following is the entire license notice for the + JavaScript code in this page. + + Copyright (C) 2014 Ernesto Alfonso + + The JavaScript code in this page is free software: you can + redistribute it and/or modify it under the terms of the GNU + General Public License (GNU GPL) as published by the Free Software + Foundation, either version 3 of the License, or (at your option) + any later version. The code is distributed WITHOUT ANY WARRANTY; + without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + + As additional permission under GNU GPL version 3 section 7, you + may distribute non-source (e.g., minimized or compacted) forms of + that code without the copy of the GNU GPL normally required by + section 4, provided you include this license notice and a URL + through which recipients can access the Corresponding Source. + + + @licend The above is the entire license notice + for the JavaScript code in this page. +*/ + +function createElementWithProperties(tag, props) { + var elm = document.createElement(tag); + for (var key in props) { + // elm.setAttribute(key, attrs[key]); + var val = props[key]; + var path = key.split("."); + var last = path.pop(); + var nested = path.reduce(function(cum, a) { + return cum[a] + }, elm) + nested[last] = val; + } + return elm; +} + +var ui = { + cellSize: "30", + cellGrid: [], + fontSize: "30px", + loading: createElementWithProperties("img", { + hw: ["400", "550"], + show: function(show) { + if (show) { + this.height = this.hw[0]; + this.width = this.hw[1]; + this.style = ""; + } else { + this.height = "0"; + this.width = "0"; + this.style = "visibility:hidden"; + + } + } + }), + moveNoElm: null, + colors: { + 'BLUE': "#0000f0", + 'BLACK': "#000000", + 'WHITE': "#ffffff", + 'GREEN': 3, + filled: this.BLUE, + blank: this.WHITE + }, + tableCreate: function(width, height) { + + var body = document.getElementsByTagName("body")[0]; + + this.loading.show(false); + body.appendChild(this.loading); + + var tbl = document.createElement("table"); + tbl.class = "table"; + var tblBody = document.createElement("tbody"); + + for (var j = 0; j < height; j++) { + cellRow = []; + this.cellGrid.push(cellRow); + var row = document.createElement("tr"); + for (var i = 0; i < width; i++) { + var cell = document.createElement("td"); + cellRow.push(cell); + + cell.width = this.cellSize; + cell.height = this.cellSize; + cell.bgColor = this.colors.blank; + cell.style.border = "1px solid #000" + + var cellText = document.createTextNode(""); + cell.appendChild(cellText); + row.appendChild(cell); + } + + tblBody.appendChild(row); + } + tbl.appendChild(tblBody); + body.appendChild(tbl); + tbl.setAttribute("border", "2"); + + body.appendChild(createElementWithProperties( + "label", { + innerHTML: "Move ", + "style.fontSize": this.fontSize + })); + this.moveNoElm = (createElementWithProperties( + "label", { + "style.fontSize": this.fontSize + })); + body.appendChild(this.moveNoElm); + + body.appendChild(document.createElement("br")); + + body.appendChild(createElementWithProperties( + "label", { + innerHTML: "Speed ", + "style.fontSize": this.fontSize + })); + + this.slider = createElementWithProperties( + "input", { + type: "range", + min: 1, + max: 100, + value: TIMER_DELAY, + invertValue: function(val) { + return parseInt(this.max) - val + parseInt(this.min); + }, + onchange: function() { + TIMER_DELAY = this.invertValue(this.value); + } + }); + + this.slider.value = this.slider.invertValue(TIMER_DELAY); + + body.appendChild(this.slider); + }, + paint: function(r, c, color) { + assert(color != null); + this.cellGrid[r][c].bgColor = color; + } +} + +ui.colors.filled = ui.colors.BLUE; +ui.colors.blank = ui.colors.WHITE; + +var RETRY_TIMEOUT = 500; +var SERVER_TIMEOUT = 20000; +var TIMER_DELAY = 90; + +function assert(condition, message) { + + // todo: upload stack trace + + if (!condition) { + message = message || "Assertion failed" + alert(message); + debugger; + throw message; + } +} + +function serverRequest(requestcode, responseHanlder) { + + assert(requestcode != null && responseHanlder != null); + + var xhr = new XMLHttpRequest(); + xhr.open('get', requestcode); + xhr.onreadystatechange = function() { + // Ready state 4 means the request is done + if (xhr.readyState === 4) { + if (xhr.status != 200) { + error(requestcode + " returned non-200 status: " + xhr.status + ": server response: " + xhr.responseText); + } else { + var response = null; + try { + response = JSON.parse(xhr.responseText); + } catch (err) { + console.log("failed to parse request: " + xhr.responseText); + + state.consecFailedMills += RETRY_TIMEOUT; + + if (state.consecFailedMills > SERVER_TIMEOUT) { + error("server seems unresponsive. try again later") + } else { + setTimeout(function() { + serverRequest(requestcode, responseHandler) + }, RETRY_TIMEOUT); + } + return; + } + assert(!(response == null), " error from server"); + state.consecFailedMills = 0; + responseHanlder(response); + } + } + } + xhr.send(null); +} + +var state = { + moveQueue: [], + b: { //active block + m: null, //model number + r: null, //rotation state + x: null, //x distance from left + y: null //y from top + }, + answer: { //server game move response + r: null, + x: null + }, + pausedP: false, + gameOver: false, + moveNo: null, + gameNo: null, + shapes: null, + consecFailedMills: 0, + + grid: { + relief: null, + width: null, + height: null, + needClear: [], + needsClear: false, + rowcounts: null, + g: null + } +} + +function bIter() { + return (function() { + var i = 0; + + var x = state.b.x; + var y = state.b.y; + var rotCoords = state.shapes[state.b.m].rotations[state.b.r].configurations; + + var cont = { + value: [null, null], + done: false + }; + var done = { + done: true + }; + return { + next: function() { + if (i < rotCoords.length) { + cont.value[0] = rotCoords[i][0] + x; + cont.value[1] = rotCoords[i][1] + y; + i++; + return cont; + } else { + return done; + } + }, + hasNext: function() { + return i < rotCoords.length; + } + } + })(); +} + +function gridBlockIntersects() { + for (var itr = bIter(); itr.hasNext();) { + var xy = itr.next().value; + assert(ui.cellGrid[xy[1]][xy[0]].bgColor == ui.colors.blank || + state.grid.g[xy[1]][xy[0]] == ui.colors.filled); + if (ui.cellGrid[xy[1]][xy[0]].bgColor != ui.colors.blank) { + return true; + } + } + return false; +} + +function paintTo(color, checkIntersects) { + if (checkIntersects && gridBlockIntersects()) { + return true; + } else { + for (var itr = bIter(); itr.hasNext();) { + var xy = itr.next().value; + ui.paint(xy[1], xy[0], color); + } + return true; + } +} + +function moveTetro(moveFun) { + paintTo(ui.colors.blank); + moveFun(); + var succ = paintTo(ui.colors.filled, true); //undo last move and repaint if this doesn't succeed + if (!succ) { + state.gameOver = true; + moveFun(true); //undo + paintTo(ui.colors.filled); + } +} + +function addTetro() { + paintTo(ui.colors.filled); +} + + +function left(undo) { + !undo ? state.b.x-- : state.b.x++; +} + +function right(undo) { + !undo ? state.b.x++ : state.b.x--; +} + +function rotcw(undo) { + !undo ? state.b.r++ : state.b.r--; + state.b.r %= 4; +} + +function rotccw(undo) { + !undo ? state.b.r-- : state.b.r++; +} + +function down(undo) { + !undo ? state.b.y++ : state.b.y--; +} + +function getDropDistance() { + var b = state.b; + var botCrust = state.shapes[b.m].rotations[b.r].crusts["bot"]; + + var grid = state.grid; + var dist, minDist = grid.height; + var x = state.b.x; + var y = state.b.y; + + for (var i = 0, reliefY; i < botCrust.length; i++) { + xy = botCrust[i]; + reliefY = grid.relief[xy[0] + b.x]; + dist = reliefY - xy[1]; + if (dist < minDist) { + minDist = dist; + newY = reliefY; + } + } + + var dropDist = minDist - 1 - b.y; + return dropDist; +} + +function drop() { + var grid = state.grid; + var dropDistance = getDropDistance(); + if (dropDistance < 0) { + state.gameOver = true; + } else { + + var b = state.b; + b.y += dropDistance; + + var topCrust = state.shapes[b.m].rotations[b.r].crusts["top"]; + for (var i = 0, xy = null; i < topCrust.length; i++) { + xy = topCrust[i]; + grid.relief[xy[0] + b.x] = xy[1] + b.y; + } + + for (var itr = bIter(); itr.hasNext();) { + var xy = itr.next().value; + if (++grid.rowcounts[xy[1]] == grid.width) { + grid.needsClear = true; + grid.needClear.push(xy[1]); + } + grid.g[xy[1]][xy[0]] = ui.colors.filled; + } + } +} + +function listMin(list) { + var min = null; + for (var i = 0; i < list.length; i++) { + if (min == null || list[i] < min) + min = list[i]; + } + return min; +} + +function clearLines() { + var grid = state.grid; + if (!grid.needsClear) return; + + var cmpNum = function(a, b) { + return a - b + } + // grid.lastCleared = grid.needClear.length; + if (grid.needClear.length > 0) { + // cmpNum is necessary, otherwise sort is lexicographic, eg 10<9 + // would be nice to make needClear a pqueue + grid.needClear.sort(cmpNum); //smallest to largest + } + const YMIN = grid.needClear[grid.needClear.length - 1]; + const YMAX = listMin(grid.relief); //the tallest row is 0. ymax should be as small as possible + var y = YMIN; + // grid.needClear.reverse (); + var nextNonFull = y - 1; //not necessarily non-full here + var cleared = []; + + while (nextNonFull >= YMAX) { + while (grid.rowcounts[nextNonFull] == grid.width) + nextNonFull -= 1; + if (nextNonFull < YMAX) + break; + //nextNonFull should be non-full now + if (grid.rowcounts[y] == grid.width) { + assert(grid.needClear[grid.needClear.length - 1] == y, " assertion failed at 485 "); + grid.needClear.pop(); + cleared.push(grid.g[y]); + } + //copy nextNonFull row into y-th row + grid.g[y] = grid.g[nextNonFull]; + grid.rowcounts[y] = grid.rowcounts[nextNonFull]; + y -= 1; + nextNonFull -= 1; + } + + while (grid.needClear.length > 0) + cleared.push(grid.g[grid.needClear.pop()]); + + while (y >= YMAX) { + // assert((cleared.length>0 && sum(cleared[0])==grid.width) + // || sum(grid.g[y])==grid.width); + + assert(cleared.length > 0, " cleared.length assertion "); + grid.g[y] = cleared.pop(); + grid.rowcounts[y] = 0; + for (var i = 0; i < grid.width; i++) + grid.g[y][i] = ui.colors.blank; + y -= 1; + } + + assert(grid.needClear.length == 0, " grid.needClear.length==0 assertion failed"); + assert(cleared.length == 0, "cleared.length==0 assertion failed"); + + for (var i = 0; i < grid.width; i++) { + var relief = grid.relief[i]; + while (relief < grid.height && grid.g[relief][i] == ui.colors.blank) + relief += 1; + grid.relief[i] = relief; + } + + grid.needsClear = false; + repaintRows(YMAX, YMIN + 1); +} + +function repaintRows(ymin, ymax) { + var grid = state.grid; + for (; ymin < ymax; ymin++) { + for (var x = 0; x < grid.width; x++) { + ui.paint(ymin, x, grid.g[ymin][x] || ui.colors.blank); + } + } +} + +function fetch(response) { + assert(state.gameNo != null && state.moveNo != null); + + if (response == null) { + var uri = "/games/" + state.gameNo + "/moves/" + state.moveNo; + serverRequest(uri, fetch); + return; + } else { + move = response; + + state.b.m = move.shape, state.b.r = 0, state.b.x = state.grid.width / 2 - 1, state.b.y = 0; + state.answer.r = move.rot, state.answer.x = move.col; + state.moveNo++; + ui.moveNoElm.innerHTML = state.moveNo; + timer(); + } +} + +function init(response) { + if (response == null) { + serverRequest("/games/" + state.gameNo, init); + return; + } + var grid = state.grid; + + var miny = grid.height; + + game = response; + + var skip = false; // start from move 0` + if (skip) { + game.move_no = -1; + game.on_cells = []; + } + + state.moveNo = game.move_no; + + // game.moveNo is for current move. + state.moveNo++; + + console.log("move no is: " + state.moveNo); + grid.width = game.width; + grid.height = game.height; + + grid.rowcounts = [] + grid.g = []; + grid.relief = []; + state.answerRx = [null, null]; + + ui.tableCreate(grid.width, grid.height); //delete previous table + + for (var i = 0; i < grid.height; i++) { + grid.rowcounts.push(0); + var row = []; + grid.g.push(row); + for (var ii = 0; ii < grid.height; ii++) + row.push(ui.colors.blank); + } + for (var i = 0; i < grid.width; i++) { + grid.relief.push(grid.height); + } + + for (var i = 0; i < game.on_cells.length; i++) { + xy = game.on_cells[i]; + x = xy % grid.width; + y = Math.floor(xy / grid.width); + y = grid.height - 1 - y; + + grid.g[y][x] = ui.colors.filled; + ui.paint(y, x, ui.colors.filled); + if (y < miny) + miny = y; + if (y < grid.relief[x]) { + grid.relief[x] = y; + } + grid.rowcounts[y]++; + } + repaintRows(0, miny); + timer(); +} + +function initShapes(response) { + if (response == null) { + serverRequest("shapes", initShapes) + } else { + state.shapes = response; + if (state.shapes.length == 0) { + error("0 shapes received from server!"); + } + for (var i = 0; i < state.shapes.length; i++) { + var shape = state.shapes[i]; + var rots = shape.rotations; + for (var r = 0; r < rots.length; r++) { + var zeroSeen = false; + var rot = rots[r]; + var rotH = rot.height; + var rotCoords = rot.configurations; + for (var b = 0; b < rotCoords.length; b++) { + var cr = rotCoords[b]; + cr[1] *= -1; + cr[1] += rotH - 1; + assert(cr[1] >= 0); + zeroSeen = zeroSeen || cr[1] == 0; + } + assert(zeroSeen); + + CRUSTNAMES = ["top", "bot", "left", "right"]; + for (var c = 0; c < CRUSTNAMES.length; c++) { + var crust = rot.crusts[CRUSTNAMES[c]]; + for (var b = 0; b < crust.length; b++) { + var cr = crust[b]; + cr[1] *= -1; + cr[1] += rotH - 1; + assert(cr[1] >= 0); + } + } + } + } + timer(); + } +} + +function error(message) { + console.log(new Error().stack); + var msg = "error: " + message; + console.log(msg); + alert(msg); +} + +function initGameNo(response) { + if (response == null) { + serverRequest("/games", initGameNo); + } else { + gamenoList = response; + if (gamenoList.length == 0) { + error("no current games on server"); + } else { + state.gameNo = gamenoList[gamenoList.length - 1]; + console.log("init gameNo is " + state.gameNo); + timer(); + } + } +} + +function plan() { + for (var r = state.b.r, direc = state.b.r < state.answer.r ? 1 : -1; r != state.answer.r; r += direc) { + state.moveQueue.push(direc > 0 ? rotcw : rotcw); + } + for (var x = state.b.x, direc = state.b.x < state.answer.x ? 1 : -1; x != state.answer.x; x += direc) { + state.moveQueue.push(direc > 0 ? right : left); + } + state.moveQueue.push(drop); + state.moveQueue.push(clearLines); + state.moveQueue.push(fetch); + state.moveQueue.push(addTetro); + state.moveQueue.push(plan); +} + +function pauseToggle() { + state.pausedP = !pauseP; +} + +function gameOverFun() { + alert("game over!"); + console.log("game over"); +} + +function timer() { + if (state.pausedP) { + return; + } + if (state.moveQueue.length > 0) { + var move = state.moveQueue.shift(); + if (move.name in paintMoves) { + moveTetro(move); + } else { + move(); + } + if (state.gameOver) { + gameOverFun(); + } else if (!(move.name in twoStepMoves)) { + if ((move.name in noDelayMoves)) { + timer(); + } else { + var extra = move.name == "plan" ? 200 * Math.random() : 0; + setTimeout(timer, TIMER_DELAY + extra); + } + + } + //otherwise two-step-move must bring timer back to life + + } else { + alert("no more pending moves"); + } +} + +paintMoves = { + rotcw: true, + rotccw: true, + drop: true, + left: true, + right: true, + down: true +}; +twoStepMoves = { + fetch: true, + init: true, + initShapes: true, + initGameNo: true +}; +noDelayMoves = { + fetch: true, + init: true +}; + +/*unfortunate hack for IE, in which function.name doesn't work:*/ +init.name = "init"; +fetch.name = "fetch"; +rotcw.name = "rotcw"; +rotccw.name = "rotccw"; +left.name = "left"; +right.name = "right"; +drop.name = "drop"; +down.name = "down"; +initShapes.name = "initShapes"; +clearLines.name = "clearLines"; +pauseToggle.name = "pauseToggle"; +plan.name = "plan"; +addTetro.name = "addTetro"; + +state.moveQueue.push(initGameNo); +state.moveQueue.push(init); +state.moveQueue.push(initShapes); +state.moveQueue.push(fetch); +state.moveQueue.push(addTetro); +state.moveQueue.push(plan); + +console.log("hola"); + +timer(); From 5a1eed9fa096aa0cf5dbeceeb31888af37a70649 Mon Sep 17 00:00:00 2001 From: Ernesto Date: Sat, 28 Dec 2019 23:36:13 -0800 Subject: [PATCH 19/19] Revert "add old js client code from 2014" This reverts commit b94f683045b1156ad7e10eae750b659c05cb91ec. --- service/front/tetris_client.js | 702 --------------------------------- 1 file changed, 702 deletions(-) delete mode 100755 service/front/tetris_client.js diff --git a/service/front/tetris_client.js b/service/front/tetris_client.js deleted file mode 100755 index 18b067a..0000000 --- a/service/front/tetris_client.js +++ /dev/null @@ -1,702 +0,0 @@ -/* - @licstart The following is the entire license notice for the - JavaScript code in this page. - - Copyright (C) 2014 Ernesto Alfonso - - The JavaScript code in this page is free software: you can - redistribute it and/or modify it under the terms of the GNU - General Public License (GNU GPL) as published by the Free Software - Foundation, either version 3 of the License, or (at your option) - any later version. The code is distributed WITHOUT ANY WARRANTY; - without even the implied warranty of MERCHANTABILITY or FITNESS - FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. - - As additional permission under GNU GPL version 3 section 7, you - may distribute non-source (e.g., minimized or compacted) forms of - that code without the copy of the GNU GPL normally required by - section 4, provided you include this license notice and a URL - through which recipients can access the Corresponding Source. - - - @licend The above is the entire license notice - for the JavaScript code in this page. -*/ - -function createElementWithProperties(tag, props) { - var elm = document.createElement(tag); - for (var key in props) { - // elm.setAttribute(key, attrs[key]); - var val = props[key]; - var path = key.split("."); - var last = path.pop(); - var nested = path.reduce(function(cum, a) { - return cum[a] - }, elm) - nested[last] = val; - } - return elm; -} - -var ui = { - cellSize: "30", - cellGrid: [], - fontSize: "30px", - loading: createElementWithProperties("img", { - hw: ["400", "550"], - show: function(show) { - if (show) { - this.height = this.hw[0]; - this.width = this.hw[1]; - this.style = ""; - } else { - this.height = "0"; - this.width = "0"; - this.style = "visibility:hidden"; - - } - } - }), - moveNoElm: null, - colors: { - 'BLUE': "#0000f0", - 'BLACK': "#000000", - 'WHITE': "#ffffff", - 'GREEN': 3, - filled: this.BLUE, - blank: this.WHITE - }, - tableCreate: function(width, height) { - - var body = document.getElementsByTagName("body")[0]; - - this.loading.show(false); - body.appendChild(this.loading); - - var tbl = document.createElement("table"); - tbl.class = "table"; - var tblBody = document.createElement("tbody"); - - for (var j = 0; j < height; j++) { - cellRow = []; - this.cellGrid.push(cellRow); - var row = document.createElement("tr"); - for (var i = 0; i < width; i++) { - var cell = document.createElement("td"); - cellRow.push(cell); - - cell.width = this.cellSize; - cell.height = this.cellSize; - cell.bgColor = this.colors.blank; - cell.style.border = "1px solid #000" - - var cellText = document.createTextNode(""); - cell.appendChild(cellText); - row.appendChild(cell); - } - - tblBody.appendChild(row); - } - tbl.appendChild(tblBody); - body.appendChild(tbl); - tbl.setAttribute("border", "2"); - - body.appendChild(createElementWithProperties( - "label", { - innerHTML: "Move ", - "style.fontSize": this.fontSize - })); - this.moveNoElm = (createElementWithProperties( - "label", { - "style.fontSize": this.fontSize - })); - body.appendChild(this.moveNoElm); - - body.appendChild(document.createElement("br")); - - body.appendChild(createElementWithProperties( - "label", { - innerHTML: "Speed ", - "style.fontSize": this.fontSize - })); - - this.slider = createElementWithProperties( - "input", { - type: "range", - min: 1, - max: 100, - value: TIMER_DELAY, - invertValue: function(val) { - return parseInt(this.max) - val + parseInt(this.min); - }, - onchange: function() { - TIMER_DELAY = this.invertValue(this.value); - } - }); - - this.slider.value = this.slider.invertValue(TIMER_DELAY); - - body.appendChild(this.slider); - }, - paint: function(r, c, color) { - assert(color != null); - this.cellGrid[r][c].bgColor = color; - } -} - -ui.colors.filled = ui.colors.BLUE; -ui.colors.blank = ui.colors.WHITE; - -var RETRY_TIMEOUT = 500; -var SERVER_TIMEOUT = 20000; -var TIMER_DELAY = 90; - -function assert(condition, message) { - - // todo: upload stack trace - - if (!condition) { - message = message || "Assertion failed" - alert(message); - debugger; - throw message; - } -} - -function serverRequest(requestcode, responseHanlder) { - - assert(requestcode != null && responseHanlder != null); - - var xhr = new XMLHttpRequest(); - xhr.open('get', requestcode); - xhr.onreadystatechange = function() { - // Ready state 4 means the request is done - if (xhr.readyState === 4) { - if (xhr.status != 200) { - error(requestcode + " returned non-200 status: " + xhr.status + ": server response: " + xhr.responseText); - } else { - var response = null; - try { - response = JSON.parse(xhr.responseText); - } catch (err) { - console.log("failed to parse request: " + xhr.responseText); - - state.consecFailedMills += RETRY_TIMEOUT; - - if (state.consecFailedMills > SERVER_TIMEOUT) { - error("server seems unresponsive. try again later") - } else { - setTimeout(function() { - serverRequest(requestcode, responseHandler) - }, RETRY_TIMEOUT); - } - return; - } - assert(!(response == null), " error from server"); - state.consecFailedMills = 0; - responseHanlder(response); - } - } - } - xhr.send(null); -} - -var state = { - moveQueue: [], - b: { //active block - m: null, //model number - r: null, //rotation state - x: null, //x distance from left - y: null //y from top - }, - answer: { //server game move response - r: null, - x: null - }, - pausedP: false, - gameOver: false, - moveNo: null, - gameNo: null, - shapes: null, - consecFailedMills: 0, - - grid: { - relief: null, - width: null, - height: null, - needClear: [], - needsClear: false, - rowcounts: null, - g: null - } -} - -function bIter() { - return (function() { - var i = 0; - - var x = state.b.x; - var y = state.b.y; - var rotCoords = state.shapes[state.b.m].rotations[state.b.r].configurations; - - var cont = { - value: [null, null], - done: false - }; - var done = { - done: true - }; - return { - next: function() { - if (i < rotCoords.length) { - cont.value[0] = rotCoords[i][0] + x; - cont.value[1] = rotCoords[i][1] + y; - i++; - return cont; - } else { - return done; - } - }, - hasNext: function() { - return i < rotCoords.length; - } - } - })(); -} - -function gridBlockIntersects() { - for (var itr = bIter(); itr.hasNext();) { - var xy = itr.next().value; - assert(ui.cellGrid[xy[1]][xy[0]].bgColor == ui.colors.blank || - state.grid.g[xy[1]][xy[0]] == ui.colors.filled); - if (ui.cellGrid[xy[1]][xy[0]].bgColor != ui.colors.blank) { - return true; - } - } - return false; -} - -function paintTo(color, checkIntersects) { - if (checkIntersects && gridBlockIntersects()) { - return true; - } else { - for (var itr = bIter(); itr.hasNext();) { - var xy = itr.next().value; - ui.paint(xy[1], xy[0], color); - } - return true; - } -} - -function moveTetro(moveFun) { - paintTo(ui.colors.blank); - moveFun(); - var succ = paintTo(ui.colors.filled, true); //undo last move and repaint if this doesn't succeed - if (!succ) { - state.gameOver = true; - moveFun(true); //undo - paintTo(ui.colors.filled); - } -} - -function addTetro() { - paintTo(ui.colors.filled); -} - - -function left(undo) { - !undo ? state.b.x-- : state.b.x++; -} - -function right(undo) { - !undo ? state.b.x++ : state.b.x--; -} - -function rotcw(undo) { - !undo ? state.b.r++ : state.b.r--; - state.b.r %= 4; -} - -function rotccw(undo) { - !undo ? state.b.r-- : state.b.r++; -} - -function down(undo) { - !undo ? state.b.y++ : state.b.y--; -} - -function getDropDistance() { - var b = state.b; - var botCrust = state.shapes[b.m].rotations[b.r].crusts["bot"]; - - var grid = state.grid; - var dist, minDist = grid.height; - var x = state.b.x; - var y = state.b.y; - - for (var i = 0, reliefY; i < botCrust.length; i++) { - xy = botCrust[i]; - reliefY = grid.relief[xy[0] + b.x]; - dist = reliefY - xy[1]; - if (dist < minDist) { - minDist = dist; - newY = reliefY; - } - } - - var dropDist = minDist - 1 - b.y; - return dropDist; -} - -function drop() { - var grid = state.grid; - var dropDistance = getDropDistance(); - if (dropDistance < 0) { - state.gameOver = true; - } else { - - var b = state.b; - b.y += dropDistance; - - var topCrust = state.shapes[b.m].rotations[b.r].crusts["top"]; - for (var i = 0, xy = null; i < topCrust.length; i++) { - xy = topCrust[i]; - grid.relief[xy[0] + b.x] = xy[1] + b.y; - } - - for (var itr = bIter(); itr.hasNext();) { - var xy = itr.next().value; - if (++grid.rowcounts[xy[1]] == grid.width) { - grid.needsClear = true; - grid.needClear.push(xy[1]); - } - grid.g[xy[1]][xy[0]] = ui.colors.filled; - } - } -} - -function listMin(list) { - var min = null; - for (var i = 0; i < list.length; i++) { - if (min == null || list[i] < min) - min = list[i]; - } - return min; -} - -function clearLines() { - var grid = state.grid; - if (!grid.needsClear) return; - - var cmpNum = function(a, b) { - return a - b - } - // grid.lastCleared = grid.needClear.length; - if (grid.needClear.length > 0) { - // cmpNum is necessary, otherwise sort is lexicographic, eg 10<9 - // would be nice to make needClear a pqueue - grid.needClear.sort(cmpNum); //smallest to largest - } - const YMIN = grid.needClear[grid.needClear.length - 1]; - const YMAX = listMin(grid.relief); //the tallest row is 0. ymax should be as small as possible - var y = YMIN; - // grid.needClear.reverse (); - var nextNonFull = y - 1; //not necessarily non-full here - var cleared = []; - - while (nextNonFull >= YMAX) { - while (grid.rowcounts[nextNonFull] == grid.width) - nextNonFull -= 1; - if (nextNonFull < YMAX) - break; - //nextNonFull should be non-full now - if (grid.rowcounts[y] == grid.width) { - assert(grid.needClear[grid.needClear.length - 1] == y, " assertion failed at 485 "); - grid.needClear.pop(); - cleared.push(grid.g[y]); - } - //copy nextNonFull row into y-th row - grid.g[y] = grid.g[nextNonFull]; - grid.rowcounts[y] = grid.rowcounts[nextNonFull]; - y -= 1; - nextNonFull -= 1; - } - - while (grid.needClear.length > 0) - cleared.push(grid.g[grid.needClear.pop()]); - - while (y >= YMAX) { - // assert((cleared.length>0 && sum(cleared[0])==grid.width) - // || sum(grid.g[y])==grid.width); - - assert(cleared.length > 0, " cleared.length assertion "); - grid.g[y] = cleared.pop(); - grid.rowcounts[y] = 0; - for (var i = 0; i < grid.width; i++) - grid.g[y][i] = ui.colors.blank; - y -= 1; - } - - assert(grid.needClear.length == 0, " grid.needClear.length==0 assertion failed"); - assert(cleared.length == 0, "cleared.length==0 assertion failed"); - - for (var i = 0; i < grid.width; i++) { - var relief = grid.relief[i]; - while (relief < grid.height && grid.g[relief][i] == ui.colors.blank) - relief += 1; - grid.relief[i] = relief; - } - - grid.needsClear = false; - repaintRows(YMAX, YMIN + 1); -} - -function repaintRows(ymin, ymax) { - var grid = state.grid; - for (; ymin < ymax; ymin++) { - for (var x = 0; x < grid.width; x++) { - ui.paint(ymin, x, grid.g[ymin][x] || ui.colors.blank); - } - } -} - -function fetch(response) { - assert(state.gameNo != null && state.moveNo != null); - - if (response == null) { - var uri = "/games/" + state.gameNo + "/moves/" + state.moveNo; - serverRequest(uri, fetch); - return; - } else { - move = response; - - state.b.m = move.shape, state.b.r = 0, state.b.x = state.grid.width / 2 - 1, state.b.y = 0; - state.answer.r = move.rot, state.answer.x = move.col; - state.moveNo++; - ui.moveNoElm.innerHTML = state.moveNo; - timer(); - } -} - -function init(response) { - if (response == null) { - serverRequest("/games/" + state.gameNo, init); - return; - } - var grid = state.grid; - - var miny = grid.height; - - game = response; - - var skip = false; // start from move 0` - if (skip) { - game.move_no = -1; - game.on_cells = []; - } - - state.moveNo = game.move_no; - - // game.moveNo is for current move. - state.moveNo++; - - console.log("move no is: " + state.moveNo); - grid.width = game.width; - grid.height = game.height; - - grid.rowcounts = [] - grid.g = []; - grid.relief = []; - state.answerRx = [null, null]; - - ui.tableCreate(grid.width, grid.height); //delete previous table - - for (var i = 0; i < grid.height; i++) { - grid.rowcounts.push(0); - var row = []; - grid.g.push(row); - for (var ii = 0; ii < grid.height; ii++) - row.push(ui.colors.blank); - } - for (var i = 0; i < grid.width; i++) { - grid.relief.push(grid.height); - } - - for (var i = 0; i < game.on_cells.length; i++) { - xy = game.on_cells[i]; - x = xy % grid.width; - y = Math.floor(xy / grid.width); - y = grid.height - 1 - y; - - grid.g[y][x] = ui.colors.filled; - ui.paint(y, x, ui.colors.filled); - if (y < miny) - miny = y; - if (y < grid.relief[x]) { - grid.relief[x] = y; - } - grid.rowcounts[y]++; - } - repaintRows(0, miny); - timer(); -} - -function initShapes(response) { - if (response == null) { - serverRequest("shapes", initShapes) - } else { - state.shapes = response; - if (state.shapes.length == 0) { - error("0 shapes received from server!"); - } - for (var i = 0; i < state.shapes.length; i++) { - var shape = state.shapes[i]; - var rots = shape.rotations; - for (var r = 0; r < rots.length; r++) { - var zeroSeen = false; - var rot = rots[r]; - var rotH = rot.height; - var rotCoords = rot.configurations; - for (var b = 0; b < rotCoords.length; b++) { - var cr = rotCoords[b]; - cr[1] *= -1; - cr[1] += rotH - 1; - assert(cr[1] >= 0); - zeroSeen = zeroSeen || cr[1] == 0; - } - assert(zeroSeen); - - CRUSTNAMES = ["top", "bot", "left", "right"]; - for (var c = 0; c < CRUSTNAMES.length; c++) { - var crust = rot.crusts[CRUSTNAMES[c]]; - for (var b = 0; b < crust.length; b++) { - var cr = crust[b]; - cr[1] *= -1; - cr[1] += rotH - 1; - assert(cr[1] >= 0); - } - } - } - } - timer(); - } -} - -function error(message) { - console.log(new Error().stack); - var msg = "error: " + message; - console.log(msg); - alert(msg); -} - -function initGameNo(response) { - if (response == null) { - serverRequest("/games", initGameNo); - } else { - gamenoList = response; - if (gamenoList.length == 0) { - error("no current games on server"); - } else { - state.gameNo = gamenoList[gamenoList.length - 1]; - console.log("init gameNo is " + state.gameNo); - timer(); - } - } -} - -function plan() { - for (var r = state.b.r, direc = state.b.r < state.answer.r ? 1 : -1; r != state.answer.r; r += direc) { - state.moveQueue.push(direc > 0 ? rotcw : rotcw); - } - for (var x = state.b.x, direc = state.b.x < state.answer.x ? 1 : -1; x != state.answer.x; x += direc) { - state.moveQueue.push(direc > 0 ? right : left); - } - state.moveQueue.push(drop); - state.moveQueue.push(clearLines); - state.moveQueue.push(fetch); - state.moveQueue.push(addTetro); - state.moveQueue.push(plan); -} - -function pauseToggle() { - state.pausedP = !pauseP; -} - -function gameOverFun() { - alert("game over!"); - console.log("game over"); -} - -function timer() { - if (state.pausedP) { - return; - } - if (state.moveQueue.length > 0) { - var move = state.moveQueue.shift(); - if (move.name in paintMoves) { - moveTetro(move); - } else { - move(); - } - if (state.gameOver) { - gameOverFun(); - } else if (!(move.name in twoStepMoves)) { - if ((move.name in noDelayMoves)) { - timer(); - } else { - var extra = move.name == "plan" ? 200 * Math.random() : 0; - setTimeout(timer, TIMER_DELAY + extra); - } - - } - //otherwise two-step-move must bring timer back to life - - } else { - alert("no more pending moves"); - } -} - -paintMoves = { - rotcw: true, - rotccw: true, - drop: true, - left: true, - right: true, - down: true -}; -twoStepMoves = { - fetch: true, - init: true, - initShapes: true, - initGameNo: true -}; -noDelayMoves = { - fetch: true, - init: true -}; - -/*unfortunate hack for IE, in which function.name doesn't work:*/ -init.name = "init"; -fetch.name = "fetch"; -rotcw.name = "rotcw"; -rotccw.name = "rotccw"; -left.name = "left"; -right.name = "right"; -drop.name = "drop"; -down.name = "down"; -initShapes.name = "initShapes"; -clearLines.name = "clearLines"; -pauseToggle.name = "pauseToggle"; -plan.name = "plan"; -addTetro.name = "addTetro"; - -state.moveQueue.push(initGameNo); -state.moveQueue.push(init); -state.moveQueue.push(initShapes); -state.moveQueue.push(fetch); -state.moveQueue.push(addTetro); -state.moveQueue.push(plan); - -console.log("hola"); - -timer();