view src/net/kryshen/charamega/game.mirah @ 3:1bbcb3d4302f

Updated .hgignore.
author Mikhail Kryshen <mikhail@kryshen.net>
date Sat, 14 Jul 2012 06:55:35 +0400
parents 91ecd24948de
children 28c25c08059d
line wrap: on
line source

#
#  Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net>
#
#  This file is part of Charamega.
#
#  Charamega 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 3 of the License, or
#  (at your option) any later version.
#
#  Charamega 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 Charamega.  If not, see <http://www.gnu.org/licenses/>.
#

package net.kryshen.charamega

import java.util.Collection
import java.util.List
import java.util.ArrayList
import java.util.Collections

class Game

  def initialize
    @symbols = ArrayList.new

    add_range 0x03B1, 0x03C9                 # greek
    add_all [0x0439, 0x044A, 0x0463, 0x0467] # cyrillic
    add_range 0x20A0, 0x20B5                 # currency
    add_all [0x2190, 0x2194, 0x21CC]         # arrows
    add_all [0x221A, 0x221E, 0x222B, 0x222E] # math
    add_all [0x2318, 0x23CE, 0x23CF, 0x23E3]
    add_all [0x25A8, 0x25A9]
    add_range 0x25C6, 0x25D0
    add_all [0x25D4]
    add_range 0x2600, 0x261A
    add_range 0x2620, 0x2630
    add_range 0x2638, 0x2672
    add_range 0x267A, 0x2689
    add_range 0x2690, 0x269C
    add_all [0x26A0, 0x26A1]
    add_all [0x2706, 0x2707, 0x2708, 0x2709]
    add_all [0x270C, 0x270D, 0x270E]
    add_all [0x2744]

    self.players = 1
  end

  def shuffle
    return false  if @shuffled
    shuffle @cards.size / 2

    true
  end

  def shuffle(npairs:int):void
    Collections.shuffle @symbols
    
    cards = ArrayList.new npairs    
    time = System.nanoTime

    @symbols.subList(0, npairs).each do |c|
      cards.add Card.new Character(c).charValue, time
      cards.add Card.new Character(c).charValue, time
    end
    
    Collections.shuffle cards
    @cards = cards
    @layout = List(nil)
    @first = @second = Card(nil)
    @non_matching = 0
    @seconds = 0
    @shuffled = true

    compute_limits
  end

  def players=(nplayers:int):void
    @matches = int[nplayers]
    @player = 0
  end

  def cards
    @cards
  end

  def start:void
    shuffle
    @shuffled = false

    @matches.length.times { |i| @matches[i] = 0 }
    @player = 0

    @start_time = System.nanoTime
    @playing = true
  end

  def stop:void
    @playing = false

    unless @shuffled
      time = System.nanoTime
      delay = long(2E8 / @cards.size)
      
      @layout.each do |row|
        List(row).each do |e|
          card = Card(e)
          card.open time  unless card.matched?
          time += delay
        end
      end
    end
  end

  # Returns list of lists of cards for each row.
  def layout(w:int, h:int):List
    return @layout  unless @layout.nil? or @shuffled

    n = @cards.size
    cols = columns(float(w) / h)
    rows = int(Math.ceil float(n) / cols)

    if !@layout.nil? and 
        @layout_cols == cols and 
        @layout_rows == rows

      return @layout
    end

    layout = ArrayList.new rows

    d = cols * rows - n
    c = 0
    rows.times do |i|
      k = cols
      k -= 1  if i >= rows - d
      
      row = ArrayList.new k
      k.times do
        row.add @cards.get(c)
        c += 1
      end

      layout.add row
    end

    Collections.shuffle layout

    @layout_cols = cols
    @layout_rows = rows
    @layout = layout
  end

  # Called periodically by UI.
  def update
    time = System.nanoTime
    @seconds = int((time - @start_time) / 1E9)

    # Flip the cards back after some time.
    if !@second.nil? and
        @first.auto_close?(time) and @second.auto_close?(time)

      @first.close time
      @second.close time
      return true
    end

    false
  end

  # Number of removed pairs.
  def matched
    matched = 0
    @matches.each { |i| matched += i }
    
    matched
  end

  def finished?
    ignore_limits = false
    # ignore_limits = true

    matched * 2 == @cards.size or
      (!multiplayer? and !ignore_limits and
       (@non_matching > @max_non_matching or
        @seconds >= @max_seconds))
  end

  def winner
    max = -1
    winner = -1

    @matches.length.times do |i|
      if @matches[i] > max
        max = @matches[i]
        winner = i
      elsif @matches[i] == max
        winner = -1
      end
    end
    
    winner
  end

  def multiplayer?
    @matches.length > 1
  end

  def status
    if multiplayer?
      sb = StringBuffer.new
      sb.append 'Scores: '
      @matches.length.times do |i|
        sb.append ', '  if i > 0
        sb.append '('  if i == @player
        sb.append String.valueOf(@matches[i])
        sb.append ')'  if i == @player
      end
      sb.append ". "

      if finished?
        w = winner
        sb.append "Player #{w + 1} wins!"  if w >= 0
        sb.append "Tie!"  if w < 0
      else
        sb.append "Player #{@player + 1}'s turn."
      end

      sb.toString
    else
      sec = @max_seconds - @seconds
      time = Integer[2]
      time[0] = Integer.valueOf(sec / 60)
      time[1] = Integer.valueOf(sec % 60)      
      
      sb = StringBuffer.new
      sb.append String.format("Time left: %02d:%02d.", time)
      sb.append "  Non-matching: #{@non_matching}"
      sb.append " / #{@max_non_matching}."

      left = @cards.size / 2 - matched

      if left == 0
        sb.append "  You win!"
        puts "#{@cards.size / 2},#{@non_matching},#{@seconds}"
      else
        sb.append "  Pars left: #{left}."
      end

      sb.toString
    end
  end

  def open(card:Card):boolean
    return false  if finished?
    return false  if card.matched?
    return false  if card.opened? and (@first.nil? or @second.nil?)

    time = System.nanoTime

    if @first.nil?
      @first = card.open time
    elsif @second.nil?
      @second = card.open time
    else
      @first.close time
      @second.close time
      @first = card.open time
      @second = nil
    end

    unless @second.nil? 
      if @first.symbol == @second.symbol
        @first.match time
        @second.match time
        @first = @second = nil
        @matches[@player] += 1
      else
        @non_matching += 1
        @player = (@player + 1) % @matches.length
      end
    end

    true
  end

  def hit(x:int, y:int, w:int, h:int)
    return nil  if x < 0 or y < 0

    layout = layout(w, h)
    i = y * layout.size / h

    return nil  if i >= layout.size

    row = List(layout.get i)
    j = x * row.size / w
    
    return nil  if j >= row.size

    Card(row.get j)
  end

  private

  def compute_limits
    pairs = @cards.size / 2
    
    @max_non_matching = 0
    @max_seconds = 2

    # No need to optimize this.
    pairs.downto(3) do |i|
      if i > 180
        @max_non_matching += 9
      elsif i > 150
        @max_non_matching += 8
      elsif i > 120
        @max_non_matching += 7
      elsif i > 90
        @max_non_matching += 6
      elsif i > 60
        @max_non_matching += 5
      elsif i > 30
        @max_non_matching += 4
      elsif i > 20
        @max_non_matching += 3
      elsif i > 8
        @max_non_matching += 2
      else
        @max_non_matching += 1
      end
      
      if i > 160
        @max_seconds += 30
      elsif i > 120
        @max_seconds += 25
      elsif i > 80
        @max_seconds += 20
      elsif i > 60
        @max_seconds += 15
      elsif i > 40
        @max_seconds += 12
      elsif i > 20
        @max_seconds += 9
      elsif i > 9
        @max_seconds += 6
      elsif i > 5
        @max_seconds += 4
      else
        @max_seconds += 2
      end
    end
  end

  def columns(aspect:float)
    n = @cards.size
    c = Math.max(1, int(Math.ceil Math.sqrt(n * aspect)))
    
    loop do
      m = n % c
      break  if m == 0 or c - m <= n / c
      c -= 1
    end

    c
  end

  def add_range(from:int, to:int):void
    from.upto(to) do |i|
      @symbols.add Character.new char(i)
    end
  end
  
  def add_all(xs:Collection)
    xs.each do |i|
      @symbols.add Character.new char(Number(i).intValue)
    end
  end
end