Last active
February 4, 2018 15:16
-
-
Save screeny05/0a001cac09176d27eaf1a3ce8c73dd91 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
type Color = string; | |
class Player { | |
constructor(public name: string, public color: Color){ } | |
} | |
type Field = Player|null; | |
class GameField { | |
private fields: Field[][]; | |
constructor(public width: number, public height: number){ | |
this.clear(); | |
} | |
clear(): void { | |
this.fields = new Array(this.width).fill(null).map(() => new Array(this.height).fill(null).map(() => null)); | |
} | |
at(x: number, y: number): Field { | |
return this.fields[x][y]; | |
} | |
tryAt(x: number, y: number): Field { | |
if(!this.isWithinBounds(x, y)){ | |
return null; | |
} | |
return this.at(x, y); | |
} | |
setPlayerAt(x: number, y: number, player: Player): void { | |
this.fields[x][y] = player; | |
} | |
isWithinBounds(x: number, y: number): boolean { | |
return x >= 0 && y >= 0 && x < this.width && y < this.height; | |
} | |
iterate(cb: (field: Field, x?: number, y?: number) => boolean|void): void { | |
for(let x = 0; x < this.width; x++){ | |
for(let y = 0; y < this.height; y++){ | |
if(cb(this.at(x, y), x, y) === false){ | |
return; | |
} | |
} | |
} | |
} | |
getCol(x: number): Field[] { | |
const seq: Field[] = []; | |
for(let y = 0; y < this.height; y++){ | |
seq.push(this.at(x, y)); | |
} | |
return seq; | |
} | |
getRow(y: number): Field[] { | |
const seq: Field[] = []; | |
for(let x = 0; x < this.width; x++){ | |
seq.push(this.at(x, y)); | |
} | |
return seq; | |
} | |
isFull(): boolean { | |
let isFull = true; | |
this.iterate(field => { | |
if(!field){ | |
isFull = false; | |
} | |
return false; | |
}); | |
return isFull; | |
} | |
getBottomMostEmptyField(x: number): number|null { | |
for(let y = this.height - 1; y >= 0; y--){ | |
if(!this.at(x, y)){ | |
return y; | |
} | |
} | |
return null; | |
} | |
getPossibleWinSequenceAt(x: number, y: number, player: Player, minMatches: number): Field[] | null { | |
let foundSeq: Field[] | null = null; | |
[ | |
this.getHorizontalSequence.bind(this), | |
this.getVerticalSequence.bind(this), | |
this.getDiagonalAscSequence.bind(this), | |
this.getDiagonalDescSequence.bind(this) | |
].find(sequenceFactory => { | |
const seq = sequenceFactory(x, y, minMatches); | |
const valid = this.checkFieldSequence(seq, player, minMatches); | |
if(valid){ | |
foundSeq = seq; | |
} | |
return valid; | |
}); | |
return foundSeq; | |
} | |
checkFieldSequence(fields: Field[], player: Player, minMatches: number): boolean { | |
let matchCount = 0; | |
fields.forEach(field => { | |
if(field === player){ | |
matchCount++; | |
} else { | |
matchCount = 0; | |
} | |
}); | |
return matchCount >= minMatches; | |
} | |
sequenceMapper(count: number, cb: (i: number) => Field): Field[] { | |
const seq: Field[] = []; | |
for(let i = -count + 1; i < count; i++){ | |
seq.push(cb(i)); | |
} | |
return seq; | |
} | |
getHorizontalSequence(x: number, y: number, count: number): Field[] { | |
return this.sequenceMapper(count, i => this.tryAt(x + i, y)); | |
} | |
getVerticalSequence(x: number, y: number, count: number): Field[] { | |
return this.sequenceMapper(count, i => this.tryAt(x, y + i)); | |
} | |
getDiagonalAscSequence(x: number, y: number, count: number): Field[] { | |
return this.sequenceMapper(count, i => this.tryAt(x + i, y + i)); | |
} | |
getDiagonalDescSequence(x: number, y: number, count: number): Field[] { | |
return this.sequenceMapper(count, i => this.tryAt(x + i, y - i)); | |
} | |
} | |
enum GameStatus { | |
RUNNING, | |
WON, | |
FULL, | |
} | |
class Game { | |
player: Player[] = []; | |
playerTurnIndex: number = 0; | |
field: GameField; | |
status: GameStatus = GameStatus.RUNNING; | |
minMatches: number = 4; | |
constructor(){ | |
this.player.push(new Player('Sebastian', '#ffff00')); | |
this.player.push(new Player('Janike', '#ff0000')); | |
this.field = new GameField(7, 6); | |
} | |
turn(x: number): void { | |
const currentPlayer = this.rotatePlayer(); | |
const y = this.field.getBottomMostEmptyField(x); | |
if(y === null){ | |
throw new Error(`Col in field at x ${x} is full.`); | |
} | |
this.field.setPlayerAt(x, y, currentPlayer); | |
const possibleWin = this.field.getPossibleWinSequenceAt(x, y, currentPlayer, this.minMatches); | |
if(possibleWin){ | |
this.status = GameStatus.WON; | |
alert(currentPlayer.name + ' won!'); | |
return; | |
} | |
if(this.field.isFull()){ | |
this.status = GameStatus.FULL; | |
} | |
} | |
reset(){ | |
this.field.clear(); | |
this.playerTurnIndex = 0; | |
this.status = GameStatus.RUNNING; | |
} | |
rotatePlayer(): Player { | |
if(this.player.length === 0){ | |
throw new Error('Playerlist is Empty'); | |
} | |
if(this.playerTurnIndex + 1 >= this.player.length){ | |
this.playerTurnIndex = 0; | |
} else { | |
this.playerTurnIndex = this.playerTurnIndex + 1; | |
} | |
return this.currentPlayer; | |
} | |
get currentPlayer(): Player { | |
if(this.player.length === 0){ | |
throw new Error('Playerlist is Empty'); | |
} | |
return this.player[this.playerTurnIndex]; | |
} | |
} | |
class ConnectNFrontendCanvas { | |
private game: Game; | |
private ctx: CanvasRenderingContext2D; | |
private element: HTMLCanvasElement; | |
isRunning: boolean = true; | |
backgroundColor: Color = '#0000ff'; | |
highlightColor: Color = '#4a4aff'; | |
constructor(element: HTMLCanvasElement){ | |
this.game = new Game(); | |
this.element = element; | |
this.ctx = this.element.getContext('2d'); | |
this.element.addEventListener('click', this.onClick.bind(this)); | |
this.loop(); | |
} | |
render(timestamp: number): void { | |
const {ctx} = this; | |
const colWidth = this.element.width / this.game.field.width; | |
const rowHeight = this.element.height / this.game.field.height; | |
const circleDiameter = Math.min(rowHeight, colWidth) - 20; | |
ctx.fillStyle = this.backgroundColor; | |
ctx.fillRect(0, 0, this.element.width, this.element.height); | |
this.game.field.iterate((field: Field, x: number, y: number) => { | |
const posX = x * colWidth + 0.5 * colWidth; | |
const posY = y * rowHeight + 0.5 * rowHeight; | |
ctx.fillStyle = this.highlightColor; | |
ctx.beginPath(); | |
ctx.arc(posX, posY + 4, circleDiameter / 2 + 2, 0, 2 * Math.PI); | |
ctx.fill(); | |
if(field){ | |
ctx.fillStyle = field.color; | |
} else { | |
ctx.fillStyle = '#fff'; | |
} | |
ctx.beginPath(); | |
ctx.arc(posX, posY, circleDiameter / 2, 0, 2 * Math.PI); | |
ctx.fill(); | |
}); | |
} | |
loop(): void { | |
if(!this.isRunning){ | |
return; | |
} | |
requestAnimationFrame(timestamp => { | |
this.tick(timestamp); | |
this.loop(); | |
}); | |
} | |
tick(timestamp: number): void { | |
this.render(timestamp); | |
} | |
private onClick(e: MouseEvent): void { | |
if(this.game.status !== GameStatus.RUNNING){ | |
return; | |
} | |
const colWidth = this.element.clientWidth / this.game.field.width; | |
const x = Math.floor(e.x / colWidth); | |
this.game.turn(x); | |
} | |
} | |
let canvas = document.createElement('canvas'); | |
document.body.appendChild(canvas); | |
document.body.style.margin = '0'; | |
let x = new ConnectNFrontendCanvas(canvas); | |
let setSize = () => { | |
canvas.width = document.body.clientWidth; | |
canvas.height = document.body.clientHeight; | |
} | |
setSize(); | |
document.addEventListener('resize', setSize); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment