-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7186fe4
commit ebc5dc7
Showing
5 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
export class Canvas { | ||
zoom = 1; | ||
scrollX = 0; | ||
scrollY = 0; | ||
|
||
#scale = 1; | ||
#offsetX = 0; | ||
#offsetY = 0; | ||
|
||
#reqId; | ||
|
||
constructor( canvas ) { | ||
this.canvas = canvas; | ||
|
||
if ( !this.canvas ) { | ||
this.canvas = document.createElement( 'canvas' ); | ||
document.body.appendChild( this.canvas ); | ||
} | ||
this.canvas.style.width = '100%'; | ||
this.canvas.style.height = '100%'; | ||
|
||
this.canvas.oncontextmenu = () => { return false }; | ||
|
||
this.ctx = this.canvas.getContext( '2d' /*, { alpha: false }*/ ); | ||
|
||
const resizeObserver = new ResizeObserver( entries => { | ||
entries.forEach( entry => { | ||
// safari does not support devicePixelContentBoxSize, attempting to work around | ||
const width = entry.devicePixelContentBoxSize?.[ 0 ].inlineSize ?? ( entry.contentBoxSize[ 0 ].inlineSize * devicePixelRatio ); | ||
const height = entry.devicePixelContentBoxSize?.[ 0 ].blockSize ?? ( entry.contentBoxSize[ 0 ].blockSize * devicePixelRatio ); | ||
this.canvas.width = width; | ||
this.canvas.height = height; | ||
|
||
// this still needs to be based on content box | ||
const inlineSize = entry.contentBoxSize[ 0 ].inlineSize; | ||
const blockSize = entry.contentBoxSize[ 0 ].blockSize; | ||
|
||
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; | ||
} ); | ||
|
||
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.save(); { | ||
this.ctx.scale( this.zoom, this.zoom ); | ||
this.ctx.translate( this.scrollX, this.scrollY ); | ||
this.ctx.lineWidth = this.zoom; | ||
|
||
this.draw( this.ctx ); | ||
} | ||
this.ctx.restore(); | ||
} | ||
|
||
start() { | ||
if ( !this.#reqId ) { // don't try to start again if already started | ||
let lastTime; | ||
const animate = ( now ) => { | ||
lastTime ??= now; // for first call only | ||
this.update( now - lastTime ); | ||
lastTime = now; | ||
|
||
this.redraw(); | ||
|
||
if ( this.#reqId ) { // make sure we didn't stop it | ||
this.#reqId = requestAnimationFrame( animate ); | ||
} | ||
}; | ||
|
||
this.#reqId = requestAnimationFrame( animate ); | ||
} | ||
} | ||
|
||
stop() { | ||
cancelAnimationFrame( this.#reqId ); | ||
this.#reqId = null; // so we can check if stopped | ||
} | ||
|
||
update( dt ) {} | ||
draw( ctx ) {} | ||
|
||
// TODO: Account for offset when centered canvas | ||
|
||
getPointerX( e ) { | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
const Cols = 7, Rows = 6, Players = 2; | ||
|
||
const PieceColors = [ '', 'red', 'yellow' ]; | ||
const BoardColor = 'tan'; | ||
|
||
const GameStateKey = 'connect4GameState'; | ||
|
||
const piecePath = new Path2D(); | ||
piecePath.arc( 0, 0, 0.5, 0, Math.PI * 2 ); | ||
|
||
const BoardPath = new Path2D(); | ||
BoardPath.rect( -0.5, -0.5, Cols, Rows ); | ||
for ( let row = 0; row < Rows; row++ ) { | ||
for ( let col = 0; col < Cols; col++ ) { | ||
BoardPath.moveTo( col, row ); | ||
BoardPath.arc( col, row, 0.4, 0, Math.PI * 2 ); | ||
} | ||
} | ||
|
||
|
||
// NOTE: Turn is 1-indexed so the turn/team match the values in board (0 means no piece) | ||
|
||
export class Connect4 { | ||
static fromLocalStore() { | ||
const gameState = JSON.parse( localStorage.getItem( GameStateKey ) ); | ||
|
||
if ( gameState ) { | ||
return new Connect4( gameState ); | ||
} | ||
} | ||
|
||
toLocalStore() { | ||
localStorage.setItem( GameStateKey, JSON.stringify( this ) ); | ||
} | ||
|
||
static newGame() { | ||
return new Connect4( { | ||
board: Array( Cols * Rows ).fill( 0 ), | ||
history: [], | ||
turn: 1, | ||
victory: 0, | ||
active: { x: 0, y: -1, vy: 0, ay: 0 }, | ||
} ); | ||
} | ||
|
||
constructor( json ) { | ||
Object.assign( this, json ); | ||
} | ||
|
||
update( dt ) { | ||
this.active.y += this.active.vy * dt + 0.5 * this.active.ay * dt * dt; | ||
this.active.vy += this.active.ay * dt; | ||
|
||
const col = this.active.x; | ||
const row = Math.round( this.active.y ); | ||
const nextRow = Math.round( this.active.y + 0.5 ); | ||
|
||
if ( nextRow < Rows && this.getAt( this.active.x, nextRow ) == 0 ) { | ||
return true; // keep going | ||
} | ||
else { | ||
this.applyMove( [ col, row ] ); | ||
|
||
this.active.y = -1; | ||
this.active.vy = 0; | ||
this.active.ay = 0; | ||
|
||
return false; | ||
} | ||
} | ||
|
||
draw( ctx ) { | ||
if ( this.victory == 0 ) { | ||
ctx.save(); { | ||
ctx.translate( this.active.x, this.active.y ); | ||
ctx.fillStyle = PieceColors[ this.turn ]; | ||
ctx.fill( piecePath ); | ||
} | ||
ctx.restore(); | ||
} | ||
|
||
for ( let row = 0; row < Rows; row++ ) { | ||
for ( let col = 0; col < Cols; col++ ) { | ||
const team = this.board[ col + row * Cols ]; | ||
if ( team > 0 ) { | ||
ctx.save(); { | ||
ctx.translate( col, row ); | ||
ctx.fillStyle = PieceColors[ team ]; | ||
ctx.fill( piecePath ); | ||
} | ||
ctx.restore(); | ||
} | ||
} | ||
} | ||
|
||
ctx.fillStyle = BoardColor; | ||
ctx.fill( BoardPath, 'evenodd' ); | ||
|
||
if ( this.victory > 0 ) { | ||
ctx.font = '1px Arial'; | ||
ctx.textAlign = 'center'; | ||
ctx.fillStyle = 'white'; | ||
ctx.shadowColor = 'black'; | ||
ctx.shadowOffsetX = 2; | ||
ctx.shadowOffsetY = 2; | ||
|
||
ctx.fillText( `Player ${ this.victory } Wins!`, 3, 3 ); | ||
} | ||
} | ||
|
||
getAt( col, row ) { | ||
return this.board[ col + row * Cols ]; | ||
} | ||
|
||
setAt( col, row, team ) { | ||
this.board[ col + row * Cols ] = team; | ||
} | ||
|
||
applyMove( move ) { | ||
console.log( `Applying move for Player ${ this.turn }: ${ move }` ); | ||
|
||
if ( this.victory > 0 ) { | ||
console.warn( `Player ${ this.victory } already won game` ); | ||
return; | ||
} | ||
|
||
if ( move[ 0 ] < 0 || move[ 0 ] >= Cols ) { | ||
console.warn( `Invalid column: ${ move[ 0 ] }` ); | ||
return; | ||
} | ||
|
||
if ( move[ 1 ] < 0 || move[ 1 ] >= Rows ) { | ||
console.warn( `Invalid row: ${ move[ 1 ] }` ); | ||
return; | ||
} | ||
|
||
const current = this.getAt( move[ 0 ], move[ 1 ] ); | ||
|
||
if ( current != 0 ) { | ||
console.warn( `Board already has Player ${ current } at ${ move }` ); | ||
return; | ||
} | ||
|
||
this.setAt( move[ 0 ], move[ 1 ], this.turn ); | ||
|
||
const longest = this.getLongestAt( move[ 0 ], move[ 1 ], this.turn ); | ||
if ( longest >= 4 ) { | ||
this.victory = this.turn; | ||
console.log( `Player ${ this.turn } wins with ${ longest } in a row!` ); | ||
} | ||
|
||
this.turn = this.turn == Players ? 1 : this.turn + 1; | ||
this.history.push( move ); | ||
} | ||
|
||
undo() { | ||
const toRemove = this.history.pop(); | ||
|
||
if ( toRemove ) { | ||
this.turn = this.turn == 1 ? Players : this.turn - 1; | ||
this.victory = 0; | ||
this.setAt( toRemove[ 0 ], toRemove[ 1 ], 0 ); | ||
} | ||
else { | ||
console.warn( 'No moves to undo' ); | ||
} | ||
} | ||
|
||
getNextRowAt( col ) { | ||
for ( let row = Rows - 1; row >= 0; row-- ) { | ||
if ( this.getAt( col, row ) == 0 ) { | ||
return row; | ||
} | ||
} | ||
|
||
return -1; | ||
} | ||
|
||
getPossibleMoves() { | ||
const moves = []; | ||
|
||
for ( let col = 0; col < Cols; col ++ ) { | ||
const nextRow = this.getNextRowAt( col ); | ||
if ( nextRow > -1 ) { | ||
moves.push( [ col, nextRow ] ); | ||
} | ||
} | ||
|
||
return moves; | ||
} | ||
|
||
getLongestAt( col, row, team ) { | ||
// console.log( `Finding longest line for Player ${ team } 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; | ||
} | ||
} | ||
} ); | ||
|
||
// console.log( ` Length for orientation ${ orientation } is ${ length }`); | ||
|
||
longest = Math.max( longest, length ); | ||
} ); | ||
} | ||
else { | ||
console.warn( `Player at ${ col },${ row } is actually ${ this.getAt( col, row ) }, expecting ${ team }` ); | ||
} | ||
|
||
return longest; | ||
} | ||
} |
Oops, something went wrong.