Browse Source

Initial commit

master
Peter J. Jones 7 years ago
commit
7791979775

+ 17
- 0
.gitignore View File

@@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

+ 4
- 0
Gemfile View File

@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in freeplayd.gemspec
gemspec

+ 22
- 0
LICENSE View File

@@ -0,0 +1,22 @@
Copyright (c) 2012 Peter Jones

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 29
- 0
README.md View File

@@ -0,0 +1,29 @@
# Freeplayd

TODO: Write a gem description

## Installation

Add this line to your application's Gemfile:

gem 'freeplayd'

And then execute:

$ bundle

Or install it yourself as:

$ gem install freeplayd

## Usage

TODO: Write usage instructions here

## Contributing

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Added some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

+ 2
- 0
Rakefile View File

@@ -0,0 +1,2 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"

+ 74
- 0
bin/fpd View File

@@ -0,0 +1,74 @@
#!/usr/bin/env/ruby

################################################################################
require('freeplayd')
require('ostruct')
require('optparse')
require('logger')

################################################################################
class Configuration

##############################################################################
DEFAULT_OPTIONS = {
:server => Freeplayd::Servers::Single,
:port => 5678,
:digest => nil,
:board_size => 10,
:log_file => 'fpd.log',
}

##############################################################################
attr_reader(:options)

##############################################################################
def initialize
@options = OpenStruct.new(DEFAULT_OPTIONS)

OptionParser.new do |p|
p.banner = "Usage: fpd [options]"

p.on('-b', '--board-size=SIZE', 'Set the game board size to SIZE') do |b|
options.board_size = b.to_i
end

p.on('-d', '--digest=FILE', 'User names and digests') do |file|
options.digest = File.expand_path(file)
end

p.on('-h', '--help', 'This message') do
$stdout.puts(p)
exit(1)
end

p.on('--stdout', 'Use STDOUT instead of a log file') do
options.log_file = nil
end
end.parse(ARGV)

if options.digest.nil? or !File.exist?(options.digest)
raise("Whoa, you need to use --digest with a real file")
end
end
end

################################################################################
begin
config = Configuration.new.options
server = config.server

EventMachine.run do
Signal.trap("INT") { EventMachine.stop }
Signal.trap("TERM") { EventMachine.stop }

server.board_size = config.board_size
server.logger = Logger.new(config.log_file || $stdout)
server.auth = Freeplayd::Auth.new(config.digest)
server.config = config

EventMachine.start_server("0.0.0.0", config.port, config.server)
end
rescue RuntimeError => e
$stderr.puts(File.basename($0) + ": ERROR: #{e}")
exit(1)
end

+ 17
- 0
freeplayd.gemspec View File

@@ -0,0 +1,17 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/freeplayd/version', __FILE__)

Gem::Specification.new do |gem|
gem.authors = ["Peter Jones"]
gem.email = ["pjones@pmade.com"]
gem.description = %q{The Freeplay server}
gem.summary = %q{Simple server so clients can play the Freeplay game}
gem.homepage = "git://pmade.com/freeplay"

gem.files = `git ls-files`.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "freeplayd"
gem.require_paths = ["lib"]
gem.version = Freeplayd::VERSION
end

+ 16
- 0
lib/freeplayd.rb View File

@@ -0,0 +1,16 @@
################################################################################
require('freeplay')
require('eventmachine')
require('digest')
require('csv')

################################################################################
module Freeplayd
autoload('AI', 'freeplayd/ai')
autoload('Auth', 'freeplayd/auth')
autoload('Game', 'freeplayd/game')
autoload('RemotePlayer', 'freeplayd/remote_player')
autoload('Servers', 'freeplayd/server')
autoload('Spaces', 'freeplayd/spaces')
autoload('VERSION', 'freeplayd/version')
end

+ 48
- 0
lib/freeplayd/ai.rb View File

@@ -0,0 +1,48 @@
class Freeplayd::AI < Freeplay::Player

##############################################################################
def initialize (logger)
self.logger = logger
@random = Random.new
super()
end

##############################################################################
def name
"AI"
end

##############################################################################
def move
x, y = nil, nil

# First try to move to a space adjacent to my opponent's last move.
if board.last_opponent_move
logger.info("searching for an open adjacent space")
logger.info("looking near (#{board.last_opponent_move.join(',')})")

allowed = board.adjacent(*board.last_opponent_move)
open = allowed.select {|(ax, ay)| board[ax, ay] == :empty}

if !open.empty?
x, y = open[@random.rand(open.size)]
end
end

# If that didn't work just take the first available space.
if x.nil? or y.nil?
logger.info("searching for first available space")

x, y = catch(:found_empty_space) do
board.size.times do |bx|
board.size.times do |by|
throw(:found_empty_space, [bx, by]) if board[bx, by] == :empty
end
end
end
end

# Return the desired location on the board.
[x, y]
end
end

+ 36
- 0
lib/freeplayd/auth.rb View File

@@ -0,0 +1,36 @@
class Freeplayd::Auth

##############################################################################
def initialize (file)
@random = Random.new
@users = {}

CSV.foreach(file) do |row|
user_name = row[0].strip
full_name = row[1].strip
digest = row[2].strip

@users[user_name] = OpenStruct.new(
user_name: user_name,
full_name: full_name,
digest: digest)
end
end

##############################################################################
def nonce
Digest::SHA256.hexdigest(@random.bytes(512))
end

##############################################################################
def has_user? (user_name)
@users.has_key?(user_name)
end

##############################################################################
def authentic? (user_name, orig_nonce, reply)
return false unless has_user?(user_name)
digest = @users[user_name].digest
reply == Digest::SHA256.hexdigest(orig_nonce + digest)
end
end

+ 106
- 0
lib/freeplayd/game.rb View File

@@ -0,0 +1,106 @@
class Freeplayd::Game

##############################################################################
include(Freeplayd::Spaces)

##############################################################################
attr_reader(:winner)

##############################################################################
attr_reader(:score, :live)

##############################################################################
def initialize (white, black, size=10)
@white, @black, @size = white, black, size
@white.board = Freeplay::Board.new(:white, @size)
@black.board = Freeplay::Board.new(:black, @size)
@current_player = @white
@winner = nil
@score = {white: 0, black: 0}
@live = {white: [], black: []}
end

##############################################################################
def player_moved (player, x, y)
if player != @current_player
raise(Freeplay::Error, "players moved out of turn")
end

update_boards(x, y)
stop = game_over?
@score, @live = update_score
@winner = @score.max {|a,b| a.last <=> b.last}.first if stop
end

##############################################################################
private

##############################################################################
def update_boards (x, y)
@current_player.board.player_move(x, y)
@current_player = @current_player == @white ? @black : @white
@current_player.board.opponent_move(x, y)
end

##############################################################################
def update_score
score = {white: 0, black: 0}
live_stones = {white: [], black: []}

@size.times do |x|
@size.times do |y|
color = @white.board[x, y]
next if color == :empty

branches(x, y, @size) do |side_a, side_b|
live = score_for_spaces(side_a, color)
live += score_for_spaces(side_b, color)

if live == 3
score[color] += 1
live_stones[color] << [x, y]
end
end
end
end

live_stones.keys.each {|c| live[c] = live[c].sort.uniq}
[score, live_stones]
end

##############################################################################
def score_for_spaces (spaces, color)
spaces.take_while {|(x,y)| @white.board[x,y] == color}.size
end

##############################################################################
def game_over?
open_spaces = []

@size.times do |x|
@size.times do |y|
open_spaces << [x, y] if @white.board[x, y] == :empty
end
end

return true if open_spaces.size.zero?
return false if open_spaces.size > 1

# Only one remaining square, automatically move the current player
# into that space if it improves his score.
x, y = open_spaces.first[0], open_spaces.first[1]
player = @current_player.board.player

score_a, live_a = update_score

grid = @white.board.instance_variable_get(:@grid)
tx, ty = @white.board.send(:transform, x, y)
grid[tx][ty] = player

score_b, live_b = update_score
grid[tx][ty] = :empty

update_boards(x, y) if score_b[player] > score_a[player]
true
end
end

+ 2
- 0
lib/freeplayd/remote_player.rb View File

@@ -0,0 +1,2 @@
class Freeplayd::RemotePlayer < Freeplay::Player
end

+ 11
- 0
lib/freeplayd/server.rb View File

@@ -0,0 +1,11 @@
module Freeplayd::Servers

##############################################################################
SERVERS_DIR = File.expand_path('servers', File.dirname(__FILE__))

##############################################################################
Dir.foreach(SERVERS_DIR) do |file|
next if file.match(/^\./) or !file.match(/\.rb$/)
require(File.join(SERVERS_DIR, file))
end
end

+ 167
- 0
lib/freeplayd/servers/single.rb View File

@@ -0,0 +1,167 @@
class Freeplayd::Servers::Single < EM::Connection

##############################################################################
include(EM::Protocols::LineText2)

##############################################################################
COMMANDS = {
'authenticate' => :authenticate_player,
'nonce-reply' => :confirm_nonce_reply,
'move' => :move,
}

##############################################################################
def self.config= (config)
@@config = config
end

##############################################################################
def self.auth= (auth)
@@auth = auth
end

##############################################################################
def self.board_size= (size)
@@board_size = size
end

##############################################################################
def self.logger= (logger)
@@logger = logger
end

##############################################################################
def post_init
@authenticated = false
@playing = false
@user_name = nil
@nonce = nil
end

##############################################################################
def receive_line (data)
logger.info("<<< #{data}")
match = data.match(/^([^:]+):\s+(.+)$/)

if match and COMMANDS.has_key?(match[1])
send(COMMANDS[match[1]], match[2])
else
close_connection
end
end

##############################################################################
private

##############################################################################
def authenticate_player (user_name)
@user_name = user_name
@nonce = @@auth.nonce
send_line("nonce: #{@nonce}")
end

##############################################################################
def confirm_nonce_reply (reply)
if @@auth.authentic?(@user_name, @nonce, reply)
@authenticated = true

new_game
send_line("opponent: #{@ai.name}")
send_line("board: white #{@@board_size}")
send_line("move: none,none")
elsif !@@auth.has_user?(@user_name)
logger.error("missing user account for #{@user_name}")
send_line("not-authorized: there's no account for #{@user_name}")
close_connection_after_writing
else
logger.error("failed authentication for #{@user_name}")
send_line("not-authorized: authentication failed")
close_connection_after_writing
end
end

##############################################################################
def new_game
@player = Freeplayd::RemotePlayer.new
@ai = Freeplayd::AI.new(logger)
@game = Freeplayd::Game.new(@player, @ai, @@board_size)
@playing = true
end

##############################################################################
def move (coordinates)
match = coordinates.match(/^(\d+),(\d+)$/)
if !@authenticated
return player_error("please authenticate first")
elsif !@playing
return player_error("no game in progress")
elsif !match
return player_error("invalid move syntax")
end

x, y = match[1].to_i, match[2].to_i
move_player(@player, x, y) && move_player(@ai)
end

##############################################################################
# Tells the client who won, and which stones are live.
def winner (player)
score

send_line("game: #{player},#{format_live(:white)},#{format_live(:black)}")
close_connection_after_writing
end

##############################################################################
def move_player (player, x=nil, y=nil)
x, y = @ai.move if player == @ai
@game.player_moved(player, x||-1, y||-1)

if @game.winner
winner(@game.winner)
@playing = false
false
elsif player == @ai
score
send_line("move: #{x},#{y}")
true
else
true
end
rescue Freeplay::Error => e
message = player == @ai ? "whoa, AI failure: " : ""
return player_error(message + e.message)
end

##############################################################################
def score
send_line("score: #{@game.score[:white]},#{@game.score[:black]}")
end

##############################################################################
def logger
@@logger
end

##############################################################################
def send_line (message)
clean = message.gsub(/\n+/, ' ')
logger.info(">>> #{message}")
send_data(clean + "\n")
end

##############################################################################
def format_live (color)
@game.live[color].map {|(x,y)| "#{x} #{y}"}.join(";")
end

##############################################################################
def player_error (message)
@playing = false
send_line("quit: #{message}")
close_connection_after_writing
false
end
end

+ 24
- 0
lib/freeplayd/spaces.rb View File

@@ -0,0 +1,24 @@
module Freeplayd::Spaces

##############################################################################
BRANCH_TRANSFORMS = {
:vertical => [[0, 1], [0, -1]],
:horizontal => [[-1, 0], [1, 0]],
:ne_sw_diag => [[1, 1], [-1, -1]],
:nw_se_diag => [[-1, 1], [1, -1]],
}

##############################################################################
def branches (x, y, size, &block)
BRANCH_TRANSFORMS.values.each do |set|
transformed = set.map do |coords|
tx, ty = x, y
collector = []
4.times {collector << [tx += coords[0], ty += coords[1]]}
collector.select {|t| t.all? {|p| p >= 0 && p < size}}
end

block.call(*transformed)
end
end
end

+ 3
- 0
lib/freeplayd/version.rb View File

@@ -0,0 +1,3 @@
module Freeplayd
VERSION = "0.0.1"
end

Loading…
Cancel
Save