Skip to content

Commit

Permalink
Better moves for computer player (looks further ahead, doesn't miss a…
Browse files Browse the repository at this point in the history
…s many obvious moves)
  • Loading branch information
JeremyLeland committed Oct 31, 2024
1 parent e4c83e6 commit 0a4327e
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 63 deletions.
41 changes: 29 additions & 12 deletions games/Connect4/Canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export class Canvas {
scrollX = 0;
scrollY = 0;

backgroundColor = 'black';

#scale = 1;
#offsetX = 0;
#offsetY = 0;
Expand Down Expand Up @@ -38,30 +40,39 @@ export class Canvas {
this.#scale = Math.min( inlineSize, blockSize );

// this might get messed up if writing mode is vertical
this.#offsetX = inlineSize - this.#scale;
this.#offsetY = blockSize - this.#scale;
// Why did we have devicePixelRatio in here before? Is it needed by Safari?
this.#offsetX = ( inlineSize - this.#scale );// / devicePixelRatio;
this.#offsetY = ( blockSize - this.#scale );// / devicePixelRatio;

// console.log( 'offsetX = ' + this.#offsetX + ', offsetY = ' + this.#offsetY );
} );

this.ctx.translate( this.#offsetX, this.#offsetY );

this.ctx.scale( devicePixelRatio, devicePixelRatio );
this.ctx.scale( this.#scale, this.#scale );

this.redraw();
} );

resizeObserver.observe( this.canvas );
}

redraw() {
this.ctx.clearRect( 0, 0, this.ctx.canvas.width, this.ctx.canvas.height );
this.ctx.fillStyle = this.backgroundColor;
this.ctx.fillRect( 0, 0, this.ctx.canvas.width, this.ctx.canvas.height );

this.ctx.save(); {
this.ctx.translate( this.#offsetX, this.#offsetY );

this.ctx.scale( devicePixelRatio, devicePixelRatio );
this.ctx.scale( this.#scale, this.#scale );

this.ctx.scale( this.zoom, this.zoom );
this.ctx.translate( this.scrollX, this.scrollY );
this.ctx.translate( -this.scrollX, -this.scrollY );
this.ctx.lineWidth = this.zoom;

this.draw( this.ctx );
try {
this.draw( this.ctx );
}
catch ( e ) {
console.error( e );
}
}
this.ctx.restore();
}
Expand Down Expand Up @@ -96,10 +107,16 @@ export class Canvas {
// TODO: Account for offset when centered canvas

getPointerX( e ) {
return ( ( e.clientX - this.#offsetX / devicePixelRatio ) / this.#scale ) / this.zoom - this.scrollX;
return ( ( e.clientX - this.#offsetX / devicePixelRatio ) / this.#scale ) / this.zoom + this.scrollX;
}

getPointerY( e ) {
return ( ( e.clientY - this.#offsetY / devicePixelRatio ) / this.#scale ) / this.zoom - this.scrollY;
return ( ( e.clientY - this.#offsetY / devicePixelRatio ) / this.#scale ) / this.zoom + this.scrollY;
}

zoomAt( x, y, amount ) {
this.zoom *= amount;


}
}
162 changes: 122 additions & 40 deletions games/Connect4/Connect4.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,36 +122,41 @@ export class Connect4 {
}

applyMove( move ) {
console.log( `Applying move for Player ${ this.turn }: ${ move }` );
// console.log( `Applying move for Player ${ this.turn }: ${ move }` );

if ( this.victory > 0 ) {
console.warn( `Player ${ this.victory } already won game` );
return;
return 0;
}

if ( move[ 0 ] < 0 || move[ 0 ] >= Cols ) {
console.warn( `Invalid column: ${ move[ 0 ] }` );
return;
return 0;
}

if ( move[ 1 ] < 0 || move[ 1 ] >= Rows ) {
console.warn( `Invalid row: ${ move[ 1 ] }` );
return;
return 0;
}

const current = this.getAt( move[ 0 ], move[ 1 ] );

if ( current != 0 ) {
console.warn( `Board already has Player ${ current } at ${ move }` );
return;
return 0;
}

this.setAt( move[ 0 ], move[ 1 ], this.turn );

const longest = this.getLongestAt( move[ 0 ], move[ 1 ], this.turn );
if ( longest >= 4 ) {
const longest = this.getLongestAt( move[ 0 ], move[ 1 ] );

if ( Math.abs( longest ) > 4 ) {
// debugger;
}

if ( Math.abs( longest ) >= 4 ) {
this.victory = this.turn;
console.log( `Player ${ this.turn } wins with ${ longest } in a row!` );
// console.log( `Player ${ this.turn } wins with ${ longest } in a row!` );
}

this.turn = this.turn == Players ? 1 : this.turn + 1;
Expand Down Expand Up @@ -196,44 +201,121 @@ export class Connect4 {
return moves;
}

getLongestAt( col, row, team ) {
// console.log( `Finding longest line for Player ${ team } at ${ col },${ row }` );
getLongestAt( col, row ) {
// console.log( `Finding longest line score at ${ col },${ row }` );
let longest = 0;

if ( this.getAt( col, row ) == team ) {
[
[ 1, 0 ], // vertical
[ 0, 1 ], // horizontal
[ 1, 1 ], // diagonal 1
[ 1, -1 ], // diagonal 2
].forEach( orientation => {
let length = 1;

[ -1, 1 ].forEach( dir => {
let c = col, r = row;

while ( true ) {
c += dir * orientation[ 0 ];
r += dir * orientation[ 1 ];

if ( 0 <= c && c < Cols && 0 <= r && r < Rows && this.getAt( c, r ) == team ) {
length ++;
}
else {
break;
}
}
} );
const team = this.getAt( col, row );

if ( team == 0 ) {
return 0;
}

[
[ 1, 0 ], // vertical
[ 0, 1 ], // horizontal
[ 1, 1 ], // diagonal 1
[ 1, -1 ], // diagonal 2
].forEach( orientation => {
let length = 1;

[ -1, 1 ].forEach( dir => {
let c = col, r = row;

// console.log( ` Length for orientation ${ orientation } is ${ length }`);
while ( true ) {
c += dir * orientation[ 0 ];
r += dir * orientation[ 1 ];

longest = Math.max( longest, length );
if ( 0 <= c && c < Cols && 0 <= r && r < Rows && this.getAt( c, r ) == team ) {
length ++;
}
else {
break;
}
}
} );
}
else {
console.warn( `Player at ${ col },${ row } is actually ${ this.getAt( col, row ) }, expecting ${ team }` );

// console.log( ` Length for orientation ${ orientation } is ${ length }`);

longest = Math.max( longest, length );
} );

return ( team == 1 ? -1 : 1 ) * longest;
}

//
// NOTE: Currently, tied boards may give -# or # depending on which pieces it finds first
// TODO: Should tied boards be represented differently?
// NOW: Trying only win and loss numbers, zero otherwise

getScore() {
let longest = 0;

for ( let col = 0; col < Cols; col ++ ) {
for ( let row = 0; row < Rows; row ++ ) {
const longestAt = this.getLongestAt( col, row );

if ( Math.abs( longestAt ) >= 4 ) {
return longestAt;
}

// console.log( `Longest at ${ col },${ row } is ${ longestAt }` );

// if ( Math.abs( longestAt ) > Math.abs( longest ) ) {
// longest = longestAt;
// }
}
}

return longest;
// return longest;
return 0;
}

getNextMoves( depth ) {
const moves = [];

this.getPossibleMoves().forEach( move => {
this.applyMove( move );

const item = {
move: move,
};

const score = this.getScore();

if ( Math.abs( score ) >= 4 || depth <= 1 ) {
item.score = score;
}
else {
item.nextMoves = this.getNextMoves( depth - 1 );

// How to account for a move that gets 4 now being better than a move that gets 4 in several moves?

// TODO: Need to min/max here. It's assuming best moves for same player in every case
// Need to take in which player we are optimizing for to properly summarize

// FIXME: Why is it getting zero so often now?

const minFunc = ( a, b ) => a < b;
const maxFunc = ( a, b ) => a > b;

const func = this.turn == 1 ? minFunc : maxFunc;

let best = this.turn == 1 ? Infinity : -Infinity;
item.nextMoves.forEach( nextMove => {
if ( func( nextMove.score, best ) ) {
best = nextMove.score;
}
} );

item.score = best;
}

moves.push( item );

this.undo();
} );

return moves;
}
}
27 changes: 16 additions & 11 deletions games/Connect4/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,41 @@

const canvas = new Canvas();
canvas.zoom = 1 / 7;
canvas.scrollX = 0.5;
canvas.scrollY = 1.5;
canvas.scrollX = -0.5;
canvas.scrollY = -1.5;

let game = Connect4.fromLocalStore() ?? Connect4.newGame( 1 );

canvas.update = ( dt ) => {
if ( !game.update( dt ) ) {
game.toLocalStore();

// NOTE: Hardcoded for 2nd AI player only at the moment (wants positive moves)
if ( game.victory == 0 && game.aiPlayers.includes( game.turn ) ) {

let bestMove, bestScore = 0;
game.getPossibleMoves().forEach( move => {
const score = game.applyMove( move );
game.undo();
const nextMoves = game.getNextMoves( 6 );

// console.log( nextMoves );

if ( score > bestScore || ( score == bestScore && Math.random() < 0.5 ) ) {
bestMove = move;
bestScore = score;
let bestMove, bestScore = -Infinity;
nextMoves.forEach( nextMove => {
if ( nextMove.score > bestScore || ( nextMove.score == bestScore && Math.random() < 0.5 ) ) {
bestMove = nextMove.move;
bestScore = nextMove.score;
}
} );

// console.log( `Found bestMove ${ bestMove } with score ${ bestScore }` );

game.active.x = bestMove[ 0 ];
game.active.y = -1;
game.dropActive();
}
else {
canvas.stop();

// TODO: Should we do one last frame here?
// (need clearer case to check, should be showing yellow piece while thinking)
}
}
}
Expand All @@ -64,8 +71,6 @@
game.draw( ctx );
}

canvas.redraw();

function updateActivePosition( e ) {
const mouseX = canvas.getPointerX( e );
const mouseY = canvas.getPointerY( e );
Expand Down

0 comments on commit 0a4327e

Please sign in to comment.