Last active
December 17, 2015 11:29
-
-
Save maul-esel/5602514 to your computer and use it in GitHub Desktop.
"TicTacToe" and "Connect 4" implemented with CoffeeScript - designed to be easily extendable.
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Games</title> | |
<meta charset='utf-8'/> | |
<script type='text/javascript' src='coffee-script.js'></script> | |
<script type='text/coffeescript' src='lib.coffee'></script> | |
<link rel='stylesheet' type='text/css' href='style.css'/> | |
</head> | |
<body> | |
<h1>TicTacToe</h1> | |
<canvas id='tic-tac-toe' class='board' width='400px' height='400px'>canvas support required</canvas> | |
<div class='instructor' id='instructor1'></div> | |
<form name='tictactoe'> | |
<input type='button' name='restart' value='restart'/> | |
</form> | |
<hr /> | |
<hr /> | |
<h1>Connect 4</h1> | |
<canvas id='connect-4' class='board' width='400px' height='400px'>canvas support required</canvas> | |
<div class='instructor' id='instructor2'></div> | |
<form name='connect4'> | |
<input type='button' name='restart' value='restart'/> | |
</form> | |
</body> | |
</html> |
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
class Direction | |
@up = 1 | |
@down = 2 | |
@left = 4 | |
@right = 8 | |
@has_direction: (combi, flag) -> | |
(combi & flag) == flag | |
###################################################################################################### | |
class Board | |
constructor: (@canvas) -> | |
@ctx = @canvas.getContext('2d') | |
@canvas.onclick = (event) => @click(event) | |
click: (event) -> | |
@onclick?(event) | |
clear: -> | |
@canvas.width = @canvas.width | |
@draw() | |
class RasterBoard extends Board | |
constructor: (canvas, @x, @y) -> | |
super canvas | |
@fields = {} | |
@fields[i] = {} for i in [1..@x] | |
@each_field((x, y) => @fields[x][y] = false) | |
each_field: (delegate) -> | |
(delegate?(i, j) for i in [1..@x]) for j in [1..@y] | |
draw: -> | |
@field_width = @canvas.width / @x | |
@field_height = @canvas.height / @y | |
@each_field((x, y) => @drawField((x - 1) * @field_width, (y - 1) * @field_height, @field_width, @field_height)) | |
drawField: (x, y, w, h) -> | |
@ctx.save() | |
@ctx.strokeStyle = "gray" | |
@ctx.strokeRect(x, y, w, h) | |
@ctx.restore() | |
take: (player, x, y) -> | |
@fields[x][y] = player | |
is_taken: (x, y) -> | |
@fields[x][y] != false | |
clear: -> | |
super | |
@each_field((x, y) => @fields[x][y] = false) | |
full: -> | |
full = true | |
@each_field((x, y) => full &&= !!@fields[x][y]) | |
full | |
class InARowBoard extends RasterBoard | |
constructor: (canvas, x, y) -> | |
super canvas, x, y | |
take: (player, col, row) -> | |
if @is_taken(col, row) | |
throw 'This field is already taken!' | |
if player == 1 | |
@cross(col, row) | |
else | |
@circle(col, row) | |
super player, col, row | |
cross: (col, row) -> | |
x = (col - 1) * @field_width | |
y = (row - 1) * @field_height | |
left = x + @field_width * 0.25 | |
top = y + @field_height * 0.25 | |
right = x + @field_width * 0.75 | |
bottom = y + @field_height * 0.75 | |
@ctx.beginPath() | |
@ctx.moveTo(left, top) | |
@ctx.lineTo(right, bottom) | |
@ctx.stroke() | |
@ctx.beginPath() | |
@ctx.moveTo(right, top) | |
@ctx.lineTo(left, bottom) | |
@ctx.stroke() | |
circle: (col, row) -> | |
mx = (col - 0.5) * @field_width | |
my = (row - 0.5) * @field_height | |
rad = Math.min(@field_width, @field_height) * 0.25 | |
@ctx.beginPath() | |
@ctx.arc(mx, my, rad, 0, 2 * Math.PI) | |
@ctx.stroke() | |
row: (player, x, y, direction) -> | |
# row has ended here | |
if @fields[x][y] != player | |
return 0 | |
# transform coordinates | |
new_x = x | |
if Direction.has_direction(direction, Direction.left) | |
new_x-- | |
if Direction.has_direction(direction, Direction.right) | |
new_x++ | |
new_y = y | |
if Direction.has_direction(direction, Direction.down) | |
new_y++ | |
if Direction.has_direction(direction, Direction.up) | |
new_y-- | |
# check for final case: | |
# no more fields, but count this one | |
if new_x < 1 || new_x > @x || new_y < 1 || new_y > @y | |
return 1 | |
# recurse | |
return 1 + @row(player, new_x, new_y, direction) | |
class Connect4Board extends InARowBoard | |
take: (player, col, row) -> | |
(new_row = y unless @is_taken(col, y)) for y in [1..@y] | |
if new_row? | |
super player, col, new_row | |
else | |
throw 'This column is already full!' | |
###################################################################################################### | |
class Player | |
constructor: (@number) -> | |
@name = "Player #{@number}" | |
###################################################################################################### | |
class Game | |
constructor: (players) -> | |
@players = ((new Player(i)) for i in [1..players]) | |
start: -> | |
@current_player = 1 | |
@onstart?(@players[0].name) | |
end: -> | |
@onend?() | |
action: -> | |
next: -> | |
old = @current_player | |
@current_player++ | |
if (@current_player > @players.length) | |
@current_player = 1 | |
if (@onnext) | |
@onnext(@players[old-1].name, @players[@current_player-1].name) | |
winner: -> | |
win: (winner) -> | |
@end() | |
@onwin?(@players[winner-1].name) | |
restart: -> | |
@end() | |
@start() | |
class BoardGame extends Game | |
constructor: (players) -> | |
super players | |
@board.draw() | |
start: -> | |
super | |
@board.clear() | |
@board.onclick = (event) => @action(event) | |
end: -> | |
super | |
@board.onclick = null | |
class RasterBoardGame extends BoardGame | |
action: (event) -> | |
super | |
x = event.pageX - @board.canvas.offsetLeft | |
y = event.pageY - @board.canvas.offsetTop | |
column = Math.floor(x / @board.field_width) + 1 | |
row = Math.floor(y / @board.field_height) + 1 | |
@last_action = {"column" : column, "row": row} | |
class InARowGame extends RasterBoardGame | |
@board: InARowBoard | |
constructor: (canvas, x, y, @min_row) -> | |
@board = new @constructor.board canvas, x, y | |
super 2 | |
@players[0].name += " (x)" | |
@players[1].name += " (o)" | |
winner: -> | |
super | |
winner = null | |
@board.each_field((x, y) => winner ?= @in_a_row(x, y)) | |
winner | |
in_a_row: (x, y) -> | |
player = @board.fields[x][y] | |
return null if !player | |
direction_sets = [[Direction.left, Direction.right], # horizontal | |
[Direction.up, Direction.down], # vertical | |
[Direction.up|Direction.left, Direction.down|Direction.right], # top left to bottom right | |
[Direction.up|Direction.right, Direction.down|Direction.left]] # top right to bottom left | |
# subtract one because the field itself is counted twice | |
(return player if (@board.row(player, x, y, set[0]) + @board.row(player, x, y, set[1]) - 1) >= @min_row) for set in direction_sets | |
null | |
action: (event) -> | |
super | |
try | |
@board.take(@current_player, @last_action.column, @last_action.row) | |
catch error | |
@onerror?("Could not play this field: #{error}") | |
return | |
winner = @winner() | |
if winner? | |
@win(winner) | |
return | |
if @board.full() | |
@end() | |
return | |
@next() | |
class TicTacToe extends InARowGame | |
constructor: (canvas) -> | |
super canvas, 3, 3, 3 | |
class Connect4 extends InARowGame | |
@board: Connect4Board | |
constructor: (canvas) -> | |
super canvas, 6, 7, 4 | |
###################################################################################################### | |
###################################################################################################### | |
update_ui = (i, text) -> | |
document.getElementById("instructor#{i}").innerHTML = text | |
setup_handlers = (i, game) -> | |
game.onstart = (player) -> update_ui(i, "New Game! Now playing: <em>#{player}</em>") | |
game.onnext = (old, player) -> update_ui(i, "Now playing: <em>#{player}</em>") | |
game.onend = -> update_ui(i, "Game over - no winner") | |
game.onwin = (winner) -> update_ui(i, "<em>#{winner}</em> won the game!") | |
game.onerror = (msg) -> update_ui(i, "<em><strong>Error:</strong></em> #{msg}") | |
tic = new TicTacToe(document.getElementById('tic-tac-toe')); | |
document.tictactoe.restart.onclick = (event) -> tic.restart() | |
four = new Connect4(document.getElementById('connect-4')) | |
document.connect4.restart.onclick = (event) -> four.restart() | |
setup_handlers(1, tic) | |
setup_handlers(2, four) | |
tic.start() | |
four.start() |
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
.board { | |
border: solid gray 1px; | |
float: left; | |
margin-right: 50px; | |
} | |
.instructor { | |
float: left; | |
width: 680px; | |
height: 380px; | |
border: solid gray 2px; | |
padding: 10px; | |
} | |
form { | |
clear: both | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment