diff --git a/.gitignore b/.gitignore index 560d1a6..ef2202f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.db *.gem *.rbc .bundle @@ -16,3 +17,6 @@ tmp .yardoc _yardoc doc/ +oauth/ +db/connectfour.db +oauth_test \ No newline at end of file diff --git a/.schema b/.schema new file mode 100644 index 0000000..e69de29 diff --git a/Gemfile b/Gemfile index e69de29..ce8f808 100644 --- a/Gemfile +++ b/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' +gem 'rspec' +gem 'sqlite3' +gem 'tweetstream' +gem 'twitter' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 83f331d..3ed621a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,54 @@ GEM + remote: https://rubygems.org/ specs: + addressable (2.3.2) + cookiejar (0.3.0) + daemons (1.1.9) + diff-lcs (1.1.3) + em-http-request (1.0.3) + addressable (>= 2.2.3) + cookiejar + em-socksify + eventmachine (>= 1.0.0.beta.4) + http_parser.rb (>= 0.5.3) + em-socksify (0.2.1) + eventmachine (>= 1.0.0.beta.4) + em-twitter (0.2.1) + eventmachine (~> 1.0) + http_parser.rb (~> 0.5) + simple_oauth (~> 0.1) + eventmachine (1.0.0) + faraday (0.8.4) + multipart-post (~> 1.1) + http_parser.rb (0.5.3) + multi_json (1.3.6) + multipart-post (1.1.5) + rspec (2.11.0) + rspec-core (~> 2.11.0) + rspec-expectations (~> 2.11.0) + rspec-mocks (~> 2.11.0) + rspec-core (2.11.1) + rspec-expectations (2.11.3) + diff-lcs (~> 1.1.3) + rspec-mocks (2.11.3) + simple_oauth (0.1.9) + sqlite3 (1.3.6) + tweetstream (2.3.0) + daemons (~> 1.1) + em-http-request (~> 1.0.2) + em-twitter (~> 0.2) + multi_json (~> 1.3) + twitter (~> 4.0) + twitter (4.2.0) + faraday (~> 0.8) + multi_json (~> 1.3) + simple_oauth (~> 0.1.6) PLATFORMS ruby DEPENDENCIES + rspec + sqlite3 + tweetstream + twitter diff --git a/bin/connect_four b/bin/connect_four old mode 100644 new mode 100755 index e1d4259..e1399af --- a/bin/connect_four +++ b/bin/connect_four @@ -1 +1,5 @@ -#!/usr/bin/ruby \ No newline at end of file +#!/usr/bin/env ruby + +require_relative '../lib/connect_four' + +UI.start diff --git a/connect4/board.rb b/connect4/board.rb new file mode 100644 index 0000000..12c1d18 --- /dev/null +++ b/connect4/board.rb @@ -0,0 +1,138 @@ +class Board + attr_reader :col_num, :row_num + attr_accessor :cells + + def initialize(col_num = 7, row_num = 6) + @cells = Array.new(col_num * row_num, "") + @col_num = col_num + @row_num = row_num + end + + def empty? + cells.all? { |cell| cell.empty? } + end + + def empty_cells + cells.select {|cell| cell == "" }.size + end + + def place_piece(column, piece) + position = (col_num * (row_num - 1)) + (column - 1) + until cells[position].empty? + position -= col_num + end + cells[position] = piece + end + + def full? + cells.all? {|cell| cell != "" } + end + + def rows + cells.each_slice(col_num).collect { |row| row } + end + + def columns + columns_array = Array.new(col_num) { Array.new([]) } + cells.each_with_index do |cell, i| + columns_array[i % col_num] << cell + end + columns_array + end + + def get_start_indexes_to_right(win_no) + container = [] + (col_num - (win_no-1)).times do |i| + container << i + end + (row_num - (win_no-1)).times do |i| + container << i*col_num unless i == 0 + end + container + end + + def get_start_indexes_to_left(win_no) + container = [] + (col_num - win_no+1).times do |i| + container << i + (win_no-1) + end + (row_num - (win_no-1)).times do |i| + container << (i*col_num + col_num-1) unless i == 0 + end + container + end + + def row_index_number(index) + (index / col_num).floor + end + + def column_index_number(index) + index % col_num + end + + def diagonal_indexes_to_right + start_indexes = get_start_indexes_to_right(4) + leap = col_num + 1 + start_indexes.map do |index| + diagonal = [] + while true + diagonal << index + index += leap + row, column = row_index_number(index), column_index_number(index) + break if row == row_num || column == 0 + end + diagonal + end + end + + def diagonal_indexes_to_left + start_indexes = get_start_indexes_to_left(4) + leap = col_num - 1 + start_indexes.map do |index| + diagonal = [] + while true + diagonal << index + index += leap + row, column = row_index_number(index), column_index_number(index) + break if row == row_num || column == col_num-1 + end + diagonal + end + end + + # def diagonal_indexes(direction) + # start_indexes = get_start_indexes_to_left(4) + # leap = col_num + 1 if direction == "to_right" + # leap = col_num - 1 if direction == "to_left" + # start_indexes.map do |index| + # diagonal = [] + # while true + # diagonal << index + # index += leap + # row, column = row_index_number(index), column_index_number(index) + # break if (row == row_num || column == 0) && direction == "to_right" + # break if (row == row_num || column == col_num-1) && direction == "to_left" + # end + # diagonal + # end + # end + + def diagonals + (diagonal_indexes_to_right + diagonal_indexes_to_left).map {|diagonal| diagonal.map { |i| i = cells[i]}} + # (diagonal_indexes("to_right") + diagonal_indexes("to_left")).map {|diagonal| diagonal.map { |i| i = cells[i]}} + end + + + def check_four_consecutive? + # (columns + rows + diagonals).any? { |group| group.four_consecutive? } + (columns + rows + diagonals).any? { |lines| four_consecutive?(lines) } + end + + def four_consecutive?(line) + line.each_cons(4) do |element| + return true if (element.uniq.size == 1 && element.uniq[0] != "") + end + false + end + +end \ No newline at end of file diff --git a/connect4/defense.rb b/connect4/defense.rb new file mode 100644 index 0000000..6370fa7 --- /dev/null +++ b/connect4/defense.rb @@ -0,0 +1,117 @@ +require_relative 'board' + +class Defense + attr_reader :board, :positions_ratings + + def initialize + @board = Board.new + @positions_ratings = { 1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0, 7=>0} + end + + def available_moves + @available_moves = [] + first_row = [35, 36, 37, 38, 39, 40, 41] + first_row.each do |position| + until board.cells[position].empty? + position -= board.col_num + end + @available_moves << position + end + return @available_moves + end + + def rate_all_cells + available_moves.each do |move| + cell_rating_left(move) + cell_rating_right(move) + cell_rating_bottom(move) + end + positions_ratings + end + + def cell_rating_left(position) + left_cell = board.cells[position-1] + unless [35, 28, 21, 14, 7, 0].include?(position) + if left_cell == "red" + positions_ratings[(position%7)+1] += 2 + cell_rating_left2(position) unless [36, 29, 22, 15, 8, 1].include?(position) + end + end + positions_ratings[position%7+1] + end + + def cell_rating_left2(position) + left_cell2 = board.cells[position-2] + if left_cell2 == "red" + positions_ratings[position%7+1] += 4 + cell_rating_left3(position) unless [37, 30, 23, 16, 9, 2].include?(position) + end + end + + def cell_rating_left3(position) + left_cell3 = board.cells[position-3] + if left_cell3 == "red" + positions_ratings[position%7+1] += 8 + end + end + + def cell_rating_right(position) + right_cell = board.cells[position+1] + unless [41, 34, 27, 20, 13, 6].include?(position) + if right_cell == "red" + positions_ratings[position%7+1] += 2 + cell_rating_right2(position) unless [40, 33, 26, 19, 12, 5].include?(position) + end + end + positions_ratings[position%7+1] + end + + def cell_rating_right2(position) + right_cell2 = board.cells[position+2] + if right_cell2 == "red" + positions_ratings[position%7+1] += 4 + cell_rating_right3(position) unless [39, 32, 25, 18, 11, 4].include?(position) + end + end + + def cell_rating_right3(position) + right_cell3 = board.cells[position+3] + if right_cell3 == "red" + positions_ratings[position%7+1] += 8 + end + end + + def cell_rating_bottom(position) + unless [35, 36, 37, 38, 39, 40, 41].include?(position) + bottom_cell = board.cells[position+7] + if bottom_cell == "red" + positions_ratings[position%7+1] += 2 + cell_rating_bottom2(position) unless [28, 29, 30, 31, 32, 33, 34].include?(position) + end + end + positions_ratings[position%7+1] + end + + def cell_rating_bottom2(position) + bottom_cell2 = board.cells[position+14] + if bottom_cell2 == "red" + positions_ratings[position%7+1] += 4 + cell_rating_bottom3(position) unless [22, 23, 24, 25, 26, 27].include?(position) + end + end + + def cell_rating_bottom3(position) + bottom_cell3 = board.cells[position+21] + if bottom_cell3 == "red" + positions_ratings[position%7+1] += 8 + end + end + + def move + rate_all_cells + play_column = positions_ratings.max_by {|k, v| v}.first.to_i + board.place_piece(play_column, "black") + positions_ratings.each {|k,v| positions_ratings[k] = 0} + end + +end \ No newline at end of file diff --git a/connect4/defense_spec.rb b/connect4/defense_spec.rb new file mode 100644 index 0000000..25405c7 --- /dev/null +++ b/connect4/defense_spec.rb @@ -0,0 +1,120 @@ +require_relative 'defense.rb' + +describe Defense do + + describe '.new' do + it "should get a new board" do + Defense.new.board.should be_true + end + end + + describe "#avaiable_move" do + it "should give all available cells" do + deff = Defense.new + deff.board.place_piece(1, "red") + deff.available_moves.should eq [28, 36, 37, 38, 39, 40, 41] + end + end + + describe "#cell_rating" do + it "should rate 0 a lonely cell" do + deff = Defense.new + deff.cell_rating_left(36).should eq 0 + end + + it "should rate 2 a cell with one pieces on the left" do + deff = Defense.new + deff.board.place_piece(2, "red") + deff.cell_rating_left(37).should eq 2 + end + + it "should rate 6 a cell with 2 pieces on the left" do + deff = Defense.new + deff.board.place_piece(2, "red") + deff.board.place_piece(1, "red") + deff.cell_rating_left(37).should eq 6 + end + + it "should rate 14 a cell with 2 pieces on the left" do + deff = Defense.new + deff.board.place_piece(2, "red") + deff.board.place_piece(3, "red") + deff.board.place_piece(1, "red") + deff.cell_rating_left(38).should eq 14 + end + + it "should rate 2 a cell with one pieces on the right" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.cell_rating_right(40).should eq 2 + end + + it "should rate 6 a cell with 2 pieces on the right" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.board.place_piece(6, "red") + deff.cell_rating_right(39).should eq 6 + end + + it "should rate 14 a cell with 3 pieces on the right" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.board.place_piece(6, "red") + deff.board.place_piece(5, "red") + deff.cell_rating_right(38).should eq 14 + end + + it "should rate 2 a cell with 1 pieces on the bottom" do + deff = Defense.new + deff.board.place_piece(4, "red") + deff.cell_rating_bottom(31).should eq 2 + end + + it "should rate 6 a cell with 2 pieces on the bottom" do + deff = Defense.new + deff.board.place_piece(4, "red") + deff.board.place_piece(4, "red") + deff.cell_rating_bottom(24).should eq 6 + end + + it "should rate 14 a cell with 3 pieces on the bottom" do + deff = Defense.new + deff.board.place_piece(4, "red") + deff.board.place_piece(4, "red") + deff.board.place_piece(4, "red") + deff.cell_rating_bottom(17).should eq 14 + end + + it "should not rate a friend-neighbor cell" do + deff = Defense.new + deff.board.place_piece(4, "black") + deff.cell_rating_bottom(31).should eq 0 + end + + it "should rate all the available moves at once" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.rate_all_cells.should eq ({1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>2, 7=>2}) + end + end + + describe "#play" do + it "should play the highest rated move" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.board.place_piece(7, "red") + deff.board.place_piece(7, "red") + deff.move + deff.board.cells.should eq ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "black", "", "", "", "", "", "", "red", "", "", "", "", "", "", "red", "", "", "", "", "", "", "red"] + end + + it "should clear the grades hash" do + deff = Defense.new + deff.board.place_piece(7, "red") + deff.move + expect { deff.positions_ratings.all? {|k,v| positions_ratings[k] == 0} }.to be_true + end + end + + +end \ No newline at end of file diff --git a/database.rb b/database.rb new file mode 100644 index 0000000..e69de29 diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..5211a9e --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR NOT NULL, + twitter VARCHAR NOT NULL UNIQUE, + password VARCHAR NOT NULL, + created_at DEFAULT current_timestamp, + updated_at DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS games ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player1 INTEGER NOT NULL, + player2 INTEGER NOT NULL, + winner INTEGER, + created_at DEFAULT current_timestamp, + updated_at DEFAULT current_timestamp, + FOREIGN KEY (player1) REFERENCES Players(id), + FOREIGN KEY (player2) REFERENCES Players(id) +); \ No newline at end of file diff --git a/db/test.rb b/db/test.rb new file mode 100644 index 0000000..c6dfb6e Binary files /dev/null and b/db/test.rb differ diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000..b24079a Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/connect_four.rb b/lib/connect_four.rb new file mode 100644 index 0000000..0f158dc --- /dev/null +++ b/lib/connect_four.rb @@ -0,0 +1,7 @@ +require_relative 'connect_four/database' +require_relative 'connect_four/board' +require_relative 'connect_four/game' +require_relative 'connect_four/player' +require_relative 'connect_four/ui' +require_relative 'connect_four/computer_player' +require_relative 'connect_four/twitter_player' \ No newline at end of file diff --git a/lib/connect_four/.DS_Store b/lib/connect_four/.DS_Store new file mode 100644 index 0000000..a030489 Binary files /dev/null and b/lib/connect_four/.DS_Store differ diff --git a/lib/connect_four/board.rb b/lib/connect_four/board.rb new file mode 100644 index 0000000..82d6edd --- /dev/null +++ b/lib/connect_four/board.rb @@ -0,0 +1,127 @@ +class Board + attr_reader :col_num, :row_num + attr_accessor :cells + + def initialize(col_num = 7, row_num = 6) + @cells = Array.new(col_num * row_num, "") + @col_num = col_num + @row_num = row_num + end + + def empty? + cells.all? { |cell| cell.empty? } + end + + def empty_cells + cells.select {|cell| cell == "" }.size + end + + def place_piece(column, piece) + if column > col_num + puts "That column number is too big. Choose again." + UI.game.play + elsif column == "".to_i + puts "This is an invalid input." + UI.game.play + else + position = (col_num * (row_num - 1)) + (column - 1) + end + until cells[position].empty? + if position < 0 + puts "That column is full. Choose again." + UI.game.play + break + else + position -= col_num + end + end + cells[position] = piece + end + + def to_s + board_format, row_format = "|", "" + cells.each { |field| field == "" ? (row_format += ".") : (row_format += field) } + row_num.times do |i| + start_i, end_i = (col_num*i), (col_num*(i+1)) + board_format += row_format[start_i...end_i] + "|" + end + board_format + end + + def full? + cells.all? {|cell| cell != "" } + end + + def rows + cells.each_slice(col_num).collect { |row| row } + end + + def columns + columns_array = Array.new(col_num) { Array.new([]) } + cells.each_with_index do |cell, i| + columns_array[i % col_num] << cell + end + columns_array + end + + def get_start_indexes_to_right(win_no) + container = [] + (col_num - (win_no-1)).times do |i| + container << i + end + (row_num - (win_no-1)).times do |i| + container << i*col_num unless i == 0 + end + container + end + + def get_start_indexes_to_left(win_no) + container = [] + (col_num - win_no+1).times do |i| + container << i + (win_no-1) + end + (row_num - (win_no-1)).times do |i| + container << (i*col_num + col_num-1) unless i == 0 + end + container + end + + def row_index_number(index) + (index / col_num).floor + end + + def column_index_number(index) + index % col_num + end + + def diagonal_indexes(leap_no, end_no, start_indexes) + leap = col_num + leap_no + start_indexes.map do |index| + diagonal = [] + while true + diagonal << index + index += leap + row, column = row_index_number(index), column_index_number(index) + break if row == row_num || column == end_no + end + diagonal + end + end + + def diagonals + (diagonal_indexes(1, 0, get_start_indexes_to_right(4)) + diagonal_indexes(-1, col_num-1, get_start_indexes_to_left(4))).map {|diagonal| diagonal.map { |i| i = cells[i]}} + end + + def check_four_consecutive? + (columns + rows + diagonals).any? { |lines| four_consecutive?(lines) } + end + + def four_consecutive?(line) + line.each_cons(4) do |element| + return true if (element.uniq.size == 1 && element.uniq[0] != "") + end + false + end + +end + diff --git a/lib/connect_four/computer_player.rb b/lib/connect_four/computer_player.rb new file mode 100644 index 0000000..779970e --- /dev/null +++ b/lib/connect_four/computer_player.rb @@ -0,0 +1,109 @@ +class ComputerPlayer + + attr_reader :name, :twitter, :password, :id, :piece, :positions_ratings, + :opponent_piece + + def initialize(params = {}) + @name = params[:name] + @piece = params[:piece] + @positions_ratings = { 1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0, 7=>0} + @piece == "X" ? @opponent_piece = "O" : @opponent_piece = "X" + end + + # def move + # rand(7) + 1 + # end + + def available_moves + @available_moves = [] + first_row = [35, 36, 37, 38, 39, 40, 41] + first_row.each do |position| + until UI.game.board.cells[position].empty? || position < 0 + position -= 7 + end + @available_moves << position unless position < 0 + end + return @available_moves + end + + def rate_all_cells + available_moves.each do |move| + cell_rating_left(move) + cell_rating_right(move) + cell_rating_bottom(move) + end + positions_ratings + end + + def cell_rating_left(position) + left_cell = UI.game.board.cells[position - 1] + unless [35, 28, 21, 14, 7, 0].include?(position) + if left_cell == opponent_piece + positions_ratings[position%7 + 1] += 2 + cell_rating_left2(position) unless [36, 29, 22, 15, 8, 1].include?(position) + else + positions_ratings[position%7+1] ||= 0 + end + end + end + + def cell_rating_left2(position) + left_cell2 = UI.game.board.cells[position-2] + if left_cell2 == opponent_piece + positions_ratings[position%7+1] += 4 + cell_rating_left_3(position) unless [37, 30, 23, 16, 9, 2].include?(position) + end + end + + def cell_rating_left_3(position) + left_cell3 = UI.game.board.cells[position-3] + if left_cell3 == opponent_piece + positions_ratings[position%7+1] += 8 + end + end + + def cell_rating_right(position) + if position % 7 < 6 + right_cell = UI.game.board.cells[position+1] + if right_cell == opponent_piece + positions_ratings[position%7+1] = 2 + else + positions_ratings[position%7+1] ||= 0 + end + end + end + + def cell_rating_bottom(position) + unless [35, 36, 37, 38, 39, 40, 41].include?(position) + bottom_cell = UI.game.board.cells[position+7] + if bottom_cell == opponent_piece + positions_ratings[position%7+1] += 2 + cell_rating_bottom2(position) unless [28, 29, 30, 31, 32, 33, 34].include?(position) + end + end + positions_ratings[position%7+1] + end + + def cell_rating_bottom2(position) + bottom_cell2 = UI.game.board.cells[position+14] + if bottom_cell2 == opponent_piece + positions_ratings[position%7+1] += 4 + cell_rating_bottom3(position) unless [22, 23, 24, 25, 26, 27].include?(position) + end + end + + def cell_rating_bottom3(position) + bottom_cell3 = UI.game.board.cells[position+21] + if bottom_cell3 == opponent_piece + positions_ratings[position%7+1] += 8 + end + end + + def move + rate_all_cells + play_column = positions_ratings.max_by {|k, v| v}.first.to_i + positions_ratings = { 1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0, 7=>0} + return play_column + end + +end \ No newline at end of file diff --git a/lib/connect_four/database.rb b/lib/connect_four/database.rb new file mode 100644 index 0000000..cd78d84 --- /dev/null +++ b/lib/connect_four/database.rb @@ -0,0 +1,27 @@ +require 'sqlite3' + +class DB + attr_reader :file_path + def self.create(name = "connectfour.db") + @file_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'db', name)) + + unless File.exists?(@file_path) + system("sqlite3 #{@file_path} < ./db/schema.sql") + end + + SQLite3::Database.new(@file_path) + end + + def self.handler(string, *values) + db = SQLite3::Database.new(@file_path) + result = db.execute(string, *values.flatten) + db.close + result + end + + def self.check_twitter(player_name, twitter) + authorisation = handler("SELECT name, twitter FROM players WHERE twitter = ?;", twitter).flatten + authorisation[0] == player_name || authorisation.empty? + end + +end \ No newline at end of file diff --git a/lib/connect_four/game.rb b/lib/connect_four/game.rb new file mode 100644 index 0000000..f100c83 --- /dev/null +++ b/lib/connect_four/game.rb @@ -0,0 +1,50 @@ +class Game + attr_reader :player1, :player2, :winner, :board, :players + + def initialize(player1, player2) + @player1 = player1 + @player2 = player2 + @players = [@player1, @player2] + @board = Board.new + end + + def play + until over? + board.place_piece(current_player.move, current_player.piece) + toggle_player unless over? + UI.board_display unless over? + end + if board.full? + winner = nil + save(0) + else + winner = current_player + (winner.name == "MCP" || winner.class == TwitterPlayer) ? save(0) : save(winner.id) + end + UI.congratulations(winner) + UI.start("What do you want to do now?") + end + + def toggle_player + @players.rotate! + end + + def current_player + @players.first + end + + def over? + board.full? || board.check_four_consecutive? + end + + def save(winner_id) + if @player1.name == "MCP" || @player1.class == TwitterPlayer + values = [0, @player2.id, winner_id] + else + values = [@player1.id, @player2.id, winner_id] + end + DB.handler("INSERT INTO games (player1, player2, winner) VALUES (?, ?, ?);", values) + end + +end + diff --git a/lib/connect_four/player.rb b/lib/connect_four/player.rb new file mode 100644 index 0000000..ebb13a8 --- /dev/null +++ b/lib/connect_four/player.rb @@ -0,0 +1,43 @@ +class Player + attr_reader :name, :twitter, :password, :piece + + def initialize(params = {}) + @name = params[:name] + @twitter = params[:twitter] + @password = params[:password] + @piece = params[:piece] + end + + def save + values = [name, twitter, password] + if DB.handler("SELECT * FROM players WHERE twitter = ?", twitter).empty? + DB.handler("INSERT INTO players (name, twitter, password) VALUES (?, ?, ?);", values) + end + end + + def id + @id = DB.handler("SELECT id FROM players WHERE twitter = ?;", twitter).flatten.join.to_i + end + + def move + puts "-" * 50 + print "#{name}, what column do you want to play in? " + gets.chomp.to_i + end + + def to_s + name + end + + def wins + DB.handler("SELECT COUNT(*) FROM games WHERE winner = ?", id).flatten.join + end + + def losses + DB.handler("SELECT COUNT(*) FROM games WHERE (player1 = ? OR player2 = ?) AND winner != ? AND winner != 0", Array.new(3, id)).flatten.join + end + + def ties + DB.handler("SELECT COUNT(*) FROM games WHERE (player1 = ? OR player2 = ?) AND winner = 0", Array.new(2, id)).flatten.join + end +end \ No newline at end of file diff --git a/lib/connect_four/twitter_player.rb b/lib/connect_four/twitter_player.rb new file mode 100644 index 0000000..c09c4ab --- /dev/null +++ b/lib/connect_four/twitter_player.rb @@ -0,0 +1,82 @@ +require 'tweetstream' + +class TwitterPlayer + attr_reader :name, :twitter, :piece, :random_tag, :id + + def initialize(options={}, tag) + @name = options[:name] + @twitter = options[:twitter] + @piece = options[:piece] + @id = options[:id] + @random_tag = tag + end + + def self.from_twitter + @random_tag = (('a'..'z').to_a + ('A'..'Z').to_a + (1..9).to_a).shuffle[0..2].join + @file_path = File.expand_path(File.join(File.dirname(__FILE__) , 'oauth'))#, '..', '..', 'db', name)) + consumer_key_path = File.join(@file_path, "consumer_key.txt") + consumer_secret_path = File.join(@file_path, "consumer_secret.txt") + oauth_token_path = File.join(@file_path, "access_token.txt") + oauth_token_secret_path = File.join(@file_path, "access_token_secret.txt") + + Twitter.configure do |config| + config.consumer_key = File.read(consumer_key_path) + config.consumer_secret = File.read(consumer_secret_path) + config.oauth_token = File.read(oauth_token_path) + config.oauth_token_secret = File.read(oauth_token_secret_path) + end + + TweetStream.configure do |config| + config.consumer_key = File.read(consumer_key_path) + config.consumer_secret = File.read(consumer_secret_path) + config.oauth_token = File.read(oauth_token_path) + config.oauth_token_secret = File.read(oauth_token_secret_path) + config.auth_method = :oauth + end + + puts "...waiting for player" + TweetStream::Client.new.track('#dbc_c4') do |status, client| + #puts "id = #{status.user[:id]}, text = #{status.text}" + text = status.text.gsub(/#.*/, "").strip + if text == "Who wants to get demolished?" + client.stop + puts "@#{status.user[:screen_name]} Game on! #dbc_c4 #{@random_tag}" + Twitter.update("@#{status.user[:screen_name]} Game on! #dbc_c4 #{@random_tag}") + return TwitterPlayer.new({name: status.user[:name], twitter: status.user[:screen_name], piece: 'O', id: status.user[:id]}, @random_tag) + end + end + end + + def move + board = to_a(board_as_string) + column = parsed_board(board) + return column + end + + def board_as_string + TweetStream::Client.new.follow("#{@id}") do |status, client| + raw_board = status.text.gsub!(/#.*/, "") + board = raw_board.match(/[^@\w*](.*)/).to_s.strip + client.stop + return board + end + end + + def parsed_board(board_as_array) + result = nil + board_as_array + board_as_array.each_with_index do |cell, index| + if cell != UI.game.board.cells[index] + result = (index % 7) + 1 + end + end + return result + end + + def to_a(move_string) + board_info = move_string.gsub(/\|/, "").split("") + board_info.each { |field| field.gsub!(/\./, "") } + board_info + end + +end \ No newline at end of file diff --git a/lib/connect_four/ui.rb b/lib/connect_four/ui.rb new file mode 100644 index 0000000..8bb27c1 --- /dev/null +++ b/lib/connect_four/ui.rb @@ -0,0 +1,176 @@ +require 'io/console' +require './database.rb' +class UI + attr_reader :game, :tweet + + def self.start(start_message = "Welcome to Connect Four! Pick a number") + DB.create + puts "=" * 100 + puts start_message + puts "1 - (1 vs 1), 2 - (1 vs PC), 3 - (1 vs Twitter), 4 - (PC vs Twitter), " + puts "s - (Statistic), r - (repeat game), q - (quit)" + puts "=" * 100 + choices(gets.chomp) + puts "=" * 50 + puts "THE GAME IS ON:" + @game = Game.new(@player1, @player2) + @game.play + end + + def self.game + @game + end + + def self.choices(user_input) + case user_input + when "1" + puts "Good choice!" + create_1vs1_player + when "2" + puts "How dare you! I will demolish you!" + create_1vsPC_player + when "3" + puts "I will send out the message pigeon!" + create_1vsTwitter_player + when "4" + puts "Game on!" + create_PCvsTwitter_player + when "s" + show_user_statistic + when "r" + return + when "q" + puts "=" * 100 + puts "Auf Wiedersehen!" + exit + else + start("Sorry, playing against Queen Elizabeth is not an option! Try again.") + end + end + + def self.create_1vs1_player + @player1 = Player.new(create_player("Player O").merge(:piece => 'O')) + @player1.save + @player2 = Player.new(create_player("Player X").merge(:piece => 'X')) + @player2.save + end + + def self.create_1vsPC_player + @player1 = ComputerPlayer.new({name: "MCP", piece: 'O'}) + @player2 = Player.new(create_player("Player X").merge(:piece => 'X')) + @player2.save + end + + def self.create_1vsTwitter_player + @player2 = Player.new(create_player("Player X").merge(:piece => 'X')) + @player1 = TwitterPlayer.from_twitter + end + + def self.create_PCvsTwitter_player + @player2 = ComputerPlayer.new({name: "MCP", piece: 'X'}) + @player1 = TwitterPlayer.from_twitter + end + + def self.show_user_statistic + @player = Player.new(create_player("Player")) + puts "STATISTIC for #{@player}" + puts "=" * 50 + puts "WINS: #{@player.wins}" + puts "LOSSES: #{@player.losses}" + puts "TIES: #{@player.ties}" + start("What do you want to do now?") + end + + def self.board_display + if (game.current_player == @player1) && (@player1.class == TwitterPlayer) + puts "Tweeted:" + print_board + tweet_board(game.board.to_s, twitter_tag) + else + print_board + end + end + + def self.tweet_board(tweet, message) + #puts "@#{@player1.twitter} #{tweet} #{message} #{@player1.random_tag}" + Twitter.update("@#{@player1.twitter} #{tweet} #{message} #{@player1.random_tag}") + end + + def self.create_player(player) + player_name = get_player_name(player) + { name: player_name, + twitter: unique_twitter_account(player_name, player), + password: secure_password + } + end + + def self.get_player_name(player) + puts "=" * 50 + puts "Enter username for #{player}" + gets.chomp.capitalize + end + + def self.unique_twitter_account(player_name,player) + puts "Enter your twitter account" + twitter = gets.chomp + if valid_twitter?(player_name, twitter) + return twitter + else + puts "Invalid player name or twitter account" + create_player(player) + end + end + + def self.valid_twitter?(player_name, twitter) + DB.check_twitter(player_name, twitter) + end + + def self.secure_password + puts "Enter your password" + pwd = STDIN.noecho {|i| i.gets}.chomp + valid_password?(pwd) + end + + def self.valid_password?(pwd) + unless pwd.empty? # && pwd.length > 6 + return pwd + else + puts "Password too short" + secure_password + end + end + + def self.twitter_tag + if game.board.full? + message = "Draw game. Play again? #dbc_c4" + elsif game.board.check_four_consecutive? + game.board.empty_cells.even? ? message = "I win! Good game. #dbc_c4" : message = "You win. #dbc_c4" + else + message = '#dbc_c4' + end + end + + def self.congratulations(player) + if @player1.class == TwitterPlayer + tag = twitter_tag + tweet_board(game.board.to_s, tag) + else + print_board + if player == nil + puts "Draw." + elsif player.name == "MCP" + puts "You lose, loser." + else + puts "Congratulations #{player.name}. You are a real winner." + end + end + end + + def self.print_board + board_array = game.board.to_s.split('|') + board_array.each do |row| + puts "|#{row}|" unless row == "" + end + end + +end \ No newline at end of file diff --git a/spec/board_spec.rb b/spec/board_spec.rb new file mode 100644 index 0000000..c00bda5 --- /dev/null +++ b/spec/board_spec.rb @@ -0,0 +1,278 @@ +require 'spec_helper' + +describe Board do + + let(:board) { Board.new } + + def fake_row_array(index) + arr = [] + board.col_num.times do |i| + arr << i + (index * (board.row_num + 1)) + end + arr + end + + def fake_column_array(index) + arr = [] + board.row_num.times do |i| + arr << (i * board.col_num) + index + end + arr + end + + context "#initialize" do + it "should exist" do + board.should_not be_nil + end + + it "accepts a number of columns" do + board.col_num.should eq 7 + end + + it "accepts a number of rows" do + board.row_num.should eq 6 + end + end + + context "#empty?" do + it "initialized board is empty" do + board.empty?.should be_true + end + end + + context "#empty_cells" do + it "should have all cells empty in the initialization" do + board.empty_cells.should eq(board.col_num * board.row_num) + end + it "should change the value if one cell is filled" do + board.place_piece(1, "anything") + board.empty_cells.should eq((board.col_num * board.row_num) - 1) + end + end + + context "#place_piece" do + + it "places a piece on the board" do + board.place_piece(1, "anything") + board.should_not be_empty + end + + it "places the piece in the correct position" do + board.place_piece(3, "anything") + board.cells[37].should_not be_empty + end + + it "places the correct colored piece on the board" do + board.place_piece(1, "Striped") + board.cells[35].should eq "Striped" + end + + it "doesn't put a piece in an occupied cell" do + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.cells[21].should eq "Striped" + end + + end + + context "#full?" do + it "returns true if the board is full" do + board.cells.map! { |cell| cell = "tooth fairy"} + board.should be_full + end + + it "returns false of the board is not full" do + board.should_not be_full + end + + end + + context "#rows" do + it "has row_num rows" do # TODO: better way for testing this???? + board.rows.length.should eq(board.row_num) + end + end + + context "#columns" do + it "has col_num columns" do # TODO: better way for testing this???? + board.columns.length.should eq(board.col_num) + end + end + + context "#get_start_indexes_to_right" do + it "should have unique index numbers" do + board.get_start_indexes_to_right(4).uniq.length.should eq(board.get_start_indexes_to_right(4).length) + end + it "should only include numbers from the first row and the first column" do + board.get_start_indexes_to_right(4).should eq ([0,1,2,3,7,14]) + end + it "should not include any other numbers than the ones from the first row and the first column" do + board.cells[8..13].each do |num| + board.get_start_indexes_to_right(4).should_not include(num) + end + board.cells[15..41].each do |num| + board.get_start_indexes_to_right(4).should_not include(num) + end + end + it "should not have index numbers bigger than rows*columns" do + s_no = board.col_num * board.row_num + e_no = s_no + 10 + board.cells[s_no..e_no].each do |num| + board.get_start_indexes_to_right(4).should_not include(num) + end + end + it "should not include the numbers of the first row bigger than 4" do + s_no = board.col_num - 4 + e_no = board.col_num - 1 + board.cells[s_no..e_no].each do |num| + board.get_start_indexes_to_right(4).should_not include(num) + end + end + + end + + context "#get_start_indexes_to_left" do + it "should have unique index numbers" do + board.get_start_indexes_to_left(4).uniq.length.should eq(board.get_start_indexes_to_right(4).length) + end + it "should only include numbers from the first row and the last column" do + board.get_start_indexes_to_left(4).should eq ([3,4,5,6,13,20]) + end + it "should not include any other numbers than the ones from the first row and the last column" do + board.cells[7..12].each do |num| + board.get_start_indexes_to_left(4).should_not include(num) + end + board.cells[14..41].each do |num| + board.get_start_indexes_to_left(4).should_not include(num) + end + end + it "should not have index numbers bigger than rows*columns" do + s_no = board.col_num * board.row_num + e_no = s_no + 10 + board.cells[s_no..e_no].each do |num| + board.get_start_indexes_to_left(4).should_not include(num) + end + end + it "should not include the numbers smaller than 4" do + s_no = 0 + e_no = 3 + board.cells[s_no..e_no].each do |num| + board.get_start_indexes_to_left(4).should_not include(num) + end + end + end + + context "#row_index_number" do + it "returns the indexnumber of the row of a certain field" do + fake_row_array(4).each do |num| + board.row_index_number(num).should eq 4 + end + end + end + + context "#column_index_number" do + it "returns the indexnumber of the column of a certain field" do + fake_column_array(0).each do |num| + board.column_index_number(num).should eq 0 + end + end + end + + context "#diagonal_indexes" do + it "has a maximum of row_num units when its going to right" do + board.diagonal_indexes(1, 0, board.get_start_indexes_to_right(4)).each do |diagonal| + diagonal.length.should <= board.row_num + end + end + it "has a maximum of row_num units when its going to left" do + board.diagonal_indexes(-1, board.col_num-1, board.get_start_indexes_to_left(4)).each do |diagonal| + diagonal.length.should <= board.row_num + end + end + end + + context "#diagonals" do + it "has gathered the empty contents of the cells" do + board.diagonals[0].should include('') + end + it "has gathered the contents of the cells" do + board.place_piece(6, "Token") + board.diagonals[0].should include("Token") + end + end + + context "#check_four_consecutive?" do + it "returns true if there are four occupied cells in a row" do + board.place_piece(1, "Striped") + board.place_piece(2, "Striped") + board.place_piece(3, "Striped") + board.place_piece(4, "Striped") + board.check_four_consecutive?.should be_true + end + + it "returns false if there are four occupied cells in a row" do + board.place_piece(1, "Striped") + board.place_piece(2, "Striped") + board.place_piece(3, "Striped") + board.place_piece(5, "Striped") + board.check_four_consecutive?.should be_false + end + + it "returns true if there are four occupied cells in a column" do + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.check_four_consecutive?.should be_true + end + + it "returns false if there are four occupied cells in a row" do + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.place_piece(1, "Striped") + board.place_piece(5, "Striped") + board.check_four_consecutive?.should be_false + end + + it "returns true if there are four occupied cells in a column" do + board.place_piece(1, "Test") + board.place_piece(2, "Striped2") + board.place_piece(2, "Test") + board.place_piece(3, "Striped2") + board.place_piece(3, "Striped") + board.place_piece(3, "Test") + board.place_piece(4, "Striped") + board.place_piece(4, "Striped2") + board.place_piece(4, "Striped") + board.place_piece(4, "Test") + board.check_four_consecutive?.should be_true + end + + it "returns false if there are four occupied cells in a row" do + board.place_piece(1, "Test_wrong") + board.place_piece(2, "Striped2") + board.place_piece(2, "Test") + board.place_piece(3, "Striped2") + board.place_piece(3, "Striped") + board.place_piece(3, "Test") + board.place_piece(4, "Striped") + board.place_piece(4, "Striped2") + board.place_piece(4, "Striped") + board.place_piece(4, "Test") + board.check_four_consecutive?.should be_false + end + end + + context "#four_consecutive" do + it "returns false, if a line of the board has not four of the same, consecutive tokens in it" do + board.four_consecutive?(["a","a","a","b","a"]).should be_false + end + it "returns true, if a line of the board has four of the same, consecutive tokens in it" do + board.four_consecutive?(["a","a","a","a","b"]).should be_true + end + end + + + +end \ No newline at end of file diff --git a/spec/connect_four_spec.rb b/spec/connect_four_spec.rb new file mode 100644 index 0000000..f8ec369 --- /dev/null +++ b/spec/connect_four_spec.rb @@ -0,0 +1 @@ +require 'spec_helper' diff --git a/spec/database_spec.rb b/spec/database_spec.rb new file mode 100644 index 0000000..1b6b6e7 --- /dev/null +++ b/spec/database_spec.rb @@ -0,0 +1,36 @@ +require_relative 'spec_helper' + +describe DB do + let(:path) { File.expand_path(File.join(File.dirname(__FILE__), '..', 'db', 'test.db')) } + before(:each) { @db = DB.create("test.db") } + after(:each) { system("rm #{path}") } + + context "when there's no database" do + it "creates a database" do + @db + File.exists?(path).should be_true + end + + it "returns a new db connection" do + @db.should be_an_instance_of SQLite3::Database + end + + it "creates the users table" do + expect { + @db.execute("SELECT * FROM games;") + }.to_not raise_error + end + end + + context "when there is already a database" do + it "does not creates a new database" do + # @db = DB.create("test.rb") + # @db.execute("INSERT INTO players (name, twitter, password) VALUES ('brent', 'brent', 123);") + # @db = DB.create("test.rb") + # @db.execute("SELECT COUNT(*) FROM players")[0][0].should eq 1 + end + + end + + +end \ No newline at end of file diff --git a/spec/game_spec.rb b/spec/game_spec.rb new file mode 100644 index 0000000..a6a173d --- /dev/null +++ b/spec/game_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Game do + + let(:player1) { Player.new({:name => 'brent', :twitter => 'brent', :password => 'master'}) } + let(:player2) { Player.new({:name => 'jo', :twitter => 'jauny', :password => 'blah'}) } + let(:game) { Game.new(player1, player2) } + + context '#initialize' do + it 'has 2 players' do + game.player1.should be_true + game.player2.should be_true + end + + it "has a parameter for winner" do + game.should respond_to(:winner) + end + + it "has a board" do + game.should respond_to(:board) + end + end + + context "#toggle_player" do + it "should change the order of the players" do + first_player = game.players.first + second_player = game.players.last + game.toggle_player + game.players.first.should eq(second_player) + end + end + + context "#current_player" do + it "should return the current player" do + first_player = game.players.first + second_player = game.players.last + game.toggle_player + game.current_player.should eq(second_player) + end + end + +end \ No newline at end of file diff --git a/spec/player_spec.rb b/spec/player_spec.rb new file mode 100644 index 0000000..1bcce32 --- /dev/null +++ b/spec/player_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Player do + let(:player) { Player.new({ name: "Brent", twitter: "Blah", password: "master" }) } + + describe "#initialize" do + it "has a name" do + player.name.should eq "Brent" + end + + it "has a twitter account" do + player.twitter.should eq "Blah" + end + + it "has a password" do + player.password.should eq "master" + end + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..79d2a04 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require_relative '../lib/connect_four' \ No newline at end of file diff --git a/spec/twitter_player_spec.rb b/spec/twitter_player_spec.rb new file mode 100644 index 0000000..58ad325 --- /dev/null +++ b/spec/twitter_player_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe TwitterPlayer do + let(:twitter_player) {TwitterPlayer.from_twitter} + + describe "#initialize" do + it "creates a random tag" do + # twitter_player.random_tag.should be_an_instance_of String + end + end + + describe "#twitter_player" do + it "tracks a keyword" do + + end + end + +end \ No newline at end of file