# #-- # # $Id: tictactoe.rb 362 2005-11-26 17:56:46Z 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 'smagacor/listener' # TicTacToe for Smagacor module Smagacor::TicTacToe # Base player class class Player # Return a new player with the +sign+. def initialize @field = -1 end # Defines whether the player has selected a field. Default is +false+. def field_selected? @field != -1 end # Invoked by the game engine to get the field. The field is reset to the standard value so that # #field_selected? returns false. def selected_field field, @field = @field, -1 field end end # Human player. Provides the interface for a human to play TicTacToe. class HumanPlayer < Player # Set the +field+ which is returned by the next #selcted_field. def set_field( field ) @field = field end end # AI computer player. Not yet implemented! class ComputerPlayer < Player def selected_field 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 PLAYER1 = ?1 PLAYER2 = ?2 include Listener # The board on which is played. attr_accessor :board # The current player. attr_reader :cur_player # Create a new TicTacToe game with the given +players+. def initialize( player1, player2 ) @player1 = player1 @player2 = player2 @board = Board.new( 9 ) add_msg_name( :move ) add_msg_name( :finished ) end # Initialize the game state. def init @board = Board.new( 9 ) @cur_player = @player1 end # Play one (or more) round(s) of a TicTacToe game. When the current player has made his move # (Player#field_selected?), 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 @cur_player.field_selected? && !game_finished? logger.info { "Current board: #{@board}" } logger.info { "Current player: #{sign(@cur_player).chr}" } field = @cur_player.selected_field logger.info { "Field selected: #{field}" } if @board[field] == Board::FIELD_EMPTY @board[field] = sign( @cur_player ) switch_players dispatch_msg( :move, field, @board[field] ) end end if game_finished? logger.info { "Game finished" } dispatch_msg( :finished, player_won ) end end ####### private ####### def sign( player ) ( player == @player1 ? PLAYER1 : PLAYER2 ) end def switch_players @cur_player = ( @cur_player == @player1 ? @player2 : @player1 ) end # True if the board is full. def board_full? @board.all? {|i| i != Board::FIELD_EMPTY} end # True if the game is over def game_finished? board_full? || player_won?( PLAYER1 ) || player_won?( PLAYER2 ) 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?( PLAYER1 ) ? 0 : ( player_won?( PLAYER2 ) ? 1 : ( board_full? ? 2 : -1 ) ) ) 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]] # 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 class TicTacToeCanvas < Qt::Widget signals 'clicked(int)' def initialize( *args ) super( *args ) setBackgroundMode( Qt::NoBackground ) @x = @sx = Qt::Image.new( File.join( File.dirname( __FILE__ ), 'x.png' ) ) @o = @so = Qt::Image.new( File.join( File.dirname( __FILE__ ), 'o.png' ) ) end def game=( game ) @game = game @game.add_msg_listener( :move, method(:onMove) ) end def onMove( field, player ) painter = Qt::Painter.new( self ) case player when TicTacToe::PLAYER1 then paintX( painter, field ) when TicTacToe::PLAYER2 then paintO( painter, field ) end end def paintPlayArea( painter ) painter.setPen( Qt::Pen.new( Qt::black, @b_padding/10, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin ) ) painter.drawLine( @b_padding + @b_width/3, @b_padding, @b_padding + @b_width/3, @b_padding + @b_width ) painter.drawLine( @b_padding + @b_width*2/3, @b_padding, @b_padding + @b_width*2/3, @b_padding + @b_width ) painter.drawLine( @b_padding, @b_padding + @b_width/3, @b_padding + @b_width, @b_padding + @b_width/3 ) painter.drawLine( @b_padding, @b_padding + @b_width*2/3, @b_padding + @b_width, @b_padding + @b_width*2/3 ) end def paintCurGame( painter ) unless @game.nil? @game.board.each_with_index do |item, index| case item when TicTacToe::PLAYER1 then paintX( painter, index ) when TicTacToe::PLAYER2 then paintO( painter, index ) end end end end def paintX( painter, field ) x, y, w = field_info( field ) @sx = @x.smoothScale( w, w ) if @sx.width != w painter.drawImage( x, y, @sx ) end def paintO( painter, field ) x, y, w = field_info( field ) @so = @o.smoothScale( w, w ) if @so.width != w painter.drawImage( x, y, @so ) end def paintEvent( event ) pix = Qt::Pixmap.new( width, height ) painter = Qt::Painter.new painter.begin( pix ) painter.setBrush( Qt::white ) painter.drawRect( 0, 0, self.width, self.height ) paintPlayArea( painter ) paintCurGame( painter ) painter.end p = Qt::Painter.new( self ) p.drawPixmap( 0, 0, pix ) end def resizeEvent( event ) width = (self.width > self.height ? self.height : self.width ) @b_width = (width * 0.8).to_i @b_padding = (width * 0.1).to_i @f_width = @b_width/5 @f_padding = @b_padding + (@b_width/3 - @f_width)/2 end def field_info( field ) [@f_padding + @b_width/3 * (field % 3), @f_padding + @b_width/3 * (field / 3), @f_width] end def mouseReleaseEvent( event ) pos = Proc.new do |point| case point when 0..(@b_padding + @b_width/3): 0 when (@b_padding + @b_width/3)..(@b_padding + @b_width*2/3): 1 else 2 end end emit clicked( pos[event.x] + pos[event.y]*3 ) end end # Widget for TicTacToe. class TicTacToeUI < Qt::Widget slots 'start_game()', 'clicked(int)' attr_reader :gameinfo PLAYERS = {'Human' => HumanPlayer, 'Computer' => ComputerPlayer} def initialize( p, gameinfo ) super( p ) @gameinfo = gameinfo @canvas = TicTacToeCanvas.new( self ) connect( @canvas, SIGNAL('clicked(int)'), self, SLOT('clicked(int)') ) optionsLayout = Qt::VBoxLayout.new optionsLayout.setSpacing( 3 ) optionsLayout.setMargin( 6 ) p = Proc.new do |name| optionsLayout.addWidget( Qt::Label.new( name, self ) ) box = Qt::ComboBox.new( false, self ) optionsLayout.addWidget( box ) box.insertItem( 'Human' ) box.insertItem( 'Computer' ) box end @player1 = p.call( 'Player 1' ) @player2 = p.call( 'Player 2' ) button = Qt::PushButton.new( "Start Game", self ) connect( button, SIGNAL('clicked()'), self, SLOT('start_game()') ) optionsLayout.addSpacing( 10 ) optionsLayout.addWidget( button ) optionsLayout.addStretch @xicon = File.join( gameinfo.directory, 'x.png' ) @oicon = File.join( gameinfo.directory, 'o.png' ) layout = Qt::HBoxLayout.new( self ) layout.addWidget( @canvas, 1 ) layout.addLayout( optionsLayout ) end def clicked( field ) if !@game.nil? && @game.cur_player.respond_to?( :set_field ) @game.cur_player.set_field( field ) @game.play_round end end def start_game @game = TicTacToe.new( PLAYERS[@player1.currentText].new, PLAYERS[@player2.currentText].new ) @canvas.game = @game @game.add_msg_listener( :move, method(:onMove) ) @game.add_msg_listener( :finished, method(:finished) ) @game.init @game.play_round @canvas.update end def onMove( field, player ) @manager.add_undo_info( [field, player] ) # TODO think about AI moves, player changes etc end def finished( winner ) msg = (winner < 2 ? "Player #{winner+1}" : "Nobody" ) + " has won!" Qt::MessageBox.information( self, "Result", msg, Qt::MessageBox::Ok ) end def set_undo_manager( manager ) @manager = manager manager.add_msg_listener( :undo, method(:undo) ) manager.add_msg_listener( :redo, method(:redo) ) end def undo( info ) #TODO also undo cur player @game.board[info[0]] = Board::FIELD_EMPTY @canvas.update end def redo( info ) @game.board[info[0]] = info[1] @canvas.update end end class TicTacToeGame < Smagacor::GamePlugin def initialize( p, gameinfo ) @parent = p @widget = TicTacToeUI.new( p, gameinfo ) end def game_widget @widget end def set_undo_manager( manager ) @widget.set_undo_manager( manager ) end end end