Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.lang.javascript > #37802 > unrolled thread
| Started by | luserdroog <luser.droog@gmail.com> |
|---|---|
| First post | 2019-11-16 16:13 -0800 |
| Last post | 2019-11-23 05:47 -0800 |
| Articles | 5 — 2 participants |
Back to article view | Back to comp.lang.javascript
Tetris rough draft (very rough) luserdroog <luser.droog@gmail.com> - 2019-11-16 16:13 -0800
Re: Tetris rough draft (very rough) luserdroog <luser.droog@gmail.com> - 2019-11-16 21:53 -0800
Re: Tetris rough draft (very rough) Julio Di Egidio <julio@diegidio.name> - 2019-11-22 09:07 -0800
Re: Tetris rough draft (very rough) luserdroog <luser.droog@gmail.com> - 2019-11-22 21:47 -0800
Re: Tetris rough draft (very rough) Julio Di Egidio <julio@diegidio.name> - 2019-11-23 05:47 -0800
| From | luserdroog <luser.droog@gmail.com> |
|---|---|
| Date | 2019-11-16 16:13 -0800 |
| Subject | Tetris rough draft (very rough) |
| Message-ID | <19746231-7b69-47b2-b5c3-21d97a7227de@googlegroups.com> |
Many thanks for the suggestions to my questions about
my game loop. I have it a little more fleshed out now.
But there are a whole host of little problems, not least
of which is the sluggish response. I'm also having
difficulty getting the pieces to rotate around their
center points.
I chose to represent a tetromino piece as a 4x4 bitmap
in an integer expressed in hexadecimal. This worked great
on paper, but the logic to manipulate them is getting a
little cumbersome and confusing.
So, here's my mess. Any suggestions for improvement welcome.
<!doctype html>
<html lang=en>
<meta charset="utf-8">
<title>Tetris</title>
<style>
::root {
--main-color: black;
--bg-color: blue;
}
.game {
background-color: var(--bg-color);
display: flex;
align-items: flex-start;
}
.board {
border: 5px solid red;
background-color: var(--bg-color);
flex: 0 0 auto;
}
.preview {
border: 5px solid green;
background-color: var(--bg-color);
flex: 0 0 auto;
}
</style>
<header>Tetris</header>
<article class="game">
<div class="score">Score</div>
<canvas class="board" width="200" height="400"></canvas>
<canvas class="preview" width="100" height="100"></canvas>
</article>
<footer></footer>
<script>
const maxX = 200;
const maxY = 400;
class Tet {
constructor(){
var tet_list = [ 0x33, 0x63, 0x47, 0x74, 0xf, 0x27 ];
this.tet = tet_list[ Math.floor( Math.random() * tet_list.length ) ];
this.w = 9;
this.h = 9;
this.x = 80;
this.y = 10;
return this;
}
get width(){
return {
0x33: 2, 0x63: 3, 0x132: 2, 0xf: 4, 0x1111: 1,
0x47: 3, 0x113: 2, 0x71: 3, 0x322: 2,
0x74: 3, 0x223: 2, 0x17: 3, 0x311: 2,
0x27: 3, 0x131: 2, 0x72: 3, 0x232: 2
}[ this.tet ];
}
get height(){
return {
0x33: 2, 0x63: 2, 0x132: 3, 0xf: 1, 0x1111: 4,
0x47: 2, 0x113: 3, 0x71: 2, 0x322: 3,
0x74: 2, 0x223: 3, 0x17: 2, 0x311: 3,
0x27: 2, 0x131: 3, 0x72: 2, 0x232: 3
}[ this.tet ];
}
rotateLeft(){
this.tet = {
0x33: 0x33, 0x63: 0x132, 0x132: 0x63, 0xf: 0x1111, 0x1111: 0xf,
0x47: 0x113, 0x113: 0x71, 0x71: 0x322, 0x322: 0x47,
0x74: 0x223, 0x223: 0x17, 0x17: 0x311, 0x311: 0x74,
0x27: 0x131, 0x131: 0x72, 0x72: 0x232, 0x232: 0x27
}[ this.tet ];
this.fixRotate();
this.fixX();
return this;
}
rotateRight(){
this.tet = {
0x33: 0x33, 0x63: 0x132, 0x132: 0x63, 0xf: 0x1111, 0x1111: 0xf,
0x47: 0x322, 0x322: 0x71, 0x71: 0x113, 0x113: 0x47,
0x74: 0x311, 0x311: 0x17, 0x17: 0x223, 0x223: 0x74,
0x27: 0x232, 0x232: 0x72, 0x72: 0x131, 0x131: 0x27
}[ this.tet ];
this.fixRotate();
this.fixX();
return this;
}
moveLeft(){
this.x -= 10;
this.fixX();
}
moveRight(){
this.x += 10;
this.fixX();
}
fixRotate(){
if( this.tet == 0x1111 ||
this.tet == 0x232 ){
this.x += 10
}
if( this.tet == 0xf ||
this.tet == 0x131 ){
this.x -= 10
}
if( this.tet == 0x27 ){
this.y += 10
}
}
fixX(){
if( this.x < 0 ){
this.x = 0;
}
if( this.x > maxX - this.width * 10 ){
this.x = maxX - this.width * 10;
}
}
moveDown(){
this.y += 10;
}
draw( c, x, y ){
for( var i = 0; i < 4; ++i ){
for( var j = 0; j < 4; ++j ){
if( this.tet & (1<<(i*4+j)) ){
c.fillRect( x + j*10, y + i*10, this.w, this.h )
}
}
}
}
}
class Game {
constructor( board, preview ){
board.fillStyle = 'black';
preview.fillStyle = 'blue';
this.board = board;
this.preview = preview;
this.pressedKeys = {
left: false,
right: false,
up: false,
down: false,
comma: false,
dot: false
};
this.next = new Tet();
this.next.draw( this.preview, 20, 20 );
this.lines = [];
for( var i = 0; i < maxY; i += 10 ){
this.lines.push( 0 );
}
return this;
}
draw(){
this.board.clearRect( 0, 0,
this.board.canvas.width,
this.board.canvas.height );
const len = this.lines.length;
const width = maxX / 10;
for( var i = 0; i < len; ++i ){
const line = this.lines[i];
if( line ){
for( var j = 0; j < width; ++j ){
if( line & (1 << j) ){
this.board.fillRect( j * 10, i * 10, 9, 9 );
}
}
}
}
if( this.piece ){
this.piece.draw( this.board, this.piece.x, this.piece.y );
}
}
addPieceToBoard(){
const ii = this.piece.y / 10;
for( var i = 0; i < 4; ++i ){
if( this.piece.tet >> (i*4) ){
this.lines[ ii + i ] |=
((this.piece.tet >> (i*4)) & 0xf) << (this.piece.x / 10);
}
}
this.piece = 0;
}
checkFit(){
const ii = this.piece.y / 10;
for( var i = 0; i < 4; ++i ){
if( this.piece.tet >> (i*4) ){
if( this.lines[ ii + i ] &
(((this.piece.tet >> (i*4)) & 0xf) << (this.piece.x / 10)) ){
this.piece.y -= 10;
this.addPieceToBoard();
}
}
}
if( (this.piece.y + this.piece.height*10) > maxY ){
this.piece.y -= 10;
this.addPieceToBoard();
}
}
step(){
if( ! this.piece ){
this.piece = this.next;
this.next = new Tet();
this.preview.clearRect( 0, 0,
this.preview.canvas.width,
this.preview.canvas.height );
this.next.draw( this.preview, 20, 20 );
}
this.processInput();
this.checkFit();
this.draw();
}
processInput(){
if( this.pressedKeys.left ^
this.pressedKeys.right ){
if( this.pressedKeys.left ){
this.piece.moveLeft();
} else {
this.piece.moveRight();
}
}
if( this.pressedKeys.down ){
this.piece.moveDown();
}
if( this.pressedKeys.comma ^
this.pressedKeys.dot ^
this.pressedKeys.up ){
if( this.pressedKeys.comma | this.pressedKeys.up ){
this.piece.rotateLeft();
} else {
this.piece.rotateRight();
}
}
}
}
var keyMap = {
37: 'left',
39: 'right',
38: 'up',
40: 'down',
188: 'comma',
190: 'dot'
};
var qs = (sel,ctx) => (ctx || document).querySelector( sel );
function keydown( event ){
var key = keyMap[ event.keyCode ];
game.pressedKeys[ key ] = true;
}
function keyup( event ){
var key = keyMap[ event.keyCode ];
game.pressedKeys[ key ] = false;
}
window.addEventListener( "keydown", keydown, false );
window.addEventListener( "keyup", keyup, false );
var game = new Game( qs( ".board" ).getContext("2d"),
qs( ".preview" ).getContext("2d") );
function update( progress ){
//console.log( progress );
game.step();
}
var lastRender = 0;
function loop( timestamp ){
var progress = timestamp - lastRender;
if( progress > 100 ){
update( progress );
lastRender = timestamp;
}
window.requestAnimationFrame( loop );
}
window.requestAnimationFrame( loop );
</script>
[toc] | [next] | [standalone]
| From | luserdroog <luser.droog@gmail.com> |
|---|---|
| Date | 2019-11-16 21:53 -0800 |
| Message-ID | <d5d45944-e196-4478-a3f8-11f4b96ba4e1@googlegroups.com> |
| In reply to | #37802 |
On Saturday, November 16, 2019 at 6:13:35 PM UTC-6, luserdroog wrote: > Many thanks for the suggestions to my questions about > my game loop. I have it a little more fleshed out now. > But there are a whole host of little problems, not least > of which is the sluggish response. I'm also having > difficulty getting the pieces to rotate around their > center points. > > I chose to represent a tetromino piece as a 4x4 bitmap > in an integer expressed in hexadecimal. This worked great > on paper, but the logic to manipulate them is getting a > little cumbersome and confusing. > Found a nice graphic at tetris.wiki/Arika_Rotation_System which suggests that I can handle rotation around a center by adjusting my placement of the bits in the 4x4 frame. I also think things will be easier if I represent the board as s 2D array instead of an array of integers.
[toc] | [prev] | [next] | [standalone]
| From | Julio Di Egidio <julio@diegidio.name> |
|---|---|
| Date | 2019-11-22 09:07 -0800 |
| Message-ID | <d3485732-2d16-4ea4-8ca2-9e3600bf4067@googlegroups.com> |
| In reply to | #37802 |
On Sunday, 17 November 2019 01:13:35 UTC+1, luserdroog wrote:
> Many thanks for the suggestions to my questions about
> my game loop. I have it a little more fleshed out now.
> But there are a whole host of little problems, not least
> of which is the sluggish response. I'm also having
> difficulty getting the pieces to rotate around their
> center points.
>
> I chose to represent a tetromino piece as a 4x4 bitmap
> in an integer expressed in hexadecimal. This worked great
> on paper, but the logic to manipulate them is getting a
> little cumbersome and confusing.
>
> So, here's my mess. Any suggestions for improvement welcome.
Few sparse comments/suggestions:
> class Tet {
The main problem is that you have not partitioned responsibilities correctly
(not enough). The Tet "class" is your game logic (call it Tetris, with possibly
a TetrisPiece "class" to represent the single piece), and it should be in charge
of everything related to the logic of the game, not just positions and allowed
moves, but also the logic for initial as well as end-game positions (logic for
winning/losing matches), how many points one scores, and similar.
OTOH, any rendering logic should be in a View "class", in particular how to map
game board and pieces from their own (game-specific) representation and cords
(and you would use unit coords and distances there: e.g. offsets are by 1, not
by 10, which is a detail of the rendering and not necessarily a fixed one) to
pixels on a canvas or other rendering target of any arbitrary kind and size.
> get width(){
> return {
> 0x33: 2, 0x63: 3, 0x132: 2, 0xf: 4, 0x1111: 1,
> 0x47: 3, 0x113: 2, 0x71: 3, 0x322: 2,
> 0x74: 3, 0x223: 2, 0x17: 3, 0x311: 2,
> 0x27: 3, 0x131: 2, 0x72: 3, 0x232: 2
> }[ this.tet ];
> }
That and similar are clever but highly inefficient. Use a constant data
structure in the outer scope to hold that data (for one thing, you are now
recreating it at each and every function invocation), and access that where you
need. Moreover, better use a single data structure where to each key (to each
piece type) you associate all the needed info, from how big it is to how it
rotates. And better get also rid of those "adjust" functions as separate
functions: that logic should be encapsulated directly into the game board
logic itself. I mean, something like (just an initial sketch to give an
overall idea, the details might very well end up being different):
const TetrisPieceSpec = {
0x33: {
width: 2,
height: 2,
rotLeft: { key: 0x33, offsetX: 0, offsetY: 0 },
...
},
...
}
function TetrisPiece(key, x, y) {
this.key = key;
this.x = x;
this.y = y;
this.rotateLeft = function () {
var left = TetrisPieceSpec[this.key].rotLeft;
var leftX = this.x + left.offsetX;
var leftY = this.y + left.offsetY;
return new TetrisPiece(left.key, leftX, leftY);
};
...
}
function Tetris(onUpdate) { // a callback as the simpliest notification
// mechanism: of course better can be done...
this.board = ...create pieces...;
this.state = TETRIS_STATE.BOOTING; // then READY, STARTED, ENDED, or similar
...
this.start = function () { ...; onUpdate(); };
this.move = function (move) { ...; onUpdate(); };
...
this.canStart = function () { ... };
this.canMove = function (move) { ... };
...
}
function TetrisView(Tetris) { // Tetris (the "game") is directly the model: I
// am passing the constructor here due to having
// to pass a callback to it, but, as said, better
// can be done, and I do not mean custom events
// but rather observable interfaces and data
// binding engines: look up Knockout.js...
var game = new Tetris(onUpdate);
function onUpdate() {
...re-render...
}
this.start = function () { if game.canStart() { game.start(); } };
...
}
> var lastRender = 0;
> function loop( timestamp ){
> var progress = timestamp - lastRender;
> if( progress > 100 ){
> update( progress );
> lastRender = timestamp;
> }
> window.requestAnimationFrame( loop );
> }
> window.requestAnimationFrame( loop );
BTW, isn't that actually keeping your CPU pretty busy all the time?
That is just not the way you implement an animation loop in any language,
all the more so in JS where Thread.sleep and similar do not even make sense...
Julio
[toc] | [prev] | [next] | [standalone]
| From | luserdroog <luser.droog@gmail.com> |
|---|---|
| Date | 2019-11-22 21:47 -0800 |
| Message-ID | <544af123-8327-4009-a884-eff3c13b0e90@googlegroups.com> |
| In reply to | #37835 |
On Friday, November 22, 2019 at 11:07:48 AM UTC-6, Julio Di Egidio wrote:
> On Sunday, 17 November 2019 01:13:35 UTC+1, luserdroog wrote:
> > Many thanks for the suggestions to my questions about
> > my game loop. I have it a little more fleshed out now.
> > But there are a whole host of little problems, not least
> > of which is the sluggish response. I'm also having
> > difficulty getting the pieces to rotate around their
> > center points.
> >
> > I chose to represent a tetromino piece as a 4x4 bitmap
> > in an integer expressed in hexadecimal. This worked great
> > on paper, but the logic to manipulate them is getting a
> > little cumbersome and confusing.
> >
> > So, here's my mess. Any suggestions for improvement welcome.
>
> Few sparse comments/suggestions:
>
> > class Tet {
>
> The main problem is that you have not partitioned responsibilities correctly
> (not enough). The Tet "class" is your game logic (call it Tetris, with possibly
> a TetrisPiece "class" to represent the single piece), and it should be in charge
> of everything related to the logic of the game, not just positions and allowed
> moves, but also the logic for initial as well as end-game positions (logic for
> winning/losing matches), how many points one scores, and similar.
>
> OTOH, any rendering logic should be in a View "class", in particular how to map
> game board and pieces from their own (game-specific) representation and cords
> (and you would use unit coords and distances there: e.g. offsets are by 1, not
> by 10, which is a detail of the rendering and not necessarily a fixed one) to
> pixels on a canvas or other rendering target of any arbitrary kind and size.
Good ideas, thank you. I've already changed to using unit coords in the
next re-write. I don't have a separate View class so far, just draw()
functions, but I'll see if I can factor those out and isolate the view.
> > get width(){
> > return {
> > 0x33: 2, 0x63: 3, 0x132: 2, 0xf: 4, 0x1111: 1,
> > 0x47: 3, 0x113: 2, 0x71: 3, 0x322: 2,
> > 0x74: 3, 0x223: 2, 0x17: 3, 0x311: 2,
> > 0x27: 3, 0x131: 2, 0x72: 3, 0x232: 2
> > }[ this.tet ];
> > }
>
> That and similar are clever but highly inefficient. Use a constant data
> structure in the outer scope to hold that data (for one thing, you are now
> recreating it at each and every function invocation), and access that where you
> need.
This is surprising to me, but it does make sense. I would have expected
the object to be optimized to a constant, but it seems that's a QOI
issue and not part of the language per se. I'll factor these kinds of
things out of my functions. I've seen advice elsewhere that it might
be better to use a closure (IIFE) instead of a class to remove all
of the "this" keywords. I haven't worried too much about efficiency
yet, but I'll do a pass (or another re-write) to try to improve
performance.
> Moreover, better use a single data structure where to each key (to each
> piece type) you associate all the needed info, from how big it is to how it
> rotates. And better get also rid of those "adjust" functions as separate
> functions: that logic should be encapsulated directly into the game board
> logic itself. I mean, something like (just an initial sketch to give an
> overall idea, the details might very well end up being different):
>
> const TetrisPieceSpec = {
> 0x33: {
> width: 2,
> height: 2,
> rotLeft: { key: 0x33, offsetX: 0, offsetY: 0 },
> ...
> },
> ...
> }
>
> function TetrisPiece(key, x, y) {
> this.key = key;
> this.x = x;
> this.y = y;
>
> this.rotateLeft = function () {
> var left = TetrisPieceSpec[this.key].rotLeft;
>
> var leftX = this.x + left.offsetX;
> var leftY = this.y + left.offsetY;
>
> return new TetrisPiece(left.key, leftX, leftY);
> };
>
> ...
> }
>
> function Tetris(onUpdate) { // a callback as the simpliest notification
> // mechanism: of course better can be done...
> this.board = ...create pieces...;
> this.state = TETRIS_STATE.BOOTING; // then READY, STARTED, ENDED, or similar
> ...
> this.start = function () { ...; onUpdate(); };
> this.move = function (move) { ...; onUpdate(); };
> ...
> this.canStart = function () { ... };
> this.canMove = function (move) { ... };
> ...
> }
>
> function TetrisView(Tetris) { // Tetris (the "game") is directly the model: I
> // am passing the constructor here due to having
> // to pass a callback to it, but, as said, better
> // can be done, and I do not mean custom events
> // but rather observable interfaces and data
> // binding engines: look up Knockout.js...
> var game = new Tetris(onUpdate);
>
> function onUpdate() {
> ...re-render...
> }
>
> this.start = function () { if game.canStart() { game.start(); } };
> ...
> }
>
> > var lastRender = 0;
> > function loop( timestamp ){
> > var progress = timestamp - lastRender;
> > if( progress > 100 ){
> > update( progress );
> > lastRender = timestamp;
> > }
> > window.requestAnimationFrame( loop );
> > }
> > window.requestAnimationFrame( loop );
>
> BTW, isn't that actually keeping your CPU pretty busy all the time?
> That is just not the way you implement an animation loop in any language,
> all the more so in JS where Thread.sleep and similar do not even make sense...
>
Based on my research, requestAnimationFrame seems to be widely
recommended for game loops recently. Other options are setTimeout()
or sleep() in a loop. But requestAnimationFrame has the advantage
that it automatically stops running when you switch tabs. I checked
my CPU usage and it seems pretty low (~20%) when the game is running.
I think sleep() in a loop would be worse, because it would monopolize
the (single) thread. But maybe a timeout is worth considering,
although I think it would also need to pause itself when switching
tabs, which means extra code. OTOH using timeouts might make gravity
easier to do.
Thanks again for taking a look. I should be able to post a fully
working game soon. I expect some stupids will still remain.
droog
[toc] | [prev] | [next] | [standalone]
| From | Julio Di Egidio <julio@diegidio.name> |
|---|---|
| Date | 2019-11-23 05:47 -0800 |
| Message-ID | <0d20308e-0e71-40c4-ad11-9865d814ca2d@googlegroups.com> |
| In reply to | #37842 |
On Saturday, 23 November 2019 06:48:02 UTC+1, luserdroog wrote: > On Friday, November 22, 2019 at 11:07:48 AM UTC-6, Julio Di Egidio wrote: <snipped throughout> > > by 10, which is a detail of the rendering and not necessarily a fixed one) to > > pixels on a canvas or other rendering target of any arbitrary kind and size. > > Good ideas, thank you. How kind, but it's more than just "ideas": it was again a *code review*, the first round of it. > I've already changed to using unit coords in the > next re-write. I don't have a separate View class so far, just draw() > functions, but I'll see if I can factor those out and isolate the view. But you do have a view class, just you have called it Game... > > > 0x27: 3, 0x131: 2, 0x72: 3, 0x232: 2 > > > }[ this.tet ]; > > > > That and similar are clever but highly inefficient. Use a constant data > > structure in the outer scope to hold that data (for one thing, you are now > > recreating it at each and every function invocation), and access that where you > > need. > > This is surprising to me, but it does make sense. I would have expected > the object to be optimized to a constant, JS is a dynamic language, so there are more limits to what a compiler can optimise away, moreover there is no "standard" compiler you can trust. In any case, my point is immediately on logic and clarity, indeed most performance issues (as well as many bugs) automagically disappear if one just streamlines/rationalises/cleans up the code. > but it seems that's a QOI > issue and not part of the language per se. I'll factor these kinds of > things out of my functions. I've seen advice elsewhere that it might > be better to use a closure (IIFE) instead of a class to remove all > of the "this" keywords. More than the problem of few more keystrokes (code should be readable, not "writable"), it is about recovering a private scope. Indeed, you'll see that you end up with something quite similar to the good old JS without that "class" nonsense... > > BTW, isn't that actually keeping your CPU pretty busy all the time? > > That is just not the way you implement an animation loop in any language, > > all the more so in JS where Thread.sleep and similar do not even make sense... > > Based on my research, requestAnimationFrame seems to be widely > recommended for game loops recently. Widely recommended? Really? > Other options are setTimeout() The common option. > or sleep() in a loop. Again: sleep does NOT exist in JS, there are NO threads in JS at all. OTOH, where it does exist, it is the usual way to handle such loops. > But requestAnimationFrame has the advantage > that it automatically stops running when you switch tabs. I see there is an example of what you did on MDN and the blog posts linked thereof: it's highly inefficient, I have no idea what the heck they are thinking... To recover the benefits of the animation stopping when switching tabs, besides rather combining the requestAnimationFrame with scheduling the next timeout (but again, why s omuch pain?), one can even just listen to the window.blur event. There are *other* scenarios where requestAnimationFrame is the way to go, but these are the scenarios where a setTimeout(., 0) makes sense, just not the loops... > I checked > my CPU usage and it seems pretty low (~20%) when the game is running. Sure, for now. > Thanks again for taking a look. I should be able to post a fully > working game soon. I expect some stupids will still remain. It's naive on your part, not to say demeaning (to the discipline, not to me) to think that it's just about few isolated stupids. Anyway, even that is part of the very same and very long learning curve... Will look forward to the running game (and the next version of the code), fun stuff in any case. Julio
[toc] | [prev] | [standalone]
Back to top | Article view | comp.lang.javascript
csiph-web