PageSourceSearch

https://htwins.net/scrabbletetris/

html htwins.net collected 2026-09-24 09:00:09 UTC 47,499 bytes, 1,185 lines download raw bytes

1<!DOCTYPE html>
2<html>
3	<head>
4		<title>Scrabble Tetris</title>
5		<style>
6			body {
7				position: relative;
8				-webkit-touch-callout: none;
9				-webkit-user-select: none;
10				-khtml-user-select: none;
11				-moz-user-select: none;
12				-ms-user-select: none;
13				user-select: none;
14			}
15			#myCanvas {
16				position: relative;
17				left: 0;
18				top: 0;
19			}
20		</style>
21	</head>
22	<body>
23        <canvas id="myCanvas" width="1600" height="900"></canvas>
24		
24<script language="javascript" type="text/javascript">
25		    var tetrominoes = [
26            [[0,0]],
27            [[0,0],[0,1]],
28            [[0,0],[1,0],[0,1]],
29            [[0,0],[1,0],[-1,0]],
30            [[0,0],[0,1],[1,0],[1,1]],
31            [[0,0],[1,0],[2,0],[-1,0]],
32            [[0,0],[1,0],[-1,0],[0,1]],
33            [[0,0],[1,0],[-1,0],[1,1]],
34            [[0,0],[1,0],[-1,0],[1,-1]],
35            [[0,0],[1,0],[0,1],[-1,1]],
36            [[0,0],[-1,0],[0,1],[1,1]]];
37            
38            var audio_pack_len = 5;
39            var audio_filenames = ["break0","break1","break2","side","down0","down1","down2","down3","land","rotate","send","menu","hold","gameover"];
40            let audio = new Map();
41            for(var i = 0; i < audio_filenames.length; i++){
42                var stri = audio_filenames[i];
43                var filepath = stri+'.wav';
44                var newAudio = new Audio(filepath);
45                var na_pack = [0,newAudio];
46                na_pack[1].load();
47                audio.set(stri, na_pack);
48            }
49            var ECHO_DELAY = 50;
50            var ECHO_TIME = 1000;
51            function getRandomLetter(){
52                var choice = Math.random();
53                for(var i = 0; i < ALPHA_N; i++){
54                    if(freqs[i] > choice){
55                        return i+1;
56                    }
57                }
58            }
59            function playAudio(stri){
60                var pack = audio.get(stri);
61                pack[pack[0]+1].play();
62                pack[0] = (pack[0]+1)%audio_pack_len;
63                while(pack.length < pack[0]+2){
64                    pack.push(pack[1].cloneNode(true));
65                    pack[pack.length-1].load();
66                }
67            }
68            class Button{
69                constructor(index,x,y,w,h,text,op,op_type){
70                    this.index = index;
71                    this.x = x;
72                    this.y = y;
73                    this.w = w;
74                    this.h = h;
75                    this.col = "#FFCC00";
76                    this.text = text;
77                    this.op = -1;
78                    if(op != undefined){
79                        this.op = op;
80                        this.op_type = op_type;
81                    }
82                }
83                drawButton(){
84                    ctx.fillStyle = this.col;
85                    if(this.op >= 0){
86                        if(options[this.op] != this.op_type){
87                            ctx.fillStyle = grayedColor;
88                        }
89                    }else if(this.index < 8){
90                        var g = Math.floor(this.index/2)-2;
91                        var dire = (this.index%2 == 1);
92                        if((dire == 0 && heights[g] <= BOARD_MIN_H) ||
93                        (dire == 1 && heights[g] >= BOARD_MAX_H)){
94                            ctx.fillStyle = grayedColor;
95                        }
96                    }else if(this.index == 8){
97                        if(dicty == null){
98                            ctx.fillStyle = grayedColor;
99                        }
100                    }
101                    ctx.fillRect(this.x,this.y,this.w,this.h);
102                    var txt = this.text;
103                    if(this.text === "Pause"){
104                        centerText(ctx,"(ESC)",this.x+this.w/2,this.y+77,"#000000",32);
105                        if(paused){
106                            txt = "Resume";
107                        }
108                    }
109                    centerText(ctx,txt,this.x+this.w/2,this.y+45,"#000000",32);
110                }
111                testClick(mX,mY){
112                    return (mX >= this.x && mX < this.x+this.w && mY >= this.y && mY < this.y+this.h);
113                }
114            }
115		    class Piece{
116		        constructor(getBomb){
117		            this.pieceType = r(tetrominoes.length);
118		            if(getBomb){
119		                this.pieceType = 0;
120		            }
121		            var len = tetrominoes[this.pieceType].length;
122		            this.info = new Array(len);
123		            this.x = -1;
124		            this.y = -1;
125		            for(var t = 0; t < len; t++){
126		                var x = tetrominoes[this.pieceType][t][0];
127		                var y = tetrominoes[this.pieceType][t][1];
128		                var letter = getRandomLetter();
129		                if(getBomb){
130		                    letter = BOMB_N;
131		                }
132		                this.info[t] = [x,y,letter];
133		            }
134		            this.preRotateInfo = JSON.parse(JSON.stringify(this.info));
135		        }   
136		        placeOnBoard(){
137		            this.x = 5;
138		            var minY = 0;
139		            for(var t = 0; t < this.info.length; t++){
140                      var thisY = this.info[t][1];
141                      if(thisY < minY){
142                          minY = thisY;
143                      }
144		            }
145		            this.y = -minY;
146		        }
147		    }
148		    class Game{
149		      constructor(index,grid_W,grid_H) {
150		        this.index = index;
151		        this.alive = true;
152                this.board = newArray(grid_W,grid_H,0);
153                this.queue = new Array(QUEUE_LENGTH);
154                this.drop = null;
155                this.queueDropTime = -1;
156                this.disappearDropTime = -1;
157                this.vibrationTime = -1;
158                this.score = 0;
159                this.recentWords = [];
160                this.downPress = false;
161                this.hold = null;
162                this.alreadyHeld = false;
163                this.clearArrays();
164                this.winner = true;
165                this.lands = 0;
166                this.echo = ["",0,true];
167                this.finalSpeed = 0;
168                this.startTime = Date.now();
169                this.endTime = null;
170                for(var q = 0; q < QUEUE_LENGTH; q++){
171                    this.queue[q] = new Piece();
172                }
173                this.pullFromQueue();
174              }
175              shiftTimers(dur){
176                this.queueDropTime += dur;
177                this.disappearDropTime += dur;
178                this.vibrationTime += dur;
179                this.startTime += dur;
180                if(this.endTime != null){
181                    this.endTime += dur;
182                }
183              }
184              pullFromQueue(getBomb){
185                this.drop = this.queue.shift();
186                this.drop.placeOnBoard();
187                this.queue[QUEUE_LENGTH-1] = new Piece(getBomb);
188                this.queueDropTime = Date.now();
189              }
190              dropPiece(){
191                if(this.hittingGround()){
192                    var isBomb = (this.drop.info[0][2] == BOMB_N);
193                    this.imprintPiece();
194                    this.disp = newArrayShape(this.board,0);
195                    this.recentWords = [];
196                    if(isBomb){
197                        this.explode();
198                        this.makePiecesFall();
199                        this.pullFromQueue(false);
200                    }else{
201                        var wordsNow = this.scanBoard(true);
202                        var shouldGetBomb = this.shouldGetBombFrom(wordsNow);
203                        this.pullFromQueue(shouldGetBomb);
204                    }
205                    this.alreadyHeld = false;
206                    this.checkForAlive();
207                    return false;
208                }
209                this.drop.y += 1;
210                var v = 0;
211                if(games.length == 2){
212                    v = this.index+(this.lands%2)*2;
213                }else{
214                    v = this.drop.y%4;
215                }
216                playAudio("down"+v);
217                return true;
218              }
219              checkForAlive(){
220                for(var x = 0; x < this.board.length; x++){
221                    if(this.board[x][0] >= 1){
222                        this.finalSpeed = this.getSpeed();
223                        this.endTime = Date.now();
224                        this.alive = false;
225                        playAudio("gameover");
226                        if(games.length == 2){
227                            var other = games[1-this.index];
228                            if(other.alive){
229                                this.winner = false;
230                            }
231                        }
232                    }
233                }
234              }
235              clearArrays(){
236                this.settled = true;
237                this.toRemove = newArrayShape(this.board,false);
238                this.justVanished = newArrayShape(this.board,0);
239                this.disp = newArrayShape(this.board,0);
240              }
241              scanBoard(asLand){
242                this.clearArrays();
243                var wordsNow = this.checkForWords();
244                this.makePiecesFall();
245                this.checkForOpponentWords(wordsNow);
246                if(wordsNow.length >= 1){ // active
247                    playAudio("break"+Math.floor(Math.random()*3));
248                    var longest = this.getLongestWord(wordsNow,0);
249                    this.echo = [longest[0],Date.now(),false];
250                    this.recentWords = wordsNow;
251                }else if(asLand){
252                    playAudio("land");
253                    this.lands += 1;
254                }
255                return wordsNow;
256              }
257              shouldGetBombFrom(wordsNow){
258                  if(options[1] == 1){
259                      return false;
260                  }else{
261                      for(var w = 0; w < wordsNow.length; w++){
262                          if(wordsNow[w][0].length >= 4){
263                              return true;
264                          }
265                      }
266                      return false;
267                  }
268              }
269              getLongestWord(wordsNow, minLength){
270                  var recordHolder = null;
271                  for(var w = 0; w < wordsNow.length; w++){
272                      var rw = wordsNow[w];
273                      if(rw[0].length >= minLength && (recordHolder == null || rw[0].length >= recordHolder[0].length)){
274                          recordHolder = rw;
275                      }
276                  }
277                  return recordHolder;
278              }
279              checkForOpponentWords(wordsNow){
280                  if(games.length >= 2){
281                      var sendLength = 5;
282                      var longest = this.getLongestWord(wordsNow,sendLength);
283                      if(longest != null){
284                          this.sendToOpponent(longest);
285                      }
286                  }
287              }
288              sendToOpponent(rw){
289                  var stri = rw[0];
290                  var oGame = games[1-this.index];
291                  var choiceX = rw[1];
292                  if(rw[2] == 1){ // it's vertical.
293                    choiceX -= Math.floor(stri.length/2);
294                    if(stri.length%2 == 0){ // it's an even-length word, so you can be on the left or right sides
295                        if(Math.random() > 0.5){
296                            choiceX += 1;
297                        }
298                    }
299                    choiceX = Math.min(Math.max(choiceX,0),oGame.board.length-stri.length);
300                  }
301                  var bottomY = oGame.board[0].length-1;
302                  for(var x = choiceX; x < choiceX+stri.length; x++){
303                      for(var y = 0; y < bottomY; y++){
304                          oGame.board[x][y] = oGame.board[x][y+1];
305                      }
306                      oGame.board[x][bottomY] = stri.charCodeAt(x-choiceX)-64+ALPHA_N;
307                  }
308                  oGame.vibrationTime = Date.now();
309                  oGame.settled = false;
310                  oGame.disappearDropTime = Date.now();
311                  playAudio("send");
312              }
313              explode(){
314                  this.clearArrays();
315                  var x = this.drop.x;
316                  var y = this.drop.y;
317                  var x_start = Math.max(x-1,0);
318                  var x_end = Math.min(x+1,this.board.length-1);
319                  var y_start = Math.max(y-1,0);
320                  var y_end = Math.min(y+1,this.board[0].length-1);
321                  for(var ex = x_start; ex <= x_end; ex++){
322                      for(var ey = y_start; ey <= y_end; ey++){
323                        this.toRemove[ex][ey] = true;
324                        this.justVanished[ex][ey] = -this.board[ex][ey];
325                        if(this.board[ex][ey] >= 1){
326                            this.score += 1;
327                        }
328                      }
329                  }
330                  playAudio("break"+Math.floor(Math.random()*3));
331                  this.recentWords = [["!!!",x,0]];
332              }
333              imprintPiece(){
334                  for(var t = 0; t < this.drop.info.length; t++){
335                      var at_me_x = this.drop.x+this.drop.info[t][0];
336                      var at_me_y = this.drop.y+this.drop.info[t][1];
337                      var tile = this.drop.info[t][2];
338                      if(this.isInBounds(at_me_x,at_me_y)){
339                        this.board[at_me_x][at_me_y] = tile;
340                      }
341                  }
342              }
343              isInBounds(x,y){
344                  return (x >= 0 && x < this.board.length && y >= 0 && y < this.board[0].length);
345              }
346              isFilled(x,y){
347                  if(!this.isInBounds(x,y)){
348                      return true;
349                  }else if(this.board[x][y] >= 1){
350                      return true;
351                  }
352                  return false;
353              }
354              hittingGround(){
355	            for(var t = 0; t < this.drop.info.length; t++){
356	                var newX = this.drop.x+this.drop.info[t][0];
357	                var newY = this.drop.y+this.drop.info[t][1]+1;
358	                if(this.isFilled(newX,newY)){
359	                    return true;
360	                }
361	            }
362	            return false;
363	          }
364	          moveSide(sign){
365	            for(var t = 0; t < this.drop.info.length; t++){
366                  var beside_x = this.drop.x+this.drop.info[t][0]+sign;
367                  var beside_y = this.drop.y+this.drop.info[t][1];
368                  if(this.isFilled(beside_x,beside_y)){
369	                return true;
370	              }
371                }
372                playAudio("side");
373	            this.drop.x += sign;
374	          }
375	          instaDrop(){
376	              while(this.dropPiece()){
377	              }
378	          }
379	          rotate(sign){
380	              var copy = JSON.parse(JSON.stringify(this.drop.info));
381	              var minX = 9999;
382	              var minY = 9999;
383	              for(var t = 0; t < copy.length; t++){
384	                  var ph = copy[t][0];
385	                  copy[t][0] = -sign*copy[t][1];
386	                  copy[t][1] = sign*ph;
387	                  minX = Math.min(minX,copy[t][0]);
388	                  minY = Math.min(minY,copy[t][1]);
389	              }
390	              if(this.drop.pieceType == SQUARE_N || this.drop.pieceType == L_N){
391	                  for(var t = 0; t < copy.length; t++){
392	                      copy[t][0] -= minX;
393	                      copy[t][1] -= minY;
394	                  }
395	              }
396	              var x_shift = this.getFittingXshift(copy);
397	              if(x_shift != null){
398	                this.drop.x += x_shift;
399	                playAudio("rotate");
400	                this.drop.info = copy;
401	              }
402	          }
403	          
404	          getFittingXshift(copy){
405	              var x_shift_options = [0,1,-1];
406	              if(this.drop.pieceType == LINE_N){
407	                  x_shift_options = [0,1,-1,2,-2];
408	              }
409	              var x_shift_choice = null;
410	              for(var s = 0; s < x_shift_options.length; s++){
411	                  var dx = x_shift_options[s];
412	                  var valid = true;
413    	              for(var t = 0; t < copy.length; t++){
414    	                  var newX = this.drop.x+copy[t][0]+dx;
415    	                  var newY = this.drop.y+copy[t][1];
416    	                  if(this.isFilled(newX,newY)){
417    	                      valid = false;
418    	                  }
419    	              }
420    	              if(valid){
421    	                  return dx;
422    	              }
423	              }
424	              return null;
425	          }
426	          
427	          checkForWord(x,y,LEN,dim,wordsNow){
428	              var word = "";
429                  var valid = true;
430                  var margin = options[1] ? 1 : 0;
431                  var containsFresh = false;
432                  for(var le = 0; le < LEN; le++){
433                      var dx = (1-dim)*le;
434	                  var dy = (dim)*le;
435                      if(!this.isInBounds(x+dx,y+dy)){
436                          return;
437                      }
438                      var tile = this.board[x+dx][y+dy];
439                      
440                      if(tile == 0){
441                          valid = false;
442                          continue;
443                      }
444                      word += numToLet(tile);
445                      if(tile <= ALPHA_N){
446                          containsFresh = true;
447                      }
448                  }
449                  if(valid && dicty[word.length].has(word) && containsFresh){
450                      for(var perp = -margin; perp <= margin; perp++){
451                          for(var le = -margin; le < LEN+margin; le++){
452                              var dx = (1-dim)*le+(dim)*perp;
453    	                      var dy = (dim)*le+(1-dim)*perp;
454    	                      if(this.isInBounds(x+dx,y+dy)){
455    	                          if(this.board[x+dx][y+dy] >= 1){
456                                      var sign = (perp == 0 && le >= 0 && le < LEN) ? 1 : -1;
457                                      if(sign == 1 || this.justVanished[x+dx][y+dy] == 0){
458                                        this.justVanished[x+dx][y+dy] = sign*this.board[x+dx][y+dy];
459                                      }
460                                      if(!this.toRemove[x+dx][y+dy]){
461            	                          this.score += 1;
462        	                          }
463    	                          }
464    	                          this.toRemove[x+dx][y+dy] = true;
465    	                      }
466                          }
467                      }
468                      wordsNow.push([word,x,dim]);
469                  }
470	          }
471	          checkForWords(){
472	              var wordsNow = [];
473	              var SHORTEST = options[1] ? 4 : 3;
474	              for(var len = SHORTEST; len <= MAX_WORD_LENGTH; len++){
475                      for(var x = 0; x <= this.board.length; x++){
476                          for(var y = 0; y < this.board[0].length; y++){
477                              for(var dim = 0; dim < 2; dim++){
478                                this.checkForWord(x,y,len,dim,wordsNow);
479                              }
480                          }
481    	              }
482	              }
483	              return wordsNow;
484	          }
485	          makePiecesFall(){
486	              for(var x = 0; x < this.board.length; x++){
487                      for(var y = 0; y < this.board[0].length; y++){
488                          if(this.toRemove[x][y]){
489                              this.settled = false;
490                              for(var fy = y; fy >= 1; fy--){
491                                  this.board[x][fy] = this.board[x][fy-1];
492                                  this.disp[x][fy] = this.disp[x][fy-1];
493                                  this.disp[x][fy] -= 1;
494                              }
495                              this.board[x][0] = 0;
496                              this.disappearDropTime = Date.now();
497                              this.vibrationTime = Date.now();
498                          }
499                      }
500	              }
501	          }
502	          pressDown(){
503	              if(!this.downPress){
504	                  this.downPress = true;
505	                  var now = Date.now();
506    	              if(this.queueDropTime < now-this.getTimeToDrop()){
507    	                  this.queueDropTime = now-this.getTimeToDrop();
508    	              }
509	              }
510	          }
511	          iterate(){
512	            if(this.alive){
513    	            var now = Date.now();
514                    while(now >= this.queueDropTime+this.getTimeToDrop()){
515                        this.queueDropTime += this.getTimeToDrop();
516                        this.dropPiece();
517                    }
518                    if(!this.settled && now >= this.disappearDropTime+this.getTimeToSpirit()){
519                        this.settled = (this.scanBoard(false).length == 0);
520                    }
521                    if(!this.echo[2] && Date.now() >= this.echo[1]+ECHO_DELAY){
522                        speak(this.echo[0],1.0);
523                        this.echo[2] = true;
524                    }
525	            }
526	          }
527	          getShake(){
528	              var timePassed = getNow()-this.vibrationTime;
529			      var shake = 12*Math.pow(0.005,timePassed/1000);
530			      if(shake < 0.1){
531			          return 0;
532			      }
533			      var choice = (frand(timePassed,0))*2-1;
534	              return choice*shake;
535	          }
536	          getTimeToDrop(){
537	              var val = 1000/this.getSpeed();
538	              if(this.downPress){
539	                  val /= 8;
540	              }
541	              return val;
542	          }
543	          listRecentWords(){
544	              var result = "";
545	              for(var w = 0; w < this.recentWords.length; w++){
546                      var rw = this.recentWords[w][0];
547                      if(w >= 1){
548                          result += ", ";
549                      }
550                      result += rw;
551	              }
552	              return result;
553	          }
554	          holdPiece(){
555	              if(!this.alreadyHeld){
556    	              if(this.hold == null){
557    	                  this.hold = this.drop;
558    	                  this.hold.info = this.hold.preRotateInfo;
559    	                  this.pullFromQueue();
560    	              }else{
561    	                  var ph = this.drop;
562    	                  this.drop = this.hold;
563    	                  this.hold = ph;
564    	                  this.hold.info = this.hold.preRotateInfo;
565    	                  this.drop.placeOnBoard();
566    	              }
567    	              this.alreadyHeld = true;
568    	              playAudio("hold");
569    	              this.queueDropTime = Date.now();
570	              }
571	          }
572	          getSpeed(){
573	            if(!this.alive){
574	                return this.finalSpeed;
575	            }
576	            if(!options[2]){
577	                return 1.0;
578	            }
579	            if(games.length == 1){
580		            return 1+this.score/100;
581	            }else{
582	                var other = games[1-this.index];
583	                if(!other.alive){
584	                    return 1+this.score/100;
585	                }
586	                return 1+other.score/100;
587	            }
588		      }
589		      getTimeToSpirit(){
590                return 2000/this.getSpeed();
591              }
592              getExtremeDPY(sign){
593                  var record = -sign*9999;
594                  for(var t = 0; t < this.drop.info.length; t++){
595                      var at_me_y = this.drop.y+this.drop.info[t][1];
596                      if(sign*at_me_y > sign*record){
597                          record = at_me_y;
598                      }
599                  }
600                  return record;
601              }
602		    }
603		    function newArrayShape(example, value){
604		        var W = example.length;
605		        var H = example[0].length;
606		        var arr = new Array(W);
607                for(var x = 0; x < W; x++){
608                    arr[x] = new Array(H);
609                    for(var y = 0; y < H; y++){
610                        arr[x][y] = value;
611                    }
612                }
613                return arr;
614		    }
615		    function newArray(W,H,value){
616		        var arr = new Array(W);
617                for(var x = 0; x < W; x++){
618                    arr[x] = new Array(H);
619                    for(var y = 0; y < H; y++){
620                        arr[x][y] = value;
621                    }
622                }
623                return arr;
624		    }
625		    function hue_to_RGB(hue,green_suppress){
626		        var hp = (hue*6)%1.0;
627		        if(hue < 1/6){
628		            return [255,hp*255*green_suppress,0];
629		        }else if(hue < 2/6){
630		            return [255-hp*255,255*green_suppress,0];
631		        }else if(hue < 3/6){
632		            return [0,255*green_suppress,hp*255];
633		        }else if(hue < 4/6){
634		            return [0,(255-hp*255)*green_suppress,255];
635		        }else if(hue < 5/6){
636		            return [hp*255,0,255];
637		        }else{
638		            return [255,0,255-hp*255];
639		        }
640		    }
641		    loadFreqs();
642		    loadWords();
643			var c=document.getElementById("myCanvas");
644			c.onmousedown = onClick;
645            var ctx = c.getContext("2d");
646            var cW = myCanvas.width;
647            var cH = myCanvas.height;
648            var BOARD_W = 10;
649            var BOARD_MIN_H = 2;
650            var BOARD_MAX_H = 20;
651            var ALPHA_N = 26;
652            var TILE_S = 40;
653            var QUEUE_LENGTH = 5;
654            var BOMB_N = ALPHA_N*2+1;
655            var MAX_WORD_LENGTH = 10;
656            var dicty = null;
657            
658            var bgColor = "#000000";
659            var borderColor = "#808080";
660            var dropColor = "#FFFFFF";
661            var spiritColor = "#00FFFF";
662            var textColor = "#000000";
663            var statTextColor = "#FFFFFF";
664            var grayedColor = "#808080";
665            var explosionColor = "#FFFF00";
666            var holdColors = ["#00FF00","#008000"];
667            var games = null;
668            var freqs = null;
669            var wordList = null;
670            var menu = true;
671            var SQUARE_N = 4;
672            var L_N = 2;
673            var LINE_N = 5;
674            var heights = [BOARD_MAX_H,BOARD_MAX_H];
675            var paused = false;
676            var pauseTime = -1;
677            
678            var options = [false,true,true];
679            var buttons = [new Button(0,30,200,400,70,"1 player",0,false),
680            new Button(1,460,200,400,70,"2 players",0,true),
681            new Button(2,30,670,400,70,"Boring-3 Mode",1,false),
682            new Button(3,460,670,400,70,"Explosive-4 Mode",1,true),
683            new Button(4,30,300,70,70,"<"),
684            new Button(5,360,300,70,70,">"),
685            new Button(6,460,300,70,70,"<"),
686            new Button(7,790,300,70,70,">"),
687            new Button(8,30,770,830,70,"Go"),
688            new Button(9,940,200,300,70,"Zen",2,false),
689            new Button(10,1270,200,300,70,"Speeds Up",2,true)];
690            
691            var SYNTH_COUNT = 10;
692            var synth = window.speechSynthesis;
693            
694            var controlsImages = [new Image, new Image];
695            for(var c = 0; c < 2; c++){
696                controlsImages[c].src = 'controls'+c+'.png';
697            }
698            
699            speak("Welcome to Scrabble Tetris! Play it immediately or else.",1.0);
700            
701            var gameButtons = [new Button(9,cW/2-140,cH-112,130,102,"Pause"),
702                new Button(10,cW-140,cH-80,130,70,"Menu")];
703            
704            async function loadFreqs() {
705              const response = await fetch("freq.tsv");
706              const freqText = await response.text();
707              freqs = createFreqTable(freqText);
708            }
709            async function loadWords() {
710              const response = await fetch("dictionary.txt");
711              const dictText = await response.text();
712              dicty = createDicty(dictText);
713            }
714            function getEnglishVoices(){
715                var result = [];
716                var list = synth.getVoices();
717                for(var v = 0; v < list.length; v++){
718                    var name = list[v].name
719                    if(name.includes("English") || name.includes("Welsh") || name.includes("Irish")){
720                        result.push(list[v]);
721                    }
722                }
723                return result;
724            }
725            function speak(stri, vol){
726                var voices = getEnglishVoices();
727                var utterance = new SpeechSynthesisUtterance(stri);
728                utterance.voice = voices[r(voices.length)];
729                utterance.volume = vol;
730                synth.speak(utterance);
731            }
732            
733
734            
735            function createFreqTable(freq_file){
736                var freq_lines = freq_file.split("\n");
737                var freq = new Array(ALPHA_N);
738                var total = freq_lines[0].split("\t")[1];
739                var summer = 0;
740                for(var a = 0; a < ALPHA_N; a++){
741                    var val = Number(freq_lines[1+a].split("\t")[1]);
742                    summer += val;
743                    freq[a] = summer/total;
744                }
745                return freq;
746            }
747            function createDicty(dict_text){
748                var result = new Array(MAX_WORD_LENGTH+1);
749                for(var len = 0; len < MAX_WORD_LENGTH+1; len++){
750                    result[len] = new Set();
751                }
752                var lines = dict_text.split("\n");
753                for(var i = 0; i < lines.length; i++){
754                    var len = lines[i].length;
755                    if(len <= MAX_WORD_LENGTH){
756                        result[len].add(lines[i]);
757                    }
758                }
759                return result;
760            }
761            
762            self.setInterval(function(){run()},1000/60);
763			function run(){
764			    if(menu){
765			        drawMenu();
766			    }else{
767			        runGame();
768                    drawScreen();
769			    }
770            }
771            function multiLinePrint(ctx, stris, x, y, col, size){
772                ctx.fillStyle = col;
773                ctx.font = size+"px Helvetica";
774                for(var s = 0; s < stris.length; s++){
775                    ctx.fillText(stris[s],x,y+s*size);
776                }
777            }
778            function drawMenu(){
779                ctx.fillStyle = bgColor;
780				ctx.fillRect(0,0,cW,cH);
781				ctx.fillStyle = statTextColor;
782				ctx.font = "96px Helvetica";
783                ctx.fillText("SCRABBLE TETRIS",30,110);
784                drawButtons();
785                var stri = [["In Boring-3 Mode: 3+ letter words will clear","4+ letter words will gift you an explosive (!!!)","5+ letter words will be sent to the opponent."],
786                ["In Explosive-4 Mode:","4+ letter words will clear and explode","5+ letter words will be sent to the opponent."]];
787                multiLinePrint(ctx,stri[options[1]?1:0],880,677,statTextColor,24);
788
789                for(var g = 0; g < options[0]+1; g++){
790                    ctx.drawImage(controlsImages[g], 55+430*g, 380, 350, 250);
791                }
792                
793                stri = ["When you send an opponent a 5-letter word,","they will receive it in gray tiles.","A word will only \"register\"","if at least one tile in the word is not gray."];
794                multiLinePrint(ctx,stri,880,787,statTextColor,24);
795            }
796            function drawButtons(){
797                for(var b = 0; b < buttons.length; b++){
798                    if(!(b >= 6 && b < 8) || options[0] == 1){
799                        var bu = buttons[b];
800                        bu.drawButton();
801                    }
802                }
803                for(var g = 0; g < options[0]+1; g++){
804                    var stri = "Board height: "+heights[g];
805                    centerText(ctx,stri,230+g*430,345,"#FFFFFF",32);
806                }
807            }
808            function runGame(){
809                if(games != null && !paused){
810                    for(var g = 0; g < games.length; g++){
811                        games[g].iterate();
812                    }
813                }
814            }
815            function drawScreen(){
816                ctx.fillStyle = bgColor;
817				ctx.fillRect(0,0,cW,cH);
818				if(games != null){
819			    	for(var g = 0; g < games.length; g++){
820    				    drawPlayer(games[g],200+800*g,14);
821    				}
822				}
823				for(var b = 0; b < gameButtons.length; b++){
824                    var bu = gameButtons[b];
825                    bu.drawButton();
826                }
827                if(paused){
828                    centerText(ctx,"GAME PAUSED",cW/2,cH/2,"#FFFFFF",96);
829                }
830			}
831			function drawPlayer(game,x,y){
832			    var shakeX = game.getShake();
833			    var shakeY = game.getShake();
834			    drawGrid(game,x+shakeX,y+shakeY,TILE_S);
835			    drawQueue(game.queue,x+480,y+TILE_S*0.5,TILE_S,grayedColor);
836			    drawHold(game,x-90,y);
837			    drawStats(game,x,y+TILE_S*game.board[0].length);
838			    drawGameOver(game,x,y);
839			}
840			function drawHold(game,x,y){
841			    if(game.hold != null){
842			        var col = holdColors[game.alreadyHeld ? 1 : 0];
843			        drawQueue([game.hold],x-TILE_S*0.5,y+TILE_S*1.5,TILE_S,col);
844			        centerText(ctx,"HOLD",x,y+TILE_S*0.5,col,32);
845			    }
846			}
847			function drawGameOver(game,x,y){
848			    if(!game.alive){
849    			    ctx.fillStyle = statTextColor;
850                    ctx.font = "96px Helvetica";
851                    ctx.fillText("GAME",x+40,y+200);
852                    ctx.fillText("OVER",x+40,y+300);
853                    if(games.length == 2){
854                        var subtext = (game.winner ? "WIN!" : "LOSE.");
855                        ctx.fillText("YOU",x+40,y+500);
856                        ctx.fillText(subtext,x+40,y+600);
857                    }
858			    }
859			}
860			function drawStats(game,x,y){
861			    ctx.fillStyle = statTextColor;
862                ctx.font = "32px Helvetica";
863                ctx.fillText(game.listRecentWords(),x,y+72);
864                var stri = (options[1] == 1) ? "Score: " : "Bore: ";
865                stri = ((options[2] == 0) ? "Zen " : "") +stri;
866                ctx.fillText(stri+game.score,x,y+40);
867                var sp = game.getSpeed().toFixed(2);
868                ctx.fillText(sp+" dps",x+240,y+40);
869                
870                var now = paused ? pauseTime : Date.now();
871                var gameTime = (game.alive ? now : game.endTime) - game.startTime;
872                ctx.fillText(timify(gameTime),x-145,y+40);
873			}
874			function timify(val){
875		        var mins = ""+Math.floor(val/60000);
876		        var ten_sec = ""+Math.floor(val/10000)%6;
877		        var sec = ""+Math.floor(val/1000)%10;
878		        var sec_tenth = ""+Math.floor(val/100)%10;
879		        return mins+":"+ten_sec+sec+"."+sec_tenth;
880			}
881			function drawQueue(queue, grid_X, grid_Y, S, queueColor){
882			    for(var q = 0; q < queue.length; q++){
883			        var py = grid_Y+q*S*3;
884                    var piece = queue[q];
885                    var extremes = [999,-999,999,-990];
886                    for(var t = 0; t < piece.info.length; t++){
887    			        var tx = piece.info[t][0];
888    			        var ty = piece.info[t][1];
889    			        extremes[0] = Math.min(extremes[0],tx);
890    			        extremes[1] = Math.max(extremes[1],tx);
891    			        extremes[2] = Math.min(extremes[2],ty);
892    			        extremes[3] = Math.max(extremes[3],ty);
893    			    }
894    			    var midX = (extremes[0]+extremes[1])/2;
895    			    var midY = (extremes[2]+extremes[3])/2;
896    			    var mar = 0.2;
897    			    var rx = grid_X+(extremes[0]-midX-mar)*S;
898    			    var ry = py+(extremes[2]-midY-mar)*S;
899    			    var rw = (extremes[1]-extremes[0]+1+mar*2)*S;
900    			    var rh = (extremes[3]-extremes[2]+1+mar*2)*S;
901    			    var rr = mar*S;
902    			    ctx.fillStyle = queueColor;
903    			    ctx.fillRect(rx,ry,rw,rh,rr);
904    			    
905    			    for(var t = 0; t < piece.info.length; t++){
906    			        var ax = piece.info[t][0]-midX;
907    			        var ay = piece.info[t][1]-midY;
908    			        var tl = piece.info[t][2];
909    			        drawTile(grid_X+ax*S,py+ay*S,0,tl,S,1,0);
910    			    }
911                }
912			}
913			function getNow(){
914			    if(paused){
915			        return pauseTime;
916			    }else{
917			        return Date.now();
918			    }
919			}
920			function drawGrid(game, grid_X, grid_Y, S){
921			    var board = game.board;
922			    var drop = game.drop;
923			    var timePassed = getNow()-game.disappearDropTime;
924			    var fac = Math.min(Math.max(timePassed/game.getTimeToSpirit(),0),1);
925			    var decay = 0.5+0.5*Math.cos(fac*Math.PI);
926			    
927			    for(var x = 0; x < board.length; x++){
928			        for(var y = 0; y < board[x].length; y++){
929			            var tile = board[x][y];
930			            var ax = grid_X+S*x;
931			            var ay = grid_Y+S*y;
932			            drawTile(ax,ay,game.disp[x][y]*S*decay,tile,S,1,0);
933			        }
934			    }
935			    if(decay >= 0.00001){
936    			    for(var x = 0; x < board.length; x++){
937    			        for(var y = 0; y < board[x].length; y++){
938    			            var spirit = game.justVanished[x][y];
939    			            var ax = grid_X+S*x;
940        			        var ay = grid_Y+S*y;
941    			            if(spirit >= 1){
942    			                drawTile(ax,ay,0,spirit,S,decay,2);
943    			            }else if(spirit <= -1){
944    			                drawExplosion(ax,ay,S,decay,timePassed);
945    			            }
946    			        }
947    			    }
948			    }
949			    if(game.alive){
950			        var fastTimePassed = getNow()-game.queueDropTime;
951			        var fastDecay = Math.pow(0.03,fastTimePassed/100);
952        			for(var t = 0; t < drop.info.length; t++){
953    			        var di = drop.info[t];
954    			        var x = drop.x+di[0];
955    			        var y = drop.y+di[1];
956    			        var tile = di[2];
957    			        var ax = grid_X+S*x;
958    			        var ay = grid_Y+S*y;
959    			        drawTile(ax,ay,-fastDecay*S,tile,S,1,1);
960    			    }
961    			    var timeSince = Date.now()-game.echo[1];
962    			    if(timeSince <= ECHO_TIME){
963    			        var BW = game.board.length;
964    			        var BH = game.board[0].length;
965    			        var size = 0.5-0.5*Math.cos(timeSince/ECHO_TIME*2*Math.PI);
966    			        var highest_dpy = game.getExtremeDPY(1)-fastDecay;
967    			        var lowest_dpy = game.getExtremeDPY(-1)-fastDecay;
968    			        var mid_dpy = (highest_dpy+lowest_dpy)/2;
969    			        
970    			        var textY = BH*0.3333;
971    			        if(textY >= lowest_dpy-2 && textY <= highest_dpy+2){
972    			            if(textY > mid_dpy){
973    			                textY = highest_dpy+2;
974    			            }else{
975    			                textY = lowest_dpy-2;
976    			            }
977    			        }
978    			        centerText(ctx,game.echo[0],grid_X+S*BW*0.5,grid_Y+S*textY+size*65,"#FFCC00",size*130);
979                    }
980			    }
981			}
982			function getTileColor(tile){
983			    if(tile == BOMB_N){
984			        return rgbToHex([128,128,128]);
985			    }else if(tile >= ALPHA_N){
986			        return grayedColor;
987			    }else{
988			        return rgbToHex(hue_to_RGB((tile-1)/26,0.65));
989			    }
990			}
991			function componentToHex(c) {
992              var hex = c.toString(16);
993              return hex.length == 1 ? "0" + hex : hex;
994            }
995			function rgbToHex(arr) {
996			    var result = "#";
997			    for(var i = 0; i < 3; i++){
998			        result += componentToHex(Math.floor(arr[i]));
999			    }
1000                return result;
1001            }
1002            function frand(a, b){
1003                var sine = Math.sin(a*100+b*37);
1004                var choice = (((sine+1)*100)%1.0);
1005                return choice;
1006            }
1007            function drawExplosion(ax,ay,S,size,tp){
1008                var R = S*0.5;
1009                var centerX = ax+R;
1010                var centerY = ay+R;
1011                ctx.fillStyle = explosionColor;
1012                ctx.beginPath();
1013                ctx.moveTo(centerX,centerY);
1014                for(var p = 0; p < 20; p++){
1015                    var far_fac = frand(tp,p*4)*0.3+0.7;
1016                    var far_dist = R*size*far_fac;
1017                    var far_angle = frand(tp,p*4+1)*Math.PI*2;
1018                    ctx.lineTo(centerX+Math.cos(far_angle)*far_dist,
1019                    centerY+Math.sin(far_angle)*far_dist);
1020                    
1021                    var close_fac = frand(tp,p*4+2)*0.3;
1022                    var close_dist = R*size*close_fac;
1023                    var close_angle = frand(tp,p*4+3)*Math.PI*2;
1024                    ctx.lineTo(centerX+Math.cos(close_angle)*close_dist,
1025                    centerY+Math.sin(close_angle)*close_dist);
1026                }
1027                ctx.fill();
1028            }
1029            function centerText(ctx,stri,x,y,col,size){
1030                ctx.fillStyle = col;
1031                ctx.font = size+"px Helvetica";
1032                var textWidth = ctx.measureText(stri).width;
1033                var textHeight = ctx.measureText(stri).height;
1034                var x_coor = x-textWidth/2-0.5;
1035                ctx.fillText(stri,x_coor,y);
1036            }
1037			function drawTile(ax, ay, ady, tile, S, size, type){
1038			    var M = 1;
1039			    var M2 = 2;
1040			    if(size == 1 && type != 1){
1041    			    ctx.fillStyle = borderColor;
1042                    ctx.fillRect(ax,ay,S,S);
1043                    ctx.fillStyle = bgColor;
1044                    ctx.fillRect(ax+M,ay+M,S-M*2,S-M*2);
1045			    }
1046                if(tile >= 1){
1047                    var colors = [getTileColor(tile), dropColor, spiritColor];
1048                    var texts = ["#FFFFFF","#000000","#000000"];
1049                    ctx.fillStyle = colors[type];
1050                    var aax = ax+S*0.5+(M2-S*0.5)*size;
1051                    var aay = ay+S+(M2-S)*size+ady;
1052                    if(options[1]){
1053                        aay = ay+S*0.5+(M2-S*0.5)*size+ady;
1054                    }
1055                    var aas = (S-M2*2)*size
1056                    ctx.fillRect(aax,aay,aas,aas);
1057                    ctx.fillStyle = texts[type];
1058                    var letter = numToLet(tile);
1059                    var fs = Math.round(32*size);
1060                    ctx.font = fs+"px Helvetica";
1061                    var atx = ax+S*0.5;
1062                    var aty = ay+S-(S*0.2)*size+ady;
1063                    if(options[1]){
1064                        aty = ay+S*0.5+(S*0.3)*size+ady;
1065                    }
1066                    centerText(ctx,letter,atx,aty,texts[type],fs);
1067                }
1068			    
1069			}
1070			function numToLet(n){
1071			    if(n == BOMB_N){
1072			        return "!!!";
1073			    }
1074			    var n2 = n;
1075			    if(n2 > ALPHA_N){
1076			        n2 -= ALPHA_N;
1077			    }
1078			    return String.fromCharCode(64+n2);
1079			}
1080			function r(n){
1081			    return Math.floor(Math.random()*n);
1082			}
1083			function startGames(){
1084			    if(dicty == null){
1085			        return;
1086			    }
1087			    menu = false;
1088			    paused = false;
1089			    var game_count = options[0] ? 2 : 1;
1090			    games = new Array(0);
1091			    for(var g = 0; g < game_count; g++){
1092			        games.push(new Game(g, BOARD_W, heights[g]));
1093			    }
1094			}
1095			function handleMenuClick(b){
1096			    var bu = buttons[b];
1097			    if(bu.op >= 0){
1098                    options[bu.op] = bu.op_type;
1099			    }else if(b == 8){
1100                    startGames();
1101                }else{
1102                    var g = Math.floor((b-4)/2);
1103                    var dire = b%2;
1104                    if(g <= options[0]){
1105                        var sign = dire*2-1;
1106                        heights[g] = Math.min(Math.max(heights[g]+sign,BOARD_MIN_H),BOARD_MAX_H);
1107                    }
1108                }
1109                playAudio("menu");
1110			}
1111			function pauseGame(){
1112			    paused = !paused;   
1113                if(paused){
1114                    pauseTime = Date.now();
1115                }else{
1116                    var pauseDur = Date.now()-pauseTime;
1117                    for(var g = 0; g < games.length; g++){
1118                        games[g].shiftTimers(pauseDur);
1119                    }
1120                }
1121			}
1122			function onClick(e){
1123			    var mX = e.offsetX;
1124				var mY = e.offsetY;
1125				if(menu){
1126    				for(var b = 0; b < buttons.length; b++){
1127                        var bu = buttons[b];
1128                        if(bu.testClick(mX,mY)){
1129                            handleMenuClick(b);
1130                        }
1131                    }
1132				}else{
1133				    for(var b = 0; b < gameButtons.length; b++){
1134                        var bu = gameButtons[b];
1135                        if(bu.testClick(mX,mY)){
1136                            if(b == 0){
1137                                pauseGame();
1138                            }if(b == 1){
1139                                menu = true;
1140                            }
1141                            playAudio("menu");
1142                        }
1143                    }
1144				}
1145			}
1146			document.onkeydown = function (e) {
1147			    if(!menu){
1148                    var k = e.keyCode;
1149                    if(k >= 97){
1150                        k -= 32;
1151                    }
1152                    var keys = ["ADWESQ","JLIOKU"];
1153                    if(k == 27){
1154                        pauseGame();
1155                    }
1156                    for(var g = 0; g < keys.length; g++){
1157                        var game = (games.length == 2 ? games[g] : games[0]);
1158                        for(var ki = 0; ki < keys[g].length; ki++){
1159                           if(!paused && k == keys[g].charCodeAt(ki) && game.alive){
1160                               if(ki == 0 || ki == 1){
1161                                   game.moveSide(-1+2*ki);
1162                               }else if(ki == 2 || ki == 3){
1163                                   game.rotate(-1+(ki-2)*2);
1164                               }else if(ki == 4){
1165                                   game.pressDown();
1166                               }else if(ki == 5){
1167                                   game.holdPiece();
1168                               }
1169                           } 
1170                        }
1171                    }
1172			    }
1173            };
1174            document.onkeyup = function (e) {
1175                if(!menu){
1176                    for(var g = 0; g < games.length; g++){
1177                        games[g].downPress = false;
1178                    }
1179                }
1180            }
1181			
1182		</script>
1182
1183		<p><button onclick="location.href='https://www.htwins.net'" style="font-size : 20px">Back to htwins.net</button></p>
1184	</body>
1185</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.