changeset 0:91ecd24948de

Imported.
author Mikhail Kryshen <mikhail@kryshen.net>
date Sat, 14 Jul 2012 05:46:29 +0400
parents
children fac1b8f35265
files .hgignore Rakefile res/net/kryshen/charamega/DejaVuSans.ttf src/net/kryshen/charamega/card.mirah src/net/kryshen/charamega/field.mirah src/net/kryshen/charamega/game.mirah src/net/kryshen/charamega/ui.mirah
diffstat 7 files changed, 1010 insertions(+), 0 deletions(-) [+]
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/.hgignore	Sat Jul 14 05:46:29 2012 +0400
     1.3 @@ -0,0 +1,5 @@
     1.4 +syntax: regexp
     1.5 +
     1.6 +~$
     1.7 +^build$
     1.8 +^local_
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/Rakefile	Sat Jul 14 05:46:29 2012 +0400
     2.3 @@ -0,0 +1,15 @@
     2.4 +require 'mirah'
     2.5 +require 'rake/clean'
     2.6 +
     2.7 +SRC = FileList['src/**/*.mirah']
     2.8 +CLEAN.include('build/**/*.class')
     2.9 +
    2.10 +task :default => [:compile]
    2.11 +
    2.12 +task :compile => SRC do
    2.13 +  Mirah::compile '-d', 'build', *SRC
    2.14 +end
    2.15 +
    2.16 +task :run => [:compile] do
    2.17 +  sh 'java -cp build:res net/kryshen/charamega/Ui'
    2.18 +end
     3.1 Binary file res/net/kryshen/charamega/DejaVuSans.ttf has changed
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/src/net/kryshen/charamega/card.mirah	Sat Jul 14 05:46:29 2012 +0400
     4.3 @@ -0,0 +1,100 @@
     4.4 +#
     4.5 +#  Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net>
     4.6 +#
     4.7 +#  This file is part of Charamega.
     4.8 +#
     4.9 +#  Charamega is free software: you can redistribute it and/or modify
    4.10 +#  it under the terms of the GNU General Public License as published by
    4.11 +#  the Free Software Foundation, either version 3 of the License, or
    4.12 +#  (at your option) any later version.
    4.13 +#
    4.14 +#  Charamega is distributed in the hope that it will be useful,
    4.15 +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    4.16 +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    4.17 +#  GNU General Public License for more details.
    4.18 +#
    4.19 +#  You should have received a copy of the GNU General Public License
    4.20 +#  along with Charamega.  If not, see <http://www.gnu.org/licenses/>.
    4.21 +#
    4.22 +
    4.23 +package net.kryshen.charamega
    4.24 +
    4.25 +class Card
    4.26 +
    4.27 +  def initialize(symbol:char, time:long)
    4.28 +    @symbol = symbol
    4.29 +    @angle = Math.random * Math.PI - Math.PI / 2
    4.30 +
    4.31 +    @flip_interval = long(2E8)
    4.32 +    @remove_interval = long(2E8)
    4.33 +
    4.34 +    @close_time = time - @flip_interval
    4.35 +    @match_time = @open_time = @close_time - 1
    4.36 +  end
    4.37 +
    4.38 +  def symbol
    4.39 +    @symbol
    4.40 +  end
    4.41 +
    4.42 +  def angle
    4.43 +    @angle
    4.44 +  end
    4.45 +
    4.46 +  def open(time:long):void
    4.47 +    @open_time = time  unless opened?
    4.48 +  end
    4.49 +
    4.50 +  def close(time:long):void
    4.51 +    @close_time = time  if opened?
    4.52 +  end
    4.53 +
    4.54 +  def match(time:long):void
    4.55 +    @match_time = time  unless matched?
    4.56 +  end
    4.57 +
    4.58 +  def opened?
    4.59 +    @close_time - @open_time <= 0
    4.60 +  end
    4.61 +
    4.62 +  def matched?
    4.63 +    @close_time - @match_time <= 0
    4.64 +  end
    4.65 +
    4.66 +  # -1.0 - closed, 1.0 - opened.
    4.67 +  def flip_state(time:long)
    4.68 +    if opened?
    4.69 +      s = 1.0
    4.70 +    else
    4.71 +      s = -1.0
    4.72 +    end
    4.73 +
    4.74 +    t = Math.max(1, time - Math.max(@close_time, @open_time))
    4.75 +    
    4.76 +    return s  if t >= @flip_interval
    4.77 +
    4.78 +    s * (t * 2 - @flip_interval) / double(@flip_interval)
    4.79 +  end   
    4.80 +  
    4.81 +  # 1.0 - visible, 0.0 - removed.
    4.82 +  def visible_state(time:long)
    4.83 +    return 1.0  if !matched?
    4.84 +
    4.85 +    t = Math.max(1, time - @match_time - @flip_interval / 2)
    4.86 +    Math.max(0.0, 1.0 - t / double(@remove_interval))
    4.87 +  end
    4.88 +
    4.89 +  def auto_close?(time:long)
    4.90 +    opened? and time - @open_time > 2E9
    4.91 +  end
    4.92 +
    4.93 +  def toString
    4.94 +    String.valueOf(@symbol) + 
    4.95 +      if matched?
    4.96 +        '[matched]'
    4.97 +      elsif opened?
    4.98 +        '[opened]'
    4.99 +      else
   4.100 +        ''
   4.101 +      end     
   4.102 +  end
   4.103 +end
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/src/net/kryshen/charamega/field.mirah	Sat Jul 14 05:46:29 2012 +0400
     5.3 @@ -0,0 +1,243 @@
     5.4 +#
     5.5 +#  Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net>
     5.6 +#
     5.7 +#  This file is part of Charamega.
     5.8 +#
     5.9 +#  Charamega is free software: you can redistribute it and/or modify
    5.10 +#  it under the terms of the GNU General Public License as published by
    5.11 +#  the Free Software Foundation, either version 3 of the License, or
    5.12 +#  (at your option) any later version.
    5.13 +#
    5.14 +#  Charamega is distributed in the hope that it will be useful,
    5.15 +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    5.16 +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    5.17 +#  GNU General Public License for more details.
    5.18 +#
    5.19 +#  You should have received a copy of the GNU General Public License
    5.20 +#  along with Charamega.  If not, see <http://www.gnu.org/licenses/>.
    5.21 +#
    5.22 +
    5.23 +package net.kryshen.charamega
    5.24 +
    5.25 +import java.awt.*
    5.26 +import java.awt.geom.*
    5.27 +import java.awt.event.*
    5.28 +import javax.swing.*
    5.29 +import java.util.List
    5.30 +
    5.31 +class Field < JComponent
    5.32 +
    5.33 +  def initialize(game:Game)
    5.34 +    @field_color = Color.WHITE
    5.35 +    @border_color = Color.GRAY
    5.36 +    @active_border_color = Color.DARK_GRAY
    5.37 +    @symbol_color = Color.BLACK
    5.38 +    @back_color = Color.LIGHT_GRAY
    5.39 +    @face_color = Color.new(0xFFFDDD)
    5.40 +
    5.41 +    @game = game 
    5.42 +
    5.43 +    setOpaque true
    5.44 +    setDoubleBuffered true
    5.45 +
    5.46 +    begin
    5.47 +      source = getClass.getResource("DejaVuSans.ttf").openStream
    5.48 +      font = Font.createFont(Font.TRUETYPE_FONT, source)
    5.49 +    ensure
    5.50 +      source.close  unless source.nil?
    5.51 +    end
    5.52 +    setFont font
    5.53 +
    5.54 +    enableEvents AWTEvent.MOUSE_EVENT_MASK
    5.55 +    enableEvents AWTEvent.MOUSE_MOTION_EVENT_MASK
    5.56 +  end
    5.57 +
    5.58 +  def processMouseEvent(event)
    5.59 +    if isEnabled
    5.60 +      if event.getID == MouseEvent.MOUSE_PRESSED
    5.61 +        
    5.62 +        @hold = hit(event.getX, event.getY)
    5.63 +        
    5.64 +      elsif event.getID == MouseEvent.MOUSE_RELEASED and
    5.65 +          !@hold.nil? and @hold == hit(event.getX, event.getY)
    5.66 +        
    5.67 +        @game.open @hold
    5.68 +        repaint
    5.69 +      elsif event.getID == MouseEvent.MOUSE_EXITED and
    5.70 +          !@hovered.nil?
    5.71 +        
    5.72 +        @hovered = nil
    5.73 +        repaint
    5.74 +      end
    5.75 +    end
    5.76 +
    5.77 +    super
    5.78 +  end
    5.79 +  
    5.80 +  def processMouseMotionEvent(event)
    5.81 +    if isEnabled
    5.82 +      h = hit(event.getX, event.getY)
    5.83 +      
    5.84 +      if event.getID == MouseEvent.MOUSE_DRAGGED
    5.85 +        if h == @hold and h != @hovered
    5.86 +          @hovered = h
    5.87 +          repaint
    5.88 +        end
    5.89 +        
    5.90 +        if h != @hold and @hold == @hovered
    5.91 +          @hovered = nil
    5.92 +          repaint
    5.93 +        end
    5.94 +        
    5.95 +      elsif h != @hovered
    5.96 +        @hovered = h
    5.97 +        repaint
    5.98 +      end
    5.99 +    end
   5.100 +
   5.101 +    super
   5.102 +  end
   5.103 +
   5.104 +  def paintComponent(g1)
   5.105 +    g = Graphics2D(g1)
   5.106 +
   5.107 +    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
   5.108 +                        RenderingHints.VALUE_ANTIALIAS_ON)
   5.109 +    g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
   5.110 +                       RenderingHints.VALUE_FRACTIONALMETRICS_ON)
   5.111 +
   5.112 +    w = getWidth
   5.113 +    h = getHeight
   5.114 +   
   5.115 +    g.setColor @field_color
   5.116 +    g.fillRect 0, 0, w, h
   5.117 +
   5.118 +    insets = getInsets
   5.119 +    w -= insets.left + insets.right
   5.120 +    h -= insets.top + insets.bottom
   5.121 +
   5.122 +    layout = @game.layout(w, h)
   5.123 +    time = System.nanoTime
   5.124 +
   5.125 +    max_cols = Math.ceil(double(@game.cards.size) / layout.size)
   5.126 +    rh = double(h) / layout.size
   5.127 +    card_size = Math.min(rh, double(w) / max_cols) / 1.4
   5.128 +    font_size = card_size
   5.129 +    font = g.getFont.deriveFont(float(font_size))
   5.130 +    frc = g.getFontRenderContext
   5.131 +
   5.132 +    rm_scale = 2.0
   5.133 +
   5.134 +    outer = Rectangle2D.Double.new
   5.135 +    inner = Rectangle2D.Double.new
   5.136 +    border = Path2D.Double.new(Path2D.WIND_EVEN_ODD)
   5.137 +
   5.138 +    save_t = g.getTransform
   5.139 +
   5.140 +    y = rh / 2 + insets.top
   5.141 +    layout.each do |row|
   5.142 +      cw = double(w) / List(row).size
   5.143 +      x = cw / 2 + insets.left
   5.144 +
   5.145 +      List(row).each do |e|
   5.146 +        card = Card(e)
   5.147 +
   5.148 +        v = card.visible_state(time)
   5.149 +
   5.150 +        # Compute area potentially affected by the card.
   5.151 +        if v < 1.0
   5.152 +          paint_x = int(Math.floor(x - cw * rm_scale / 2 - 1))
   5.153 +          paint_y = int(Math.floor(y - rh * rm_scale / 2 - 1))
   5.154 +          paint_w = int(Math.ceil(cw * rm_scale + 2))
   5.155 +          paint_h = int(Math.ceil(rh * rm_scale + 2))
   5.156 +        else
   5.157 +          paint_x = int(Math.floor(x - cw / 2 - 1))
   5.158 +          paint_y = int(Math.floor(y - rh / 2 - 1))
   5.159 +          paint_w = int(Math.ceil(cw + 2))
   5.160 +          paint_h = int(Math.ceil(rh + 2))
   5.161 +        end
   5.162 +
   5.163 +        if v > 0.0 and g.hitClip(paint_x, paint_y, paint_w, paint_h)
   5.164 +          f = card.flip_state(time)
   5.165 +
   5.166 +          if v < 1.0 or (f > -1.0 and f < 1.0)
   5.167 +            # Animation is in progress.
   5.168 +            repaint 30, paint_x, paint_y, paint_w, paint_h
   5.169 +          end
   5.170 +
   5.171 +          g.translate x, y
   5.172 +
   5.173 +          v_scale = rm_scale - Math.sqrt(v) * rm_scale
   5.174 +          g.scale Math.abs(f) + v_scale, 1.0 + v_scale
   5.175 +
   5.176 +          g.rotate card.angle * v
   5.177 +
   5.178 +          outer.setRect(-card_size / 2, -card_size / 2,
   5.179 +                        card_size, card_size)
   5.180 +          inner.setRect(1 - card_size / 2, 1 - card_size / 2,
   5.181 +                        card_size - 2, card_size - 2)
   5.182 +          border.reset
   5.183 +          border.append outer, false
   5.184 +          border.append inner, false
   5.185 +          
   5.186 +          if f > 0
   5.187 +            g.setColor with_alpha(@face_color, v)
   5.188 +          else
   5.189 +            g.setColor with_alpha(@back_color, v)
   5.190 +          end
   5.191 +
   5.192 +          g.fill inner
   5.193 +
   5.194 +          if f > 0
   5.195 +            g.setColor with_alpha(@symbol_color, Math.sqrt(v))
   5.196 +
   5.197 +            gv = font.createGlyphVector(frc, String.valueOf(card.symbol))
   5.198 +            sb = gv.getVisualBounds
   5.199 +
   5.200 +            # Scale down if the glyph does not fit.
   5.201 +            tolerance = 0.95
   5.202 +            scale = Math.min(inner.getWidth * tolerance / sb.getWidth,
   5.203 +                             inner.getHeight * tolerance / sb.getHeight)
   5.204 +            save_t_2 = g.getTransform
   5.205 +            g.scale scale, scale  if scale < 1.0
   5.206 +
   5.207 +            g.drawGlyphVector(gv, 
   5.208 +                              -float(sb.getX + sb.getWidth / 2),
   5.209 +                              -float(sb.getY + sb.getHeight / 2))
   5.210 +
   5.211 +            g.setTransform save_t_2
   5.212 +          end
   5.213 +
   5.214 +          if card == @hovered
   5.215 +            g.setColor with_alpha(@active_border_color, v)
   5.216 +          else
   5.217 +            g.setColor with_alpha(@border_color, v)
   5.218 +          end
   5.219 +
   5.220 +          g.fill border
   5.221 +
   5.222 +          g.setTransform save_t
   5.223 +        end
   5.224 +
   5.225 +        x += cw
   5.226 +      end
   5.227 +      
   5.228 +      y += rh
   5.229 +    end
   5.230 +  end
   5.231 +
   5.232 +  private
   5.233 +  
   5.234 +  def hit(x:int, y:int)
   5.235 +    insets = getInsets
   5.236 +    size = getSize
   5.237 +    @game.hit(x - insets.left,
   5.238 +              y - insets.top,
   5.239 +              size.width - insets.left - insets.right,
   5.240 +              size.height - insets.top - insets.bottom)
   5.241 +  end
   5.242 +
   5.243 +  def with_alpha(c:Color, alpha:double)
   5.244 +    Color.new c.getRed, c.getGreen, c.getBlue, int(alpha * 255)
   5.245 +  end
   5.246 +end
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/src/net/kryshen/charamega/game.mirah	Sat Jul 14 05:46:29 2012 +0400
     6.3 @@ -0,0 +1,384 @@
     6.4 +#
     6.5 +#  Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net>
     6.6 +#
     6.7 +#  This file is part of Charamega.
     6.8 +#
     6.9 +#  Charamega is free software: you can redistribute it and/or modify
    6.10 +#  it under the terms of the GNU General Public License as published by
    6.11 +#  the Free Software Foundation, either version 3 of the License, or
    6.12 +#  (at your option) any later version.
    6.13 +#
    6.14 +#  Charamega is distributed in the hope that it will be useful,
    6.15 +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    6.16 +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    6.17 +#  GNU General Public License for more details.
    6.18 +#
    6.19 +#  You should have received a copy of the GNU General Public License
    6.20 +#  along with Charamega.  If not, see <http://www.gnu.org/licenses/>.
    6.21 +#
    6.22 +
    6.23 +package net.kryshen.charamega
    6.24 +
    6.25 +import java.util.Collection
    6.26 +import java.util.List
    6.27 +import java.util.ArrayList
    6.28 +import java.util.Collections
    6.29 +
    6.30 +class Game
    6.31 +
    6.32 +  def initialize
    6.33 +    @symbols = ArrayList.new
    6.34 +
    6.35 +    add_range 0x03B1, 0x03C9                 # greek
    6.36 +    add_all [0x0439, 0x044A, 0x0463, 0x0467] # cyrillic
    6.37 +    add_range 0x20A0, 0x20B5                 # currency
    6.38 +    add_all [0x2190, 0x2194, 0x21CC]         # arrows
    6.39 +    add_all [0x221A, 0x221E, 0x222B, 0x222E] # math
    6.40 +    add_all [0x2318, 0x23CE, 0x23CF, 0x23E3]
    6.41 +    add_all [0x25A8, 0x25A9]
    6.42 +    add_range 0x25C6, 0x25D0
    6.43 +    add_all [0x25D4]
    6.44 +    add_range 0x2600, 0x261A
    6.45 +    add_range 0x2620, 0x2630
    6.46 +    add_range 0x2638, 0x2672
    6.47 +    add_range 0x267A, 0x2689
    6.48 +    add_range 0x2690, 0x269C
    6.49 +    add_all [0x26A0, 0x26A1]
    6.50 +    add_all [0x2706, 0x2707, 0x2708, 0x2709]
    6.51 +    add_all [0x270C, 0x270D, 0x270E]
    6.52 +    add_all [0x2744]
    6.53 +
    6.54 +    self.players = 1
    6.55 +  end
    6.56 +
    6.57 +  def shuffle
    6.58 +    return false  if @shuffled
    6.59 +    shuffle @cards.size / 2
    6.60 +
    6.61 +    true
    6.62 +  end
    6.63 +
    6.64 +  def shuffle(npairs:int):void
    6.65 +    Collections.shuffle @symbols
    6.66 +    
    6.67 +    cards = ArrayList.new npairs    
    6.68 +    time = System.nanoTime
    6.69 +
    6.70 +    @symbols.subList(0, npairs).each do |c|
    6.71 +      cards.add Card.new Character(c).charValue, time
    6.72 +      cards.add Card.new Character(c).charValue, time
    6.73 +    end
    6.74 +    
    6.75 +    Collections.shuffle cards
    6.76 +    @cards = cards
    6.77 +    @layout = List(nil)
    6.78 +    @first = @second = Card(nil)
    6.79 +    @non_matching = 0
    6.80 +    @seconds = 0
    6.81 +    @shuffled = true
    6.82 +
    6.83 +    compute_limits
    6.84 +  end
    6.85 +
    6.86 +  def players=(nplayers:int):void
    6.87 +    @matches = int[nplayers]
    6.88 +    @player = 0
    6.89 +  end
    6.90 +
    6.91 +  def cards
    6.92 +    @cards
    6.93 +  end
    6.94 +
    6.95 +  def start:void
    6.96 +    shuffle
    6.97 +    @shuffled = false
    6.98 +
    6.99 +    @matches.length.times { |i| @matches[i] = 0 }
   6.100 +    @player = 0
   6.101 +
   6.102 +    @start_time = System.nanoTime
   6.103 +    @playing = true
   6.104 +  end
   6.105 +
   6.106 +  def stop:void
   6.107 +    @playing = false
   6.108 +
   6.109 +    unless @shuffled
   6.110 +      time = System.nanoTime
   6.111 +      delay = long(2E8 / @cards.size)
   6.112 +      
   6.113 +      @layout.each do |row|
   6.114 +        List(row).each do |e|
   6.115 +          card = Card(e)
   6.116 +          card.open time  unless card.matched?
   6.117 +          time += delay
   6.118 +        end
   6.119 +      end
   6.120 +    end
   6.121 +  end
   6.122 +
   6.123 +  # Returns list of lists of cards for each row.
   6.124 +  def layout(w:int, h:int):List
   6.125 +    return @layout  unless @layout.nil? or @shuffled
   6.126 +
   6.127 +    n = @cards.size
   6.128 +    cols = columns(float(w) / h)
   6.129 +    rows = int(Math.ceil float(n) / cols)
   6.130 +
   6.131 +    if !@layout.nil? and 
   6.132 +        @layout_cols == cols and 
   6.133 +        @layout_rows == rows
   6.134 +
   6.135 +      return @layout
   6.136 +    end
   6.137 +
   6.138 +    layout = ArrayList.new rows
   6.139 +
   6.140 +    d = cols * rows - n
   6.141 +    c = 0
   6.142 +    rows.times do |i|
   6.143 +      k = cols
   6.144 +      k -= 1  if i >= rows - d
   6.145 +      
   6.146 +      row = ArrayList.new k
   6.147 +      k.times do
   6.148 +        row.add @cards.get(c)
   6.149 +        c += 1
   6.150 +      end
   6.151 +
   6.152 +      layout.add row
   6.153 +    end
   6.154 +
   6.155 +    Collections.shuffle layout
   6.156 +
   6.157 +    @layout_cols = cols
   6.158 +    @layout_rows = rows
   6.159 +    @layout = layout
   6.160 +  end
   6.161 +
   6.162 +  # Called periodically by UI.
   6.163 +  def update
   6.164 +    time = System.nanoTime
   6.165 +    @seconds = int((time - @start_time) / 1E9)
   6.166 +
   6.167 +    # Flip the cards back after some time.
   6.168 +    if !@second.nil? and
   6.169 +        @first.auto_close?(time) and @second.auto_close?(time)
   6.170 +
   6.171 +      @first.close time
   6.172 +      @second.close time
   6.173 +      return true
   6.174 +    end
   6.175 +
   6.176 +    false
   6.177 +  end
   6.178 +
   6.179 +  # Number of removed pairs.
   6.180 +  def matched
   6.181 +    matched = 0
   6.182 +    @matches.each { |i| matched += i }
   6.183 +    
   6.184 +    matched
   6.185 +  end
   6.186 +
   6.187 +  def finished?
   6.188 +    ignore_limits = false
   6.189 +    # ignore_limits = true
   6.190 +
   6.191 +    matched * 2 == @cards.size or
   6.192 +      (!multiplayer? and !ignore_limits and
   6.193 +       (@non_matching > @max_non_matching or
   6.194 +        @seconds >= @max_seconds))
   6.195 +  end
   6.196 +
   6.197 +  def winner
   6.198 +    max = -1
   6.199 +    winner = -1
   6.200 +
   6.201 +    @matches.length.times do |i|
   6.202 +      if @matches[i] > max
   6.203 +        max = @matches[i]
   6.204 +        winner = i
   6.205 +      elsif @matches[i] == max
   6.206 +        winner = -1
   6.207 +      end
   6.208 +    end
   6.209 +    
   6.210 +    winner
   6.211 +  end
   6.212 +
   6.213 +  def multiplayer?
   6.214 +    @matches.length > 1
   6.215 +  end
   6.216 +
   6.217 +  def status
   6.218 +    if multiplayer?
   6.219 +      sb = StringBuffer.new
   6.220 +      sb.append 'Scores: '
   6.221 +      @matches.length.times do |i|
   6.222 +        sb.append ', '  if i > 0
   6.223 +        sb.append '('  if i == @player
   6.224 +        sb.append String.valueOf(@matches[i])
   6.225 +        sb.append ')'  if i == @player
   6.226 +      end
   6.227 +      sb.append ". "
   6.228 +
   6.229 +      if finished?
   6.230 +        w = winner
   6.231 +        sb.append "Player #{w + 1} wins!"  if w >= 0
   6.232 +        sb.append "Tie!"  if w < 0
   6.233 +      else
   6.234 +        sb.append "Player #{@player + 1}'s turn."
   6.235 +      end
   6.236 +
   6.237 +      sb.toString
   6.238 +    else
   6.239 +      sec = @max_seconds - @seconds
   6.240 +      time = Integer[2]
   6.241 +      time[0] = Integer.valueOf(sec / 60)
   6.242 +      time[1] = Integer.valueOf(sec % 60)      
   6.243 +      
   6.244 +      sb = StringBuffer.new
   6.245 +      sb.append String.format("Time left: %02d:%02d.", time)
   6.246 +      sb.append "  Non-matching: #{@non_matching}"
   6.247 +      sb.append " / #{@max_non_matching}."
   6.248 +
   6.249 +      left = @cards.size / 2 - matched
   6.250 +
   6.251 +      if left == 0
   6.252 +        sb.append "  You win!"
   6.253 +        puts "#{@cards.size / 2},#{@non_matching},#{@seconds}"
   6.254 +      else
   6.255 +        sb.append "  Pars left: #{left}."
   6.256 +      end
   6.257 +
   6.258 +      sb.toString
   6.259 +    end
   6.260 +  end
   6.261 +
   6.262 +  def open(card:Card):boolean
   6.263 +    return false  if finished?
   6.264 +    return false  if card.matched?
   6.265 +    return false  if card.opened? and (@first.nil? or @second.nil?)
   6.266 +
   6.267 +    time = System.nanoTime
   6.268 +
   6.269 +    if @first.nil?
   6.270 +      @first = card.open time
   6.271 +    elsif @second.nil?
   6.272 +      @second = card.open time
   6.273 +    else
   6.274 +      @first.close time
   6.275 +      @second.close time
   6.276 +      @first = card.open time
   6.277 +      @second = nil
   6.278 +    end
   6.279 +
   6.280 +    unless @second.nil? 
   6.281 +      if @first.symbol == @second.symbol
   6.282 +        @first.match time
   6.283 +        @second.match time
   6.284 +        @first = @second = nil
   6.285 +        @matches[@player] += 1
   6.286 +      else
   6.287 +        @non_matching += 1
   6.288 +        @player = (@player + 1) % @matches.length
   6.289 +      end
   6.290 +    end
   6.291 +
   6.292 +    true
   6.293 +  end
   6.294 +
   6.295 +  def hit(x:int, y:int, w:int, h:int)
   6.296 +    return nil  if x < 0 or y < 0
   6.297 +
   6.298 +    layout = layout(w, h)
   6.299 +    i = y * layout.size / h
   6.300 +
   6.301 +    return nil  if i >= layout.size
   6.302 +
   6.303 +    row = List(layout.get i)
   6.304 +    j = x * row.size / w
   6.305 +    
   6.306 +    return nil  if j >= row.size
   6.307 +
   6.308 +    Card(row.get j)
   6.309 +  end
   6.310 +
   6.311 +  private
   6.312 +
   6.313 +  def compute_limits
   6.314 +    pairs = @cards.size / 2
   6.315 +    
   6.316 +    @max_non_matching = 0
   6.317 +    @max_seconds = 2
   6.318 +
   6.319 +    # No need to optimize this.
   6.320 +    pairs.downto(3) do |i|
   6.321 +      if i > 180
   6.322 +        @max_non_matching += 9
   6.323 +      elsif i > 150
   6.324 +        @max_non_matching += 8
   6.325 +      elsif i > 120
   6.326 +        @max_non_matching += 7
   6.327 +      elsif i > 90
   6.328 +        @max_non_matching += 6
   6.329 +      elsif i > 60
   6.330 +        @max_non_matching += 5
   6.331 +      elsif i > 30
   6.332 +        @max_non_matching += 4
   6.333 +      elsif i > 20
   6.334 +        @max_non_matching += 3
   6.335 +      elsif i > 8
   6.336 +        @max_non_matching += 2
   6.337 +      else
   6.338 +        @max_non_matching += 1
   6.339 +      end
   6.340 +      
   6.341 +      if i > 160
   6.342 +        @max_seconds += 30
   6.343 +      elsif i > 120
   6.344 +        @max_seconds += 25
   6.345 +      elsif i > 80
   6.346 +        @max_seconds += 20
   6.347 +      elsif i > 60
   6.348 +        @max_seconds += 15
   6.349 +      elsif i > 40
   6.350 +        @max_seconds += 12
   6.351 +      elsif i > 20
   6.352 +        @max_seconds += 9
   6.353 +      elsif i > 9
   6.354 +        @max_seconds += 6
   6.355 +      elsif i > 5
   6.356 +        @max_seconds += 4
   6.357 +      else
   6.358 +        @max_seconds += 2
   6.359 +      end
   6.360 +    end
   6.361 +  end
   6.362 +
   6.363 +  def columns(aspect:float)
   6.364 +    n = @cards.size
   6.365 +    c = Math.max(1, int(Math.ceil Math.sqrt(n * aspect)))
   6.366 +    
   6.367 +    loop do
   6.368 +      m = n % c
   6.369 +      break  if m == 0 or c - m <= n / c
   6.370 +      c -= 1
   6.371 +    end
   6.372 +
   6.373 +    c
   6.374 +  end
   6.375 +
   6.376 +  def add_range(from:int, to:int):void
   6.377 +    from.upto(to) do |i|
   6.378 +      @symbols.add Character.new char(i)
   6.379 +    end
   6.380 +  end
   6.381 +  
   6.382 +  def add_all(xs:Collection)
   6.383 +    xs.each do |i|
   6.384 +      @symbols.add Character.new char(Number(i).intValue)
   6.385 +    end
   6.386 +  end
   6.387 +end
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/src/net/kryshen/charamega/ui.mirah	Sat Jul 14 05:46:29 2012 +0400
     7.3 @@ -0,0 +1,263 @@
     7.4 +#
     7.5 +#  Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net>
     7.6 +#
     7.7 +#  This file is part of Charamega.
     7.8 +#
     7.9 +#  Charamega is free software: you can redistribute it and/or modify
    7.10 +#  it under the terms of the GNU General Public License as published by
    7.11 +#  the Free Software Foundation, either version 3 of the License, or
    7.12 +#  (at your option) any later version.
    7.13 +#
    7.14 +#  Charamega is distributed in the hope that it will be useful,
    7.15 +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    7.16 +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    7.17 +#  GNU General Public License for more details.
    7.18 +#
    7.19 +#  You should have received a copy of the GNU General Public License
    7.20 +#  along with Charamega.  If not, see <http://www.gnu.org/licenses/>.
    7.21 +#
    7.22 +
    7.23 +package net.kryshen.charamega
    7.24 +
    7.25 +import java.awt.*
    7.26 +import java.awt.event.*
    7.27 +import javax.swing.*
    7.28 +import java.net.URI
    7.29 +
    7.30 +class Ui < JPanel
    7.31 +  @@title = 'Charamega'
    7.32 +  @@version = '0.9'
    7.33 +  @@home = 'http://kryshen.net/games/'
    7.34 +
    7.35 +  def initialize(root_pane:JRootPane)
    7.36 +    super LayoutManager(BorderLayout.new)
    7.37 +
    7.38 +    @game = Game.new.shuffle(20)
    7.39 +    @field = Field.new(@game)
    7.40 +    @field.setBorder BorderFactory.createEmptyBorder(5, 5, 5, 5)
    7.41 +
    7.42 +    add @field, BorderLayout.CENTER
    7.43 +    add create_status, BorderLayout.SOUTH
    7.44 +
    7.45 +    ui = self
    7.46 +    @timer = Timer.new(250) { |e| ui.update }
    7.47 +
    7.48 +    @conf = create_configurator
    7.49 +    root_pane.getLayeredPane.add @conf, JLayeredPane.MODAL_LAYER
    7.50 +    root_pane.setDefaultButton @start
    7.51 +
    7.52 +    stop
    7.53 +    update_status
    7.54 +  end
    7.55 +
    7.56 +  def doLayout
    7.57 +    @conf.setSize @conf.getPreferredSize
    7.58 +    @conf.setLocation int(getWidth * 0.05), int(getHeight * 0.05)
    7.59 +    super
    7.60 +  end
    7.61 +
    7.62 +  def update
    7.63 +    @field.repaint  if @game.update
    7.64 +    stop  if @game.finished?
    7.65 +    update_status
    7.66 +  end
    7.67 +
    7.68 +  def update_status
    7.69 +    @status.setText(@game.status)
    7.70 +  end
    7.71 +
    7.72 +  def start
    7.73 +    @conf.setVisible false
    7.74 +    @field.setEnabled true
    7.75 +    @start.setEnabled false
    7.76 +    @stop.setEnabled true
    7.77 +    @game.start
    7.78 +    @timer.start
    7.79 +    @field.repaint
    7.80 +  end
    7.81 +  
    7.82 +  def stop
    7.83 +    @timer.stop
    7.84 +    @game.stop
    7.85 +    @stop.setEnabled false
    7.86 +    @start.setEnabled true
    7.87 +    @field.setEnabled false
    7.88 +    @conf.setVisible true
    7.89 +    @field.repaint
    7.90 +  end
    7.91 +
    7.92 +  private
    7.93 +
    7.94 +  def create_status
    7.95 +    ui = self
    7.96 +
    7.97 +    status_panel = JPanel.new
    7.98 +    layout = GroupLayout.new(status_panel)
    7.99 +    status_panel.setLayout layout
   7.100 +
   7.101 +    @status = JLabel.new('', SwingConstants.RIGHT)
   7.102 +    @stop = JButton.new('New game')
   7.103 +    @stop.addActionListener do |e|
   7.104 +      ui.update_status
   7.105 +      ui.stop
   7.106 +    end
   7.107 +    
   7.108 +    gap = 5
   7.109 +
   7.110 +    h_group = layout.createSequentialGroup
   7.111 +      .addGap(1, gap, gap)
   7.112 +      .addComponent(@stop)
   7.113 +      .addGap(1, gap, gap)
   7.114 +      .addComponent(@status, 
   7.115 +                    GroupLayout.DEFAULT_SIZE,
   7.116 +                    GroupLayout.DEFAULT_SIZE,
   7.117 +                    Short.MAX_VALUE)
   7.118 +      .addGap(1, gap, gap)
   7.119 +
   7.120 +    v_group = layout.createSequentialGroup
   7.121 +      .addGap(1, gap, gap)
   7.122 +      .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
   7.123 +                  .addComponent(@stop)
   7.124 +                  .addComponent(@status))
   7.125 +      .addGap(1, gap, gap)
   7.126 +    
   7.127 +    layout.setHorizontalGroup h_group
   7.128 +    layout.setVerticalGroup v_group 
   7.129 +
   7.130 +    status_panel
   7.131 +  end
   7.132 +
   7.133 +  def create_configurator
   7.134 +    # Can't use object fields in closures in Mirah 0.11.
   7.135 +    ui = self
   7.136 +    game = @game
   7.137 +    field = @field
   7.138 +
   7.139 +    conf = JPanel.new
   7.140 +    layout = GroupLayout.new(conf)
   7.141 +    conf.setLayout layout
   7.142 +    conf.setBorder BorderFactory.createRaisedBevelBorder
   7.143 +
   7.144 +    layout.setAutoCreateGaps true
   7.145 +    layout.setAutoCreateContainerGaps true
   7.146 +
   7.147 +    title = JLabel.new(@@title)
   7.148 +      .setFont(Font.new('serif', Font.BOLD, 26))
   7.149 +
   7.150 +    version = JLabel.new("version #{@@version}")
   7.151 +    link = HyperlinkLabel.new(@@home)
   7.152 +
   7.153 +    players_label = JLabel.new('Players: ')
   7.154 +    pairs_label = JLabel.new('Pairs: ')
   7.155 +
   7.156 +    players = SpinnerNumberModel.new(1, 1, 20, 1)
   7.157 +    pairs = SpinnerNumberModel.new(20, 2, 200, 5)
   7.158 +
   7.159 +    players_spinner = JSpinner.new(players)
   7.160 +    pairs_spinner = JSpinner.new(pairs)
   7.161 +    
   7.162 +    players.addChangeListener do |e|
   7.163 +      game.players = players.getNumber.intValue
   7.164 +      field.repaint  if game.shuffle
   7.165 +      ui.update_status
   7.166 +    end
   7.167 +
   7.168 +    pairs.addChangeListener do |e|
   7.169 +      game.shuffle pairs.getNumber.intValue
   7.170 +      field.repaint
   7.171 +      ui.update_status
   7.172 +    end
   7.173 +
   7.174 +    @start = JButton.new("Start")
   7.175 +    @start.addActionListener do |e|
   7.176 +      ui.start
   7.177 +    end
   7.178 +
   7.179 +    h_group = layout.createParallelGroup
   7.180 +      .addGroup(layout.createSequentialGroup
   7.181 +                  .addComponent(title)
   7.182 +                  .addComponent(version))
   7.183 +      .addGroup(layout.createSequentialGroup
   7.184 +                  .addComponent(link,
   7.185 +                                GroupLayout.DEFAULT_SIZE,
   7.186 +                                GroupLayout.DEFAULT_SIZE,
   7.187 +                                GroupLayout.PREFERRED_SIZE)
   7.188 +                  .addGap(0, 0, Short.MAX_VALUE))
   7.189 +      .addGroup(layout.createSequentialGroup
   7.190 +                  .addGap(0, 0, Short.MAX_VALUE)
   7.191 +                  .addGroup(layout.createParallelGroup
   7.192 +                              .addComponent(pairs_label)
   7.193 +                              .addComponent(players_label))
   7.194 +                  .addGroup(layout.createParallelGroup
   7.195 +                              .addComponent(pairs_spinner)
   7.196 +                              .addComponent(players_spinner))
   7.197 +                  .addGap(0, 0, Short.MAX_VALUE))
   7.198 +      .addGroup(layout.createSequentialGroup
   7.199 +                  .addGap(0, 0, Short.MAX_VALUE)
   7.200 +                  .addComponent(@start))
   7.201 +
   7.202 +    v_group = layout.createSequentialGroup
   7.203 +      .addGroup(layout.createBaselineGroup(false, true)
   7.204 +                  .addComponent(title)
   7.205 +                  .addComponent(version))
   7.206 +      .addComponent(link)
   7.207 +      .addGap(25)
   7.208 +      .addGroup(layout.createBaselineGroup(false, true)
   7.209 +                  .addComponent(pairs_label)
   7.210 +                  .addComponent(pairs_spinner))
   7.211 +      .addGroup(layout.createBaselineGroup(false, true)
   7.212 +                  .addComponent(players_label)
   7.213 +                  .addComponent(players_spinner))
   7.214 +      .addGap(15)
   7.215 +      .addComponent(@start)
   7.216 +    
   7.217 +    layout.setHorizontalGroup h_group
   7.218 +    layout.setVerticalGroup v_group 
   7.219 +
   7.220 +    conf
   7.221 +  end
   7.222 +end
   7.223 +
   7.224 +class HyperlinkLabel < JLabel
   7.225 +
   7.226 +  def initialize(uri:String)
   7.227 +    initialize URI.new(uri), uri
   7.228 +  end
   7.229 +
   7.230 +  def initialize(uri:URI)
   7.231 +    initialize uri, uri.toString
   7.232 +  end
   7.233 +
   7.234 +  def initialize(uri:URI, text:String)
   7.235 +    @uri = uri
   7.236 +
   7.237 +    s = text.replace('&', '&amp;')
   7.238 +      .replace('<', '&lt;')
   7.239 +      .replace('>', '&gt;')
   7.240 +
   7.241 +    setText "<html><u>#{s}</u></html>"
   7.242 +    setForeground Color.BLUE.darker
   7.243 +    setCursor Cursor.new(Cursor.HAND_CURSOR)
   7.244 +
   7.245 +    enableEvents AWTEvent.MOUSE_EVENT_MASK
   7.246 +  end
   7.247 +
   7.248 +  def processMouseEvent(event)
   7.249 +    if event.getID == MouseEvent.MOUSE_CLICKED
   7.250 +      Desktop.getDesktop.browse @uri
   7.251 +    end
   7.252 +
   7.253 +    super
   7.254 +  end
   7.255 +end
   7.256 +
   7.257 +# UIManager.setLookAndFeel UIManager.getSystemLookAndFeelClassName
   7.258 +
   7.259 +frame = JFrame.new 'Charamega'
   7.260 +ui = Ui.new(frame.getRootPane)
   7.261 +
   7.262 +frame.getContentPane.add ui
   7.263 +frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
   7.264 +  .setSize(800, 600)
   7.265 +  .validate
   7.266 +  .setVisible(true)