# #-- # # $Id: tictactoe.rb 190 2005-02-02 19:26:25Z thomas $ # # smagacor - a collection of small games in ruby # Copyright (C) 2004 Thomas Leitner # # This program is free software; you can redistribute it and/or modify it under the terms of the GNU # General Public License as published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program; if not, # write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #++ # require 'observer' module Smagacor # TicTacToe for Smagacor module TicTacToe # Base player class class Player # Defines whether the player can currently make a move. Default is +false+. attr_accessor :can_move # Return a new player. def initialize @can_move = false end # Invoked by the game engine to get the move. def move() end end # Human player. Provides the interface for a human to play TicTacToe. class HumanPlayer < Player def initialize super @field = -1 end # See Player#move def move self.can_move = false @field end # Set the +field+ which is returned by the next #move. can_move is set to +true+ so # that the game engine knows the this player has chosen a field. def set_field( field ) @field = field self.can_move = true end end # AI computer player. Not yet implemented! class ComputerPlayer < Player def move end end # The TicTacToe board. class Board < String # Defines the empty field. FIELD_EMPTY = ?- def initialize( size ) super( FIELD_EMPTY.chr * size ) end # Redefined each so that the methods from Enumerable work correctly. def each( &block ) each_byte( &block ) end end # Game engine for TicTacToe. Knows how to correctly play TicTacToe. class TicTacToe include Observable # The board on which is played. attr_accessor :board # Create a new TicTacToe game with the given +players+. def initialize( players ) @players = players @board = Board.new( 9 ) end # Return the current player if the game has been initialized. def cur_player @players[@cur_player] if @cur_player < 2 end # Initialize the game state. def init @board = Board.new( 9 ) @cur_player = 0 end # Play one (or more) round(s) of a TicTacToe game. When the current player can make a move # (Player#can_move), his move is verified and the board changed. Then it is the turn of the # other player. As long as the current player can make a move (and the game is not finished), # the method runs. When the current player cannot make a move, the method exists and it has to # be called again, when the current player can make his move. def play_round while @players[@cur_player].can_move && !game_finished? logger.info { "Current board: #{@board}" } logger.info { "Current player: #{cur_player}" } field = @players[@cur_player].move logger.info { "Field selected: #{field}" } if @board[field] == Board::FIELD_EMPTY @board[field] = @cur_player.to_s @cur_player = 1 - @cur_player changed notify_observers( :move ) end end if game_finished? logger.info { " Game finished" } changed notify_observers( :finished, player_won ) end end private # True if the board is full. def board_full? @board.all? {|i| i != Board::FIELD_EMPTY} end # Win situations. WIN_SITUATIONS = [[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]] # True if the game is over def game_finished? board_full? || player_won?( ?0 ) || player_won?( ?1 ) end # Return the number of the player who has won. # # Returns # 0:: player 1 # 1:: player 2 # 2:: draw # -1:: game not finished yet def player_won ( player_won?( ?0 ) ? 0 : ( player_won?( ?1 ) ? 1 : ( board_full? ? 2 : -1 ) ) ) end # Check if +player+ has won the game. def player_won?( player ) WIN_SITUATIONS.each {|a,b,c| return true if @board[a] == player && @board[b] == player && @board[c] == player } return false end end # Widget for TicTacToe. class TicTacToeUI < FXHorizontalFrame attr_reader :gameinfo def initialize( p, gameinfo ) super( p ) @gameinfo = gameinfo @canvas = FXCanvas.new( self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y ) @canvas.connect( SEL_PAINT, method( :onPaint ) ) @canvas.connect( SEL_LEFTBUTTONRELEASE, method( :onCanvasClick ) ) FXVerticalSeparator.new( self ) options = FXVerticalFrame.new( self, LAYOUT_FILL_Y ) p = Proc.new do |name| FXLabel.new( options, name ) box = FXComboBox.new( options, 20, 2, nil, 0, COMBOBOX_NORMAL|COMBOBOX_STATIC|FRAME_SUNKEN ) box.appendItem( 'Human', HumanPlayer ) box.appendItem( 'Computer', ComputerPlayer ) box end @player1 = p.call( 'Player 1' ) @player2 = p.call( 'Player 2' ) FXButton.new( options, "Start Game", nil, nil, 0, LAYOUT_FILL_X|BUTTON_NORMAL ).connect( SEL_COMMAND, method( :onStartGame ) ) @xicon = File.join( gameinfo.directory, 'x.png' ) @oicon = File.join( gameinfo.directory, 'o.png' ) end def create super end def onCanvasClick( sender, sel, event ) if !@game.nil? && @game.cur_player.respond_to?( :set_field ) width, padding = get_lengths pos = Proc.new do |point| case point when 0..(padding + width/3): 0 when (padding + width/3)..(padding + width*2/3): 1 else 2 end end @game.cur_player.set_field( pos[event.click_x] + pos[event.click_y]*3 ) @game.play_round end end def onStartGame( sender, sel, event ) p = [@player1.getItemData( @player1.currentItem ).new, @player2.getItemData( @player2.currentItem ).new ] @canvas.update @game = TicTacToe.new( p) @game.add_observer( self ) @game.init @game.play_round end def update( reason, winner=nil ) case reason when :move: @canvas.update when :finished msg = (winner < 2 ? "Player #{winner+1}" : "Nobody" ) + " has won!" FXMessageBox.information( self, MBOX_OK, "Result", msg ) end end def onPaint( sender, sel, event ) FXDCWindow.new( sender, event ) do |dc| dc.foreground = FXColor::White dc.fillRectangle( 0, 0, sender.width, sender.height ) paintPlayArea( dc ) paintCurGame( dc ) end end private def get_lengths width = (@canvas.width > @canvas.height ? @canvas.height : @canvas.width ) [(width * 0.8).to_i, (width * 0.1).to_i] end def paintPlayArea( dc ) width, padding = get_lengths dc.foreground = FXColor::Black dc.lineWidth = 10 dc.lineCap = CAP_ROUND dc.drawLine( padding + width/3, padding, padding + width/3, padding + width ) dc.drawLine( padding + width*2/3, padding, padding + width*2/3, padding + width ) dc.drawLine( padding, padding + width/3, padding + width, padding + width/3 ) dc.drawLine( padding, padding + width*2/3, padding + width, padding + width*2/3 ) end def paintCurGame( dc ) unless @game.nil? width, padding = get_lengths size = width/5 padding = padding + (width/3 - size)/2 @game.board.each_with_index do |item, index| case item when ?0 dc.foreground = FXColor::Red dc.drawRectangle( padding + width/3 * (index % 3), padding + width/3 * (index / 3), size, size ) when ?1 dc.foreground = FXColor::Green dc.drawCircle( padding + size/2 + width/3 * (index % 3), padding + size/2 + width/3 * (index / 3), size/2 ) end end end end end end end