Skip to content

Instantly share code, notes, and snippets.

@maul-esel
Last active December 17, 2015 11:29
Show Gist options
  • Save maul-esel/5602514 to your computer and use it in GitHub Desktop.
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.
<!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>
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()
.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