[SPOILER] Solution Expert Quiz 23 in Ruby/Tk

Frank Fischer <frank.fischer-JJ2xi2hz/[email protected]>
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
Hi,

I implemented tetris in ruby with Ruby/Tk. Since im not an 
tk-expert, there is still much space for improvement of the interface 
(it's still very simply and slow). My version has two parts, a server
and client. I didn't implement the 2-player variant of the 2nd milestone
but another one: Two players play against each other. If one player
deletes two or more lines, the opponent gets some extra lines. The
player who crashes first loses the game.

To start a game, first start the server with:
	ruby tserver.rb [-p port]
and one or two clients with
	ruby tclient.rb -n nick [-a server-address] [-p port]

The interface is very incomplete but does the job for this quiz, I
think.

I put the files as attachment to this mail because the are a little
large, hope this is ok.

Gruß
Frank
tcore.rb (text/plain, 6.6 KB)
BRICKS = 
####
  [	 8, 9,10,11, 	 2, 6,10,14,	 8, 9,10,11,	  2, 6,10,14,
##
##
    	 9,10, 5, 6,	 9,10, 5, 6,	 9,10, 5, 6,	  9,10, 5, 6,
 ##
##
  	 5, 6,10,11,	14,10,11, 7,	 5, 6,10,11,	 14,10,11, 7,
##
 ##	
  	 9,10, 6, 7,	15,10,11, 6,	 9,10, 6, 7,	 15,10,11, 6,
###
#
  	 9,10,11, 5,    13,14,10, 6,     9,10,11,15,     14,10, 6, 7,
###
  #
  	 9,10,11, 7,    14,10, 6, 5,	 9,10,11,13,     15,14,10, 6, 
###
 #
   	 9,10,11, 6,	 9,14,10, 6, 	 9,10,11,14,	 14,10, 6,11]


class Brick
  attr_reader :color, :x, :y

  def initialize(type, x = 0, y = 0)
    @x = x
    @y = y
    @rot = 0
    @offset = type * 16
    @color = type+1
  end


  def down
    if yield(blocks(nil, 0, -1))
      @y -= 1
      return true
    else
      return false
    end
  end


  def left
    if yield(blocks(nil, -1, 0))
      @x -= 1
      return true
    else
      return false
    end
  end


  def right
    if yield(blocks(nil, 1, 0))
      @x += 1
      return true
    else
      return false
    end
  end


  def rotate_left
    new_rot = @rot == 0 ? 3 : @rot - 1
    if yield(blocks(new_rot)) then
      @rot = new_rot
      return true
    else
      return false
    end
  end


  def rotate_right
    new_rot = @rot == 3 ? 0 : @rot + 1
    if yield(blocks(new_rot)) then
      @rot = new_rot
      return true
    else
      return false
    end
  end


  def blocks(rot = nil, dx = 0, dy = 0)
    BRICKS[@offset + (rot || @rot)*4, 4].collect do 
      |b| 
      y, x = b.divmod(4)
      [x + @x + dx, y + @y + dy]
    end
  end


  def blocks0
    BRICKS[@offset + @rot*4, 4].collect do |b| 
      y, x = b.divmod(4)
      [x, y]
    end
  end

end


class Game
  attr_reader :board, :next_brick, :current_brick, :score, :last_full_lines
  attr_reader :last_difference

  GAME_OVER = 0
  REMOVED_LINES = 1
  NEXT_STONE = 2
  OK = 3

  def initialize(width = 10, height = 20, level = 1)
    @width = width
    @height = height
    @level = level
    @score = 0
    @lines = 0

    @board = Array.new(@height)
    @last_board = Array.new(@height)
    for i in 0...@height do
      @board[i] = Array.new(@width, 0)
      @last_board[i] = Array.new(@width, 0)
    end

    @last_difference = []
    get_next_brick # the first one
    get_next_brick # and the next one

    @penalty_lines = 0
  end


  def next_level(incr = 1)
    @level += incr
  end


  def gravity
    1.0 / 1.3 ** (@level - 1)
  end
  

  def get_next_brick
    @current_brick = @next_brick
    @next_brick = Brick.new(rand(BRICKS.length/16), 
			    @width/2-2, @height-3)
    if @current_brick
      # simply set the new blocks as difference
      color = @current_brick.color
      @current_brick.blocks.each do |x,y| 
	@last_difference << [x,y,color]  if y < @height
      end
    end
  end


  def update_difference(last_blocks)
    # first update the current board, i.e. do the last change
    @last_difference.each { |x,y,c| @last_board[y][x] = c }

    # new calculate the difference of the moved block
    new_blocks = @current_brick.blocks
    color = @current_brick.color
    @last_difference = []
    (new_blocks - last_blocks).each do |x,y| 
      @last_difference << [x,y,color] if y < @height 
    end
    (last_blocks - new_blocks).each do |x,y| 
      @last_difference << [x,y,0] if y < @height
    end
  end


  def check(brick)
    brick.each { |x,y|
      if not (0...@width).member?(x) or y < 0 or
	 (y < @height and @board[y][x] > 0 )
      then
	return false
      end
    }
    return true
  end


  def check_full_lines
    lines = @lines
    full = []
    @board.each_index { |i| full << i unless @board[i].member?(0) }
    full.reverse_each { |i| 
      @score += @level * @width
      @lines += 1
      @board.delete_at(i) 
      @board << Array.new(@width, 0)
    }
    next_level if @lines/10 - lines/10 > 0
    if full.empty? 
      return false
    else
      @last_full_lines = full.reverse
      return true
    end
  end


  def down_brick
    last_blocks = @current_brick.blocks
    if @current_brick.down { |b| check(b) } 
      update_difference(last_blocks)
      return OK
    else
      score = 0
      @current_brick.blocks.each { |x,y|
	score +=1 if x == 0 or @board[y][x-1] > 0
	score +=1 if x == @width-1 or @board[y][x+1] > 0
	score +=1 if y == 0 or @board[y-1][x] > 0
      }
      @score += score
      @current_brick.blocks.each { |x,y|
	@board[y][x] = @current_brick.color
      }

      # now update differences
      @last_difference.each { |x,y,c| @last_board[y][x] = c }
      @last_difference = []
      get_next_brick
      return GAME_OVER unless check(@current_brick.blocks)
      # check for full lines to clear
      has_removed_lines = check_full_lines
      # now check, if we have to insert some penalty lines from other players
      has_inserted_random_lines = insert_random_lines
      # if something changed (removed lines or penalty-lines), recalculate
      # the difference
      if has_inserted_random_lines or has_removed_lines then
	for y in 0...@height
	  brow = @board[y]
	  lrow = @last_board[y]
	  for x in 0...@width
	    if brow[x] != lrow[x]
	      @last_difference << [x,y,brow[x]]
	    end
	  end
	end
	return REMOVED_LINES if has_removed_lines
      end
      return NEXT_STONE
    end
  end


  def left_brick
    last_blocks = @current_brick.blocks
    if @current_brick.left { |b| check(b) }
      update_difference(last_blocks)
      return true
    else
      return false
    end
  end


  def right_brick
    last_blocks = @current_brick.blocks
    if @current_brick.right { |b| check(b) }
      update_difference(last_blocks)
      return true
    else
      return false
    end
  end


  def rotate_left_brick
    last_blocks = @current_brick.blocks
    if @current_brick.rotate_left { |b| check(b) }
      update_difference(last_blocks)
      return true
    else
      return false
    end
  end


  def rotate_right_brick
    last_blocks = @current_brick.blocks
    if @current_brick.rotate_right { |b| check(b) }
      update_difference(last_blocks)
      return true
    else
      return false
    end
  end


  def quick_down_brick
    last_blocks = @current_brick.blocks
    while @current_brick.down { |b| check(b) } do end
    update_difference(last_blocks)
  end


  def insert_penalty_lines(n)
    @penalty_lines += n
  end


  def insert_random_lines
    return false if @penalty_lines == 0
    random_lines = []
    1.upto(@penalty_lines) do
      line = []
      while line.length < @width / 2
	line << rand(@width)
	line.uniq!
      end

      row = Array.new(@width, 0)
      line.each { |col| row[col] = rand(BRICKS.length / 16) + 1 }
      random_lines << row
    end
    @board = random_lines + @board[0...-@penalty_lines]
    @penalty_lines = 0
    return true
  end
end
tclient.rb (text/plain, 10.1 KB)
require 'tk'
require 'socket'
require 'thread'
require 'timeout'
require 'getopts'

$width = 10
$height = 20
$port = 47898
$host = 'localhost'

COLORS = {' ' => nil, 
          '+' => '#FF0000', 
	  '#' => '#0000FF', 
	  '@' => '#00FF00', 
	  '%' => '#FFFF00', 
	  '*' => '#FF00FF', 
	  '&' => '#00FFFF',  
	  '$' => '#A52A2A' }


class Player
  attr_accessor :name, :board
  def initialize(name)
    @name = name
  end
end

   
class Block
  def initialize(parent, x1, y1, x2, y2, color = nil)
    if color.nil? or color == COLORS[0]
      @color_light = @color_inner = @color_dark = 'grey'
    else
      @color_light = darken(color, 100)
      @color_inner = darken(color, 75)
      @color_dark = darken(color, 50)
    end

    x1, x2 = [x1,x2].min, [x1,x2].max
    y1, y2 = [y1,y2].min, [y1,y2].max

    w = (0.1 * (x1-x2).abs).ceil
    h = (0.1 * (y1-y2).abs).ceil

    @inner = TkcRectangle.new(parent, x1+w, y1+h, x2-w, y2-h) {
      fill @color_inner
      outline @color_inner
    }

    @upright = TkcPolygon.new(parent, 
			      x1,y1, x1+w,y1+h, x2-w,y1+h, x2-w,y2-h,
			      x2,y2, x2,y1) {
      fill @color_light
      outline @color_light
    }

    @downleft = TkcPolygon.new(parent,
			       x1,y1, x1+w,y1+h, x1+w,y2-h, x2-w,y2-h,
			       x2,y2, x1,y2) {
      fill @color_dark
      outline @color_dark
    }
  end

  def color=(color)
    if color.nil? or color == COLORS[0]
      @color_light = @color_inner = @color_dark = 'grey'
    else
      @color_light = darken(color, 100)
      @color_inner = darken(color, 75)
      @color_dark = darken(color, 50)
    end
    
    @inner.fill @color_inner
    @inner.outline @color_inner
    @upright.fill @color_light
    @upright.outline @color_light
    @downleft.fill @color_dark
    @downleft.outline @color_dark
  end


  def darken(color, percent)
    if /^#([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})/ =~ color
      return ("\#%02X%02X%02X" % [ $1, $2, $3 ].collect { |x| 
	      x.hex * percent / 100 })
    else
      return color
    end
  end
	                              
end


class Tetris < TkFrame
  def initialize(width, height, nick, *args, &block)
    super(*args, &block)

    @width = width
    @height = height

    @block_width = 30
    @block_height = 30
    
    @root = self

    init_board
    init_preview

    @field_items.each { |row|
      row.each { |item|
	item.color = nil
      }
    }

    @nick_label = TkLabel.new(@root) {
      text nick
      pack('anchor' => 'n')
    }
    @score_label = TkLabel.new(@root) {
      text '0'
      pack('anchor' => 's')
    }
  end


  def init_board
    w = @block_width * @width
    h = @block_height * @height
    @board_canvas = TkCanvas.new(@root) {
      width w
      height h
      pack('padx' => 10, 'pady' => 10, 'side' => 'left' )
    }
    @board_canvas.pack

    @field_items = Array.new(@height)
    w = @board_canvas.width
    h = @board_canvas.height
    dx = w / @width
    dy = h / @height
    for y in 0...@height do
      row_items = Array.new(@width, nil)
      for x in 0...@width do
	row_items[x] = Block.new(@board_canvas, 
				 x*dx, h-y*dy, (x+1)*dx, h-(y+1)*dy, nil)
      end
      @field_items[y] = row_items
    end
  end


  def init_preview
    @preview_canvas = TkCanvas.new(@root) {
      width 60
      height 60
      background 'grey'
      pack('padx' => 10, 'pady' => 10, 
           'anchor' => 'n' )
    }
    @preview_canvas.pack
    @preview_items = Array.new(4)
    w = @preview_canvas.width
    h = @preview_canvas.height
    dx = w / 6
    dy = h / 6
    for y in 0...4 do
      row_items = Array.new(4)
      for x in 0...4 do
	row_items[x] = Block.new(@preview_canvas, (x+1)*dx, h-(y+1)*dy,
				                  (x+2)*dx, h-(y+2)*dy,
				 nil)
      end
      @preview_items[y] = row_items
    end
  end


  def paint(diffs)
    begin
      diffs.each do |x,y,c|
	@field_items[y][x].color = c
      end
    rescue Exception => e
      puts e
      puts e.backtrace
    end
  end


  def paint_preview(diffs)
    @preview_items.each do |row|
      row.each do |item|
	item.color = nil
      end
    end
    diffs.each do |x,y,c|
      @preview_items[y][x].color = c
    end

  end


  def score=(score)
    @score_label.text score.to_s
  end

end


class Console < TkFrame
  def initialize(*args, &block)
    super(*args, &block)


    @text = TkText.new(self) {
      state 'disabled'
      wrap 'word'
      width 50
      height 10
      pack 'side' => 'left', 'expand' => 1, 'fill' => 'x'
    }

    @scrollbar = TkScrollbar.new(self) {
      width 10
      pack 'side' => 'right', 'fill' => 'y'
    }
    @scrollbar.command proc { |*args| @text.yview *args }			
    @text.yscrollcommand(proc { |first,last| @scrollbar.set(first, last) })
  end


  def add_message(msg)
    @text.state 'normal'
    @text.insert 'end', "#{msg}\n"
    @text.state 'disabled'
  end


  def join_player(name)
    add_message "#{name} joins game"
  end


  def leave_player(name)
    add_message "#{name} leaves game"
  end


  def join_spectator(name)
    add_message "#{name} joins as spectator"
  end


  def leave_spectator(name)
    add_message "Spectator #{name} leaves game"
  end


  def start_game
    add_message "Game begins"
  end


  def end_game
    add_message "Game ends"
  end


  def cancel_game
    add_message "Game aborted"
  end


  def win(name)
    add_message "#{name} wins"
  end


  def lose(name)
    add_message "#{name} loses"
  end
end


class TetrisApp
  def initialize(socket, name, be_player = true)
    @socket = socket
    @player_id = nil
    @name = name
    @players = {}
    @be_player = be_player

    @condition_start = ConditionVariable.new
    @root = TkRoot.new

    @start = TkButton.new(@root) {
      text "Start";
      pack 'side' => 'bottom', 'anchor' => 's'
    }
    @console = Console.new(@root) {
      width 50
      height 30
      pack 'side' => 'bottom', 'fill' => 'x', 'anchor' => 'n'
    }

    @start.command(proc { do_start })

    @root.bind("KeyPress-Left", proc { do_left })
    @root.bind("KeyPress-Right", proc { do_right })
    @root.bind("KeyPress-Up", proc { do_rotate })
    @root.bind("KeyPress-Down", proc { do_down })
    
    @update_mutex = Mutex.new
    Thread.new { run }
  end


  def run
    begin
      @socket.write "HELLO\r\n"

      line = nil
      timeout(5) do
	line = @socket.gets
	return if line.nil? 
      end
      line.chop!
      return unless line =~ /^GAME ([A-Z])(\d+) (\d+) (\d+)/

      $width = $1[0] - ?A + 1
      $height = $2.to_i
      n_players = $3.to_i
      @max_players = $4.to_i

      if n_players < @max_players and @be_player
        @socket.write "PLAYER #{@name}\r\n"
      else
	@socket.write "SPECTATOR #{@name}\r\n"
      end
      while (line = @socket.gets)
	line.chop!

	@update_mutex.synchronize do
	  case line
	  when /^PLAYER (\d+)/
	    @player_id = $1.to_i
	  when /^DIFF (\d+) (.*)$/
	    handle_diff($1.to_i, $2)
	  when /^PREVIEW (\d+) (.*)$/
	    handle_preview($1.to_i, $2)
	  when /^SCORE (\d+) (\d+)/
	    handle_score($1.to_i, $2.to_i)
	  when /^INFO (.+)$/
	    handle_info($1)
	  when /^START/
	    handle_start_game
	  when /^CANCEL/
	    @console.cancel_game
	  when /^END/
	    @console.end_game
	  when /^RUNNING/
	    handle_start_game
	  when /^LOSE (\d+)/
	    handle_loose($1.to_i)
	  when /^WIN (\d+)/
	    handle_win($1.to_i)
	  end
	end
      end
    rescue Exception => e
      puts e
      puts e.backtrace
    ensure
      @socket.close
    end
  end


  def handle_diff(player, diff)
    diffs = []
    while diff =~ /^([A-Z])(\d+)([ \#@%*&+$])(.*)$/ do
      diffs << [$1[0] - ?A, $2.to_i, COLORS[$3]]
      diff = $4
    end

    if player == @player_id then
      Tk.after(10, proc { 
	  @tetris.paint(diffs) 
      })
    else
      Tk.after(10, proc {
	  @players[player].board.paint(diffs)
      })
    end
  end


  def handle_preview(player, diff)
    diffs = []
    while diff =~ /^([A-Z])(\d+)([ \#@%*&+$])(.*)$/ do
      diffs << [$1[0] - ?A, $2.to_i, COLORS[$3]]
      diff = $4
    end

    if player == @player_id then
      Tk.after(10, proc { 
	  @tetris.paint_preview(diffs) 
      })
    else
      Tk.after(10, proc {
	  @players[player].board.paint_preview(diffs)
      })
    end
  end


  def handle_score(player, score)
    if player == @player_id
      Tk.after(10, proc {
	  @tetris.score = score
      })
    else
      Tk.after(10, proc {
	  @players[player].board.score = score
      })
    end
  end


  def handle_info(info)
    case info
    when /^([+-])PLAYER (\d+) (.*)$/
      id = $2.to_i
      if $1 == '+'
	@players[id] = Player.new($3)
	@console.join_player($3)
      else
	@console.leave_player(@players[id].name)
	@players.delete(id)
      end
    when /^([+-])SPECTATOR (.*)$/
      if $1 == '+'
	@console.join_spectator($2)
      else
	@console.leave_spectator($2)
      end
    end
  end


  def handle_start_game
    puts $width, $height
    Tk.after 10, proc {
      if @be_player and (@tetris.nil? or @tetris.width != $width or 
			 @tetrix.height != $height)
	@tetris.destroy if @tetris
	@tetris = Tetris.new($width, $height, @name, @root) {
	  pack 'side' => 'left' 
	}
      end

      @players.each do |id, player|
	next if id == @player_id
	if player.board.nil? or player.board.width != $width or 
	   player.board.height != $height
	then
	  player.board.destroy if player.board
	  player.board = Tetris.new($width, $height, player.name, @root) {
	    pack 'side' => 'right'
	  }
	  puts "Create\n"
	end
      end
      @console.start_game
      @condition_start.signal
    }
    @condition_start.wait(@update_mutex)
  end

  def handle_win(id)
    @console.win(@players[id].name)
  end


  def handle_loose(id)
    @console.lose(@players[id].name)
  end


  def do_left
    @socket.write "LEFT\r\n"
  end


  def do_right
    @socket.write "RIGHT\r\n"
  end


  def do_rotate
    @socket.write "ROTATE\r\n"
  end


  def do_down
    @socket.write "DOWN\r\n"
  end


  def do_start
    @socket.write "START\r\n"
  end
end


getopts('', 'p:', 's', 'n:', 'h')

if $OPT_h
  puts "Usage: tclient [-p port] [-a host] [-n nick] [-sh]"
  exit 1
end

$port = $OPT_p || $port
$host = $OPT_a || $host
be_player = !$OPT_s
nick = $OPT_n

socket = TCPSocket.new($host, $port)

app = TetrisApp.new(socket, nick, be_player)
Tk.mainloop
tserver.rb (text/plain, 9.1 KB)
require 'socket'
require 'thread'
require 'timeout'
require 'getopts'

require 'tcore'

$width = 10
$height = 20
$port = 47898
$max_players = 2

COLORS = [ ' ', '#', '@', '%', '*', '&', '+', '$' ]

class Player
  attr_reader :id, :name, :socket, :game
  attr_accessor :game_thread

  def initialize(id, name, socket)
    @id = id
    @name = name
    @socket = socket
    @game = Game.new($width, $height)
    @mutex = Mutex.new
  end


  def start(server)
    @server = server
    @game_thread = Thread.new { run_game }
  end


  def run_game
    begin
      delay = game.gravity
      send_difference
      send_preview
      @wait_again = false
      @stop_thread = false
      loop do
	sleep(delay)
	return if @stop_thread

	if @wait_again
	  sleep(delay)
	  @wait_again = false
	end

	@mutex.synchronize do
	  case @game.down_brick
	  when Game::GAME_OVER
	    @server.game_over(@id)
	    return
	  when Game::REMOVED_LINES
	    send_difference
	    send_preview
	    @server.removed_lines(@id, @game.last_full_lines.length)
	    delay = @game.gravity
	  when Game::NEXT_STONE
	    send_difference
	    send_preview
	  when Game::OK
	    send_difference
	  end
	  @server.send_broadcast "SCORE #{@id} #{@game.score}"
	end
      end
    rescue Exception => e
      puts e
      puts e.backtrace
      @server.game_over(@id)
    ensure
      @game_thread = nil
    end
  end


  def wait_cancel
    if @game_thread
      thread = @game_thread
      @stop_thread = true
      thread.run
      thread.join
    end
  end


  def game_running?
    return @game_thread != nil
  end


  def send_difference
    diff = "DIFF #{@id} "
    @game.last_difference.each do |x,y,c|
      diff += (?A + x).chr + y.to_s + COLORS[c]
    end

    @server.send_broadcast diff
  end


  def send_preview
    preview = "PREVIEW #{@id} "
    if @game.next_brick
      c = COLORS[@game.next_brick.color]
      @game.next_brick.blocks0.each do |x,y|
	preview += (?A + x).chr + y.to_s + c
      end
    end

    @server.send_broadcast preview
  end


  def insert_random_lines(n)
    @mutex.synchronize { @game.insert_penalty_lines(n) }
  end


  def do_left
    @mutex.synchronize { send_difference if game_running? and @game.left_brick }
  end


  def do_right
    @mutex.synchronize { send_difference if game_running? and 
                                            @game.right_brick }
  end


  def do_rotate
    @mutex.synchronize { send_difference if game_running? and  
                                            @game.rotate_left_brick }
  end


  def do_down
    @mutex.synchronize do
      if game_running? and !@wait_again 
	game.quick_down_brick
	send_difference
	@wait_again = true
	@game_thread.run
      end
    end
  end


  def complete_diff
    diff = ""
    board = @game.board
    @mutex.synchronize do
      for y in 0...board.length
	row = board[y]
	for x in 0...board[y].length
	  diff += (?A + x).to_s + y.to_s + COLORS[row[x]]
	end
      end
    end
    
    return diff
  end
end


class Spectator
  attr_reader :name, :socket

  def initialize(name, socket)
    @name = name
    @socket = socket
  end
end


class Server
  NOID = -1

  def initialize
    @players = Array.new(2, nil)
    @spectators = []
    @client_mutex = Mutex.new
  end


  def run
    @running = false
    server = TCPServer.new($port)
    while (client = server.accept)
      Thread.new do
	begin
	  run_client(client)
	rescue Exception => e
	  puts e
	  puts e.backtrace
	end
      end
    end
  end


  def run_client(socket)
    begin
      player = nil
      spectator = nil

      # wait for hello
      line = nil
      timeout(5) do
	line = socket.gets
	return unless line
      end

      line.chop!
      return unless line =~ /^HELLO\b/

      # send OK
      @client_mutex.synchronize do
	n_players = 0
	@players.each { |p| n_players += 1 unless p.nil? }
        socket.write "GAME #{(?A + $width-1).chr}#{$height} #{n_players}"+
	             " #{$max_players}\r\n"
      end

      # inform about all connected players
      @client_mutex.synchronize do
	@players.each_index do |id|
	  unless @players[id].nil?
	    socket.write "INFO +PLAYER #{id+1} #{@players[id].name}\r\n"
	  end
	end
	@spectators.each do |spectator|
	  socket.write "INFO +SPECTATOR #{spectator.name}\r\n"
	end
	if @running
	  socket.write "RUNNING\r\n"
	  @players.each do |player|
	    socket.write "DIFF #{player.id} #{player.complete_diff}\r\n";
	  end
	end
      end


      while (line = socket.gets)
	line.chop!
	case line
	when /^PLAYER ?(.*)/
	  if player = add_player(socket, $1)
	    spectator = nil
	  end
	when /^SPECTATOR ?(.*)/
	  if spectator = add_spectator(socket, $1)
	    player = nil
	  end
	when /^START\b/
	  start_game
	when /^LEFT\b/
	  player.do_left if player 
	when /^RIGHT\b/
	  player.do_right if player
	when /^ROTATE\b/
	  player.do_rotate if player
	when /^DOWN\b/
	  player.do_down if player 
	else
	  puts "Ignore illegal client-command: #{line}"
	end
      end
    rescue TimeoutError 
      puts "Client connection timed out"
    rescue Exception => e
      puts e, e.backtrace
    ensure
      socket.close

      @client_mutex.synchronize do
	if player
	  if @running then
	    @players.each { |player| player.wait_cancel }
	    send_broadcast "CANCEL"
	    @running = false
	  end
	end

	for i in 0...-mMg6cxhC0QCm/[email protected] do
	  if @players[i] and @players[i].socket == socket
	    @players[i] = nil
	    break
	  end
	end

	for i in [email protected] do
	  if @spectators[i].socket == socket
	    @spectators.delete_at(i)
	    break
	  end
	end
      end

    end
  end


  def add_player(socket, name)
    if @running
      socket.write "ERR Game running\r\n"
      return
    end

    player = nil
    id = 0
    @client_mutex.synchronize do
      # first, check if player is already connected
      @players.each_index do |i|
	player = @players[i]
	if player and player.socket == socket
	  return player
	end
      end

      # now, look for a free slot
      id = @players.index(nil)
      if id.nil?
	# no free slot
	socket.write "ERR No free slot\r\n"
	return nil
      end

      # found a free slot, so use it ...
      # ...but first check if player is already a spectator
      for spectator_id in [email protected] do
	if @spectators[spectator_id].socket == socket
	  send_broadcast "INFO -SPECTATOR #{@spectators[spectator_id].name}",
	    false
	  @spectators.delete_at(spectator_id)
	  break
	end
      end
	  
      name ||= "Player #{id+1}"
      player = Player.new(id+1, name, socket)
      @players[id] = player

      # inform the client for success
      socket.write "PLAYER #{id+1} #{name}\r\n"

      # and all the others for the new player
      send_broadcast "INFO +PLAYER #{id+1} #{name}", false
      return player
    end

  end


  def add_spectator(socket, name)
    spectator = nil
    @client_mutex.synchronize do 
      # already a spectator?
      @spectators.each do |s|
	return s if s.socket == socket
      end
     
      player_id = nil
      @players.each_index do |id|
	player = @players[id]
	if player and player.socket == socket
	  # if this is a player and game is running he can't leave game
	  if @running 
	    socket.write "ERR Game running\r\n"
	    return nil
	  else
	    send_broadcast "INFO -PLAYER #{id+1} #{player.name}", false
	    @players[id] = nil
	  end
	end
      end

      # everything OK, so add as new spectator
      name ||= "Anonymous \##{socket.__id__}"
      spectator = Spectator.new(name, socket)
      @spectators << spectator

      # inform the client for success
      socket.write "SPECTATOR #{name}\r\n"

      # and all the others for the new spectator
      send_broadcast "INFO +SPECTATOR #{name}", false
      return spectator
    end
  end


  def send_broadcast(msg, block = true)
    @client_mutex.lock if block
    @players.each do |player|
      unless player.nil?
	player.socket.write msg
	player.socket.write "\r\n"
      end
    end

    @spectators.each do |spectator|
      spectator.socket.write msg
      spectator.socket.write "\r\n"
    end
    @client_mutex.unlock if block
  end


  def removed_lines(id, n)
    return if n == 1
    n -= 1 if n < 4

    @players.each do |player|
      if player and player.id != id
	player.insert_random_lines(n)
      end
    end
  end


  def start_game
    @client_mutex.synchronize do
      @running = true
      send_broadcast "START", false
      @players.each do |player|
	if player
	  player.start(self) 
	end
      end
    end
  end


  def game_over(id)
    send_broadcast "LOSE #{id}"
    running = []
    @players.each do |player|
      if player and player.id != id and player.game_running?
	running << player.id
      end
    end

    if running.length == 1
      @players[running.first-1].wait_cancel
      send_broadcast "WIN #{running.first}"
    end

    if running.length <= 1
      send_broadcast "END"
    end
  end
end

getopts('p:', 'c:', 'r:', 'm:', 'h')

if $OPT_h
  puts "Usage: tserver.rb [-p port] [-c columns] [-r rows] [-m maxplayers] [-h]"
  exit 1
end

$port = ($OPT_p || $port).to_i
$width = ($OPT_c || $width).to_i
$height = ($OPT_r || $height).to_i
$max_players = ($OPT_m || $max_players).to_i

server = Server.new
server.run
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.