2010.05.09 Sun [長年日記]
# [音楽] 「Y(寄り集まって)M(ミニ鍵盤いぢる)O(おぢさん達)」がかっこよすぎる
ビッグバンド風のホーンと手拍子で始まるYMO「Technopolis」のカバー曲。かなり前からたびたび耳にするけど誰のかわからなかったので調査。
「YMO-REMIXES TECHNOPOLIS 2000-01」の「TECHNOPOLIS(THE READYMADE)(DARLIN’OF DISCOTHEQUE TRACK)」。小西康陽さんの手によるもの。「うぅーーーーーやぁ」のやつでした。思い出した。開始40秒まではすきなんだけどなぁ。サンプリング元は大昔のアメリカTV番組「マニックス特捜網」の「END GAME」。
これ聴いたらブラスバンドによるYMOモノは無いものかと探したけどヒットせず。先生がど真ん中だとしても生徒さんはナニソレだし無理も無いか。
久しぶりにニコニコでYMOものを探してて見つけたのが、親子でTECHNOPOLIS 。
【ニコニコ動画】TECHNOPOLIS YMO(COVER)
出オチ&カバー絵が素敵。確かな演奏と飛び道具の絶妙なバランス。オチもあり。うらやましい親子競演。
次に見つけたのが「Y(寄り集まって)M(ミニ鍵盤いぢる)O(おぢさん達)」さんの作品群。既に8つ動画があがっていて全部聞きましたがどれも素晴らしい。特に「COSMIC SURFIN'」と「東風シダックス版」がいい。ミニ鍵盤でこんなに素敵な音が作れることにびっくり。全く弾けませんが、そんなに高くなさそうなので買おうかな。(各動画での演奏機材が紹介されてる動画あり)
2010.05.16 Sun [長年日記]
# [ゲーム] 将棋やりたい
先々週行われた羽生善治名人と三浦弘行八段の第68期名人戦第3局を少しだけテレビ観戦。中盤9五金いくのか?のあたり。解説者は渡辺明竜王、鈴木環那女流初段、木村一基八段。接戦でかつ解説が面白く(特に木村八段)急に将棋がやりたくなってしまいました。ただここで本やら将棋盤やら買い集めても冷めるのが早い私のことなので、まずは図書館で本を借りてきました。選んだのは、やりなおしの将棋 (岩波アクティブ新書)(先崎 学)、将棋上達の方程式 手筋の公式 基礎編(北島 忠雄)。あとついでに ボナンザVS勝負脳―最強将棋ソフトは人間を超えるか (角川oneテーマ21)(保木 邦仁/渡辺 明)も。
いくつか本を読んだら(読めたら)詰め将棋にも定評のある柿木将棋8あたりを買う予定。
# [日常] いろいろすっきり
「書道ガールズ」という映画の宣伝番組をなんとなく見てたら、女子高生役5人組の中の1人に見覚えがあるのだけれど思い出せない。かなり地味な子。調べたら高畑充希さんという役者の方。「みつき」名義で歌手活動もしていて、竹内まりやさんプロデュースで「夏のモンタージュ」という曲を出している。記憶にあったのはこのPVでの顔だ。「大切なもの」という曲も出していて、これが「セクシーボイスアンドロボ」の主題歌。そうだったのか。いろいろつながってスッキリ。ついでに「竹内まりあ」ではなく「竹内まりや」なんだとわかってスッキリ。
最近復活した「もやもや」が、第三舞台の「朝日のような夕日をつれて」で使われていた「THE END OF ASIA」。スタジオの音だったような気がするけどかなり昔のことなので不確か。「増殖」では絶対無く、「ダンスリー」も違う。「千のナイフ」じゃないと思うんだけど・・・調べたけどやっぱり不明。もやもや継続中。
# [Ruby] 「リバーシのアルゴリズム」をRubyで
コンピュータ将棋を調べてたら、探索アルゴリズムやら評価関数が気になりだした。とはいえ将棋を題材にそれらを学ぶのはハードル高すぎるのでまずはリバーシ(オセロ)で。ちょうど手元に リバーシのアルゴリズム C++&Java対応―「探索アルゴリズム」「評価関数」の設計と実装 (I・O BOOKS)(Seal Software) という未読本もあるしね。
ひたすらロジック部分の解説が続き、表示部分の説明は200ページの書籍中わずか11ページ。しかもコマンドライン版のみ。GUIはなし。今回の目的にうってつけの本。ついでにRubyのお勉強も。
テーブルゲームには、洗練されたアルゴリズムが使われています。本書は、テーブルゲーム「リバーシ」のアルゴリズムを題材に、C++とJavaを利用したプログラムの設計と実装について解説しています。
本書では、大きく分けて「ボードの設計と実装」、「探索アルゴリズム」、「評価関数」の順に解説しました。「ボードの設計と実装」では、オブジェクト指向をもとにリバーシのボードを表現するクラスを作成し、「探索アルゴリズム」では、テーブルゲーム必勝法探索の前提となるゲーム木について解説した後、探索の基盤となるアルゴリズムとαβ 法をはじめとするさまざまな高速化手法について解説しています。ソースリストは、C++とJavaで併記し、詳細にコメントしてあるので、入門者や中級者にも大いに参考になるでしょう。
[書籍情報―リバーシのアルゴリズムより引用]
とりあえず一読・・・難しい。サンプルコードをRubyに置き換えつつ、もう一度読む。
2010.05.19 Wed [長年日記]
# [Ruby] 「リバーシのアルゴリズム」をRubyで その2
前回のつづき。2章「ボードの設計と実装」のJavaでのサンプルコードを参考にRubyで書きました。1クラス1ファイル。識別子の命名は基本的に変更しない(set, get, isなどそのまま)。コードの見直しは探索アルゴリズム・評価関数・定石など全て書いた後に行うものとして、とりあえずサンプルーコードのまま忠実に書く。サンプルコードは出版社・著者のサポートページどちらでも公開していないっぽい。
point.rb
$KCODE = "s"
#
# 石の座標を表すクラス
#
class Point
attr_reader :x, :y
def initialize(x = 0, y = 0)
@x = x
@y = y
end
def to_s
s = ""
s << ('a'[0] + @x - 1).chr
s << @y.to_s
end
end
disc.rb
$KCODE = "s"
#
# 石を表すクラス
#
class Disc < Point
BLACK = 1
EMPTY = 0 # 空きマス
WHITE = -1
WALL = 2 # 壁
attr_accessor :x, :y, :color
def initialize(x = 0, y = 0, color = EMPTY)
super(x, y)
@color = color
end
end
colorstorage.rb
$KCODE = "s"
#
# 石の色(黒、白、空き)別の情報を格納するクラス
#
class ColorStorage
def initialize
@data = {}
end
def get(color)
@data[color]
end
def set(color, value)
@data[color] = value
end
end
board.rb
$KCODE = "s"
require 'point'
require 'disc'
require 'colorstorage'
#
# ボードを表すクラス
#
class Board
BOARD_SIZE = 8
MAX_TURNS = 60
# 方向
NONE = 0
UPPER = 1
UPPER_LEFT = 2
LEFT = 4
LOWER_LEFT = 8
LOWER = 16
LOWER_RIGHT = 32
RIGHT = 64
UPPER_RIGHT = 128
def initialize
init
end
#
# ボードをゲーム開始直後の状態にする。
# Boardクラスのインスタンスが生成された直後は、コンストラクタに
# よって同様の初期化処理が呼ばれているので、initを呼ぶ必要は無い
#
def init
#全マスを空きマスに設定
@raw_board = Array.new(BOARD_SIZE + 2){
Array.new(BOARD_SIZE + 2, Disc::EMPTY)
}
#壁の設定
@raw_board[0].fill(Disc::WALL)
@raw_board[BOARD_SIZE + 1].fill(Disc::WALL)
@raw_board.each do |row|
row[0] = Disc::WALL
row[-1] = Disc::WALL
end
#初期配置
@raw_board[4][4] = Disc::WHITE
@raw_board[5][5] = Disc::WHITE
@raw_board[4][5] = Disc::BLACK
@raw_board[5][4] = Disc::BLACK
# 各色の石の数
@discs = ColorStorage.new
#石数の初期設定
@discs.set(Disc::BLACK, 2)
@discs.set(Disc::WHITE, 2)
@discs.set(Disc::EMPTY, BOARD_SIZE * BOARD_SIZE - 4)
# 手数は0から数える
@turns = 0
# 先手は黒
@current_color = Disc::BLACK #先手黒
# @updateをすべて消去
@update_log = []
# 打てる手をの座標を格納
@movable_pos = Array.new(MAX_TURNS + 1){[]}
# 手数/全座標のcheck_mobilityの戻り値
@movable_dir = Array.new(MAX_TURNS + 1){
Array.new(BOARD_SIZE + 2){
Array.new(BOARD_SIZE + 2)
}
}
init_movable
end
attr_reader :movable_pos
#
# 指定した位置に石を打つ。
# 処理が成功したらtrue、失敗したらfalseが返る
#
def move(point)
if (point.x <= 0 or point.x > BOARD_SIZE or \
point.y <= 0 or point.y > BOARD_SIZE or \
@movable_dir[@turns][point.x][point.y] == NONE)
return false
end
flip_discs(point)
@turns += 1
@current_color = -@current_color
init_movable
return true
end
#
# 直前の一手を元に戻す。元に戻せない場合(一手も売っていない場合)
# falseが返る。
#
def undo
return false if @turns == 0
@current_color = -@current_color
update = @update_log.pop
if update.empty?
#前回はパス
#@movable_pos と @movable_dir を再構築
@movable_pos[@turns].clear
for x in 1..BOARD_SIZE
for y in 1..BOARD_SIZE
@movable_dir[@turns][x][y] = NONE
end
end
else
#前回はパスでない
@turns -= 1
#石を元に戻す
p = update[0]
@raw_board[p.x][p.y] = Disc::EMPTY
for i in 1...update.size
p = update[i]
@raw_board[p.x][p.y] = -@current_color
end
#石数の更新
disc_diff = update.size
@discs.set(@current_color, @discs.get(@current_color) - disc_diff)
@discs.set(-@current_color, @discs.get(-@current_color) + (disc_diff - 1))
@discs.set(Disc::EMPTY, @discs.get(Disc::EMPTY) + 1)
end
return true
end
#
# パスする。
# 成功したらtrueが返る。パスできない場合(打つ手がある場合)は
# falseが返る。
#
def pass
unless @movable_pos[@turns].size == 0
return false
end
if is_gameover
return false
end
@current_color = - @current_color
@update_log << []
init_movable
return true
end
#
# 指定された位置の色を返す
#
def get_color(point)
@raw_board[point.x][point.y]
end
#
# 現在の手番の石を返す
#
def get_current_color
@current_color
end
#
# 現在の手数を返す
#
def get_turns
@turns
end
#
# ゲームが終了していればtrue、終了していなければfalseを返す
#
def is_gameover
if @turns == MAX_TURNS
return true
end
unless @movable_pos[@turns].size == 0
return false
end
disc = Disc.new
disc.color = -@current_color
for x in 1..BOARD_SIZE
disc.x = x
for y in 1..BOARD_SIZE
disc.y = y
unless check_mobility(disc) == NONE
return false
end
end
end
return true
end
#
# 指定された石の数を返す。
# 色にはBLACK、WHITE、EMPTYを指定可能
#
def count_disc(color)
@discs.get(color)
end
#
# 石を打てる座標の配列を返す
#
def get_movable_pos
@movable_pos[@turns]
end
#
# 直前の手で打った石と裏返した石が並んだ配列を返す
#
def get_update
if @update_log.empty?
return []
else
return @update_log.last
end
end
private
#
# 指定した座標に石を打てるかどうか、また、
# どの方向に石を裏返せるかを判定する。
# 石を裏返らせる方向にフラグが立った整数値が返る
#
def check_mobility(disc)
# すでに石があったら置けない
unless @raw_board[disc.x][disc.y] == Disc::EMPTY
return NONE
end
dir = NONE
#上
if @raw_board[disc.x][disc.y - 1] == -disc.color
x = disc.x
y = disc.y - 2
while @raw_board[x][y] == -disc.color
y -= 1
end
if @raw_board[x][y] == disc.color
dir |= UPPER
end
end
#下
if @raw_board[disc.x][disc.y + 1] == -disc.color
x = disc.x
y = disc.y + 2
while @raw_board[x][y] == -disc.color
y += 1
end
if @raw_board[x][y] == disc.color
dir |= LOWER
end
end
#左
if @raw_board[disc.x - 1][disc.y] == -disc.color
x = disc.x - 2
y = disc.y
while @raw_board[x][y] == -disc.color
x -= 1
end
if @raw_board[x][y] == disc.color
dir |= LEFT
end
end
#右
if @raw_board[disc.x + 1][disc.y] == -disc.color
x = disc.x + 2
y = disc.y
while @raw_board[x][y] == -disc.color
x += 1
end
if @raw_board[x][y] == disc.color
dir |= RIGHT
end
end
#右上
if @raw_board[disc.x + 1][disc.y - 1] == -disc.color
x = disc.x + 2
y = disc.y - 2
while @raw_board[x][y] == -disc.color
x += 1
y -= 1
end
if @raw_board[x][y] == disc.color
dir |= UPPER_RIGHT
end
end
#左上
if @raw_board[disc.x - 1][disc.y - 1] == -disc.color
x = disc.x - 2
y = disc.y - 2
while @raw_board[x][y] == -disc.color
x -= 1
y -= 1
end
if @raw_board[x][y] == disc.color
dir |= UPPER_LEFT
end
end
#左下
if @raw_board[disc.x - 1][disc.y + 1] == -disc.color
x = disc.x - 2
y = disc.y + 2
while @raw_board[x][y] == -disc.color
x -= 1
y += 1
end
if @raw_board[x][y] == disc.color
dir |= LOWER_LEFT
end
end
#右下
if @raw_board[disc.x + 1][disc.y + 1] == -disc.color
x = disc.x + 2
y = disc.y + 2
while @raw_board[x][y] == -disc.color
x += 1
y += 1
end
if @raw_board[x][y] == disc.color
dir |= LOWER_RIGHT
end
end
return dir
end
#
# @movable_pos[@turns]と@movable_dir[@turns]を再計算する
def init_movable
@movable_pos[@turns].clear
for x in 1..BOARD_SIZE
for y in 1..BOARD_SIZE
disc = Disc.new(x, y, @current_color)
dir = check_mobility(disc)
unless dir == NONE
@movable_pos[@turns] << disc
end
@movable_dir[@turns][x][y] = dir
end
end
end
#
# 指定した位置に石を打ち、挟みこめる全ての石を裏返す。
# 打った石と裏返した石を@update_logに挿入する。
#
def flip_discs(point)
dir = @movable_dir[@turns][point.x][point.y]
update = []
@raw_board[point.x][point.y] = @current_color
update << Disc.new(point.x, point.y, @current_color)
#上
unless (dir & UPPER) == NONE #この位置における
y = point.y
until @raw_board[point.x][y - 1] == @current_color
y -= 1
@raw_board[point.x][y] = @current_color
update << Disc.new(point.x, y, @current_color)
end
end
#下
unless (dir & LOWER) == NONE
y = point.y
until @raw_board[point.x][y + 1] == @current_color
y += 1
@raw_board[point.x][y] = @current_color
update << Disc.new(point.x, y, @current_color)
end
end
#左
unless (dir & LEFT) == NONE
x = point.x
until @raw_board[x - 1][point.y] == @current_color
x -= 1
@raw_board[x][point.y] = @current_color
update << Disc.new(x, point.y, @current_color)
end
end
#右
unless (dir & RIGHT) == NONE
x = point.x
until @raw_board[x + 1][point.y] == @current_color
x += 1
@raw_board[x][point.y] = @current_color
update << Disc.new(x, point.y, @current_color)
end
end
#右上
unless (dir & UPPER_RIGHT) == NONE
x = point.x
y = point.y
until @raw_board[x + 1][y - 1] == @current_color
x += 1
y -= 1
@raw_board[x][y] = @current_color
update << Disc.new(x, y, @current_color)
end
end
#左上
unless (dir & UPPER_LEFT) == NONE
x = point.x
y = point.y
until @raw_board[x - 1][y - 1] == @current_color
x -= 1
y -= 1
@raw_board[x][y] = @current_color
update << Disc.new(x, y, @current_color)
end
end
#左下
unless (dir & LOWER_LEFT) == NONE
x = point.x
y = point.y
until @raw_board[x - 1][y + 1] == @current_color
x -= 1
y +=1
@raw_board[x][y] = @current_color
update << Disc.new(x, y, @current_color)
end
end
#右下
unless (dir & LOWER_RIGHT) == NONE
x = point.x
y = point.y
until @raw_board[x + 1][y + 1] == @current_color
x += 1
y += 1
@raw_board[x][y] = @current_color
update << Disc.new(x, y, @current_color)
end
end
disc_diff = update.size
@discs.set(@current_color, @discs.get(@current_color) + disc_diff)
@discs.set(-@current_color, @discs.get(-@current_color) - (disc_diff - 1))
@discs.set(Disc::EMPTY, @discs.get(Disc::EMPTY) - 1)
@update_log << update
end
end
boardtest.rb
$KCODE = "s"
require "board"
class ConsoleBoard < Board
def print_board
y_str = %w(1 2 3 4 5 6 7 8)
puts " abcdefgh"
for y in 1..8
print y_str.shift
for x in 1..8
case get_color(Point.new(x, y))
when Disc::BLACK
print "○"
when Disc::WHITE
print "●"
else
print " "
end
end
puts
end
end
end
board = ConsoleBoard.new
def str_to_ary(str)
if str.size < 2
return [0, 0]
else
x = str[0] - 'a'[0] + 1
y = str[1].chr.to_i
return [x, y]
end
end
loop do
board.print_board
print "黒石#{board.count_disc(Disc::BLACK)} "
print "白石#{board.count_disc(Disc::WHITE)} "
puts "空マス#{board.count_disc(Disc::EMPTY)}"
player = board.get_current_color == 1 ? "黒" : "白"
print "手を入力してください #{board.get_turns + 1}手目#{player}番: "
input_point = board.get_movable_pos.first
if input_point.nil?
board.pass
puts "pass"
puts
next
else
puts input_point.to_s
puts
end
=begin
input = STDIN.gets
case input.chomp!
when "p"
unless board.pass
puts "パスできません"
end
next
when "u"
board.undo
next
when "q"
break
end
begin
x, y = str_to_ary(input)
input_point = Point.new(x, y)
rescue
puts "リバーシ形式の手を入力してください"
next
end
=end
if not board.move(input_point)
puts "そこには置けません"
next
end
if board.is_gameover
puts "ゲーム終了"
board.print_board
print "黒石#{board.count_disc(Disc::BLACK)} "
print "白石#{board.count_disc(Disc::WHITE)} "
if board.count_disc(Disc::BLACK) == board.count_disc(Disc::WHITE)
puts "引き分け"
elsif board.count_disc(Disc::BLACK) > board.count_disc(Disc::WHITE)
puts "黒の勝ち"
else
puts "白の勝ち"
end
#board.movable_pos.each_with_index do |ary, i|
# printf "%3d=>%3d\n", i, ary.size
#end
break
end
end
# [Ruby] 「リバーシのアルゴリズム」をRubyで その3
つづいて第3章「探索アルゴリズム」。
この本の説明だけでは今ひとつ理解できなかったので、解説しているサイトを検索。以下のサイトの記事がわかりやすかった。
Algorithms with Python / ミニマックス法とαβ法
AIの実装/ミニマックス法 - Javaでゲーム作りますが何か?
Rubyでの実装は後ほど。
2010.05.20 Thu [長年日記]
# 「リバーシのアルゴリズム」をRubyで その4
前回のつづき。第3章「探索アルゴリズム」。
# -*- coding: sjis -*-
class AI
def move(board)
end
# 事前探索の先読み手数
PRESEARCH_DEPTH = 3
# 序盤・中盤の先読み手数
NORMAL_DEPTH = 15
# 必勝読みを始める残り手数
WLD_DEPTH = 15
# 完全読みを始める残り手数
PERFECT_DEPTH = 13
end
require 'point'
require 'disc'
require 'board'
class AlphaBetaAI < AI
MAX_VALUE = 2 ** 30 - 1
MIN_VALUE = -(2 ** 30)
class Move < Point
attr_reader :e
def initialize(x = 0, y = 0, e = 0)
super(x, y)
@e = e
end
end
def move(board)
movables = board.get_movable_pos
if movables.empty?
#打てる箇所がなければパスする
board.pass
return
end
if movables.size == 1
#打てる箇所が一つだけなら探索せず、即座に打って返る
board.move(movables[0])
return
end
# 事前に手をよさそうな順にソート
sort(board, movables, PRESEARCH_DEPTH)
if Board::MAX_TURNS - board.get_turns <= WLD_DEPTH
limit = MAX_VALUE
else
limit = NORMAL_DEPTH
end
eval_max = MIN_VALUE
for i in 0...movables.size
board.move(movables[i])
eval_tmp = -alphabeta(board, limit - 1, -MAX_VALUE, -MIN_VALUE)
board.undo
if eval_tmp > eval_max
eval_max = eval_tmp
point = movables[i]
end
end
board.move(point)
end
def alphabeta(board, limit, alpha, beta)
if board.is_gameover or limit == 0
return evaluate(board)
end
pos = board.get_movable_pos
if pos.size == 0
#パス
board.pass
eval_tmp = -alphabeta(board, limit, -beta, -alpha)
board.undo
return eval_tmp
end
for i in 0...pos.size
board.move(pos[i])
eval_tmp = -alphabeta(board, limit - 1, -beta, -alpha)
board.undo
alpha = [alpha, eval_tmp].max
if alpha >= beta
#beta刈り
return alpha
end
end
return alpha
end
def sort(board, movables, limit)
moves = []
for i in 0...movables.size
point = movables[i]
board.move(point)
eval_tmp = -alphabeta(board, limit - 1, MIN_VALUE, MAX_VALUE)
board.undo
move = Move.new(point.x, point.y, eval_tmp)
moves.push(move)
end
#評価tの大きい順にソート
moves_sorted = moves.sort do |a, b|
b.e <=> a.e
end
movables.clear
moves_sorted.each do |move|
movables << move
end
return
end
def evaluate(board)
# 評価関数は4章で実装
return 0
end
end
序盤・中盤の先読み手数15は、いろいろいじらないと無理っぽい予感。
2010.05.22 Sat [長年日記]
# [Ruby] 再帰による組み合わせの生成
4章「評価関数」のコードに、辺8個のマスの黒/白/空きの全ての組み合わせを生成する箇所がある。
def generate_edge(edge, count)
if count == Board::BOARD_SIZE
# このパターンは完成したので、局面のカウント
stat = EdgeStat.new
stat.get(Disc::BLACK).set(eval_edge(edge, Disc::BLACK))
stat.get(Disc::WHITE).set(eval_edge(edge, Disc::WHITE))
@@edge_table[idx_line(edge)] = stat
return
end
# 再帰的にすべてのパターンを網羅
edge[count] = Disc::EMPTY
generate_edge(edge, count + 1)
edge[count] = Disc::BLACK
generate_edge(edge, count + 1)
edge[count] = Disc::WHITE
generate_edge(edge, count + 1)
return
end
line = Array.new(8)
generate_edge(line, 0)
これで、
[0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 1] . . [2, 2, 2, 2, 2, 2, 2, 2]
までの3 ** 8 = 6561通りの組み合わせが生成されるのだけれども、はじめ動きが追えませんでした。情けない・・・単純化してようやく理解。
#サイズ3, 値2種類, 8通りの場合
def gen(line, count)
printf "%s%s, %s\n", " " * count, line.to_s, count.to_s
if count == 3
return
end
line[count] = "A"
gen(line, count + 1)
line[count] = "B"
gen(line, count + 1)
return
end
line = [nil, nil, nil]
gen(line, 0)
#=>
[nil, nil, nil], 0
["A", nil, nil], 1
["A", "A", nil], 2
["A", "A", "A"], 3
["A", "A", "B"], 3
["A", "B", "B"], 2
["A", "B", "A"], 3
["A", "B", "B"], 3
["B", "B", "B"], 1
["B", "A", "B"], 2
["B", "A", "A"], 3
["B", "A", "B"], 3
["B", "B", "B"], 2
["B", "B", "A"], 3
["B", "B", "B"], 3
# [NFL] 堂々の1位
大駒4枚落ちのカーディナルス。現時点で戦力的にどうなのか、気になるけれども素人には判断不能。そんなところへNFLニュース。
NFL JAPAN.COM|2010シーズン、プレイオフ戦線を占う
「昨季から一転、プレイオフ進出を逃す可能性が高いチーム」の堂々第1位にアリゾナ・カーディナルス・・・逆に「新たにプレイオフ進出が有力なチーム」の第2位にサンフランシスコ・49ers。49ersはフロント・コーチがごたごたしているらしいというニュースがあったのでニンマリしてたんだけど、そうですか。うー。
# [Ruby] 「リバーシのアルゴリズム」をRubyで その6
第4章「評価関数」の実装終了。
# -*- coding: sjis -*-
# evaluator.rb
require 'point'
require 'board'
#
# 完全読み用評価関数
#
class PerfectEvaluator
def evaluate(board)
board.get_current_color \
* (board.count_disc(Disc::BLACK) - board.count_disc(Disc::WHITE))
end
end
#
# 必勝読み用評価関数
#
class WLDEvaluator
WIN = 1
DRAW = 0
LOSE = -1
def evaluate(board)
disc_diff = board.get_current_color \
* (board.count_disc(Disc::BLACK) - board.count_disc(Disc::WHITE))
if disc_diff > 0
WIN
elsif disc_diff < 0
LOSE
else
DRAW
end
end
end
#
# 中盤の評価関数
# + 着手可能手数
# - 開放度
# - ウイング
# + 確定石
# - 隅が取られていない状態でのC打ち
# - 隅が取られていない状態でのX打ち
#
class MidEvaluator
#
# 辺に関するパラメータ
#
class EdgeParam
attr_accessor :stable, :wing, :mountain, :cmove
def initialize
# 確定石の個数
@stable = 0
# ウイングの個数
@wing = 0
# 山の個数
@mountain = 0
# EC
# CX
# 危険なC打ちの個数
@cmove = 0
end
def set(param)
@stable = param.stable
@wing = param.wing
@mountain = param.mountain
@cmove = param.cmove
end
def add(param)
@stable += param.stable
@wing += param.wing
@mountain += param.mountain
@cmove += param.cmove
return self
end
end
#
# 色別のEdgeParamオブジェクトを管理するクラス
#
class EdgeStat
attr_reader :data
def initialize
@data = {}
[Disc::BLACK, Disc::WHITE, Disc::EMPTY].each do |color|
@data[color] = EdgeParam.new
end
end
def add(stat)
@data.each do |color, param|
param.add(stat.data[color])
end
end
def get(color)
@data[color]
end
end
#
# 隅周辺に関するパラメータ
#
class CornerParam
attr_accessor :corner, :xmove
def initialize
@corner = 0 # 隅にある石の数
@xmove = 0 # 危険なX打ちの個数
end
end
#
# 色別のCornerオブジェクトを管理するクラス
#
class CornerStat
attr_reader :data
def initialize
@data = {}
[Disc::BLACK, Disc::WHITE, Disc::EMPTY].each do |color|
@data[color] = CornerParam.new
end
end
def get(color)
@data[color]
end
end
#
# 重み係数を規定する構造体
#
Weight = Struct.new(
:mobility_w, # 着手可能手数
:liberty_w, # 開放度
:stable_w, # 確定石
:wing_w, # ウイング
:xmove_w, # X打ち
:cmove_w # C打ち
)
#
# 辺のパターンの組み合わせ用インデックスのサイズ
# 3 ** 8 = 6561
#
TABLE_SIZE = 6561
# 辺のパターン毎のEdgeStatの配列
@@edge_table = Array.new(TABLE_SIZE){ EdgeStat.new }
@@table_init = false
def MidEvaluator.edge_table
@@edge_table
end
attr_reader :eval_weight
def initialize
# 初回起動時にテーブルを生成
unless @table_init
line = Array.new(Board::BOARD_SIZE)
generate_edge(line, 0)
@@table_init = true
end
# 重み係数の設定(全局面共通)
@eval_weight = Weight.new
@eval_weight.mobility_w = 67 # 着手可能手数
@eval_weight.liberty_w = -13 # 開放度
@eval_weight.stable_w = 101 # 安定石
@eval_weight.wing_w = -308 # ウイング
@eval_weight.xmove_w = -449 # X打ち
@eval_weight.cmove_w = -552 # C打ち
end
#
# 評価関数
#
def evaluate(board)
edge_stat = nil
corner_stat = nil
result = nil
# 辺の評価
edge_stat = @@edge_table[idx_top(board)]
edge_stat.add(@@edge_table[idx_bottom(board)])
edge_stat.add(@@edge_table[idx_right(board)])
edge_stat.add(@@edge_table[idx_left(board)])
# 隅の評価
corner_stat = eval_corner(board)
# 確定石に関して、隅の石を2回数えているので補正
edge_stat.get(Disc::BLACK).stable \
-= corner_stat.get(Disc::BLACK).corner
edge_stat.get(Disc::WHITE).stable \
-= corner_stat.get(Disc::WHITE).corner
# パラメータの線形結合
result = \
edge_stat.get(Disc::BLACK).stable * @eval_weight.stable_w \
- edge_stat.get(Disc::WHITE).stable * @eval_weight.stable_w \
+ edge_stat.get(Disc::BLACK).wing * @eval_weight.wing_w \
- edge_stat.get(Disc::WHITE).wing * @eval_weight.wing_w \
+ corner_stat.get(Disc::BLACK).xmove * @eval_weight.xmove_w \
- corner_stat.get(Disc::WHITE).xmove * @eval_weight.xmove_w \
+ edge_stat.get(Disc::BLACK).cmove * @eval_weight.cmove_w \
- edge_stat.get(Disc::WHITE).cmove * @eval_weight.cmove_w
# 開放度の評価
unless @eval_weight.liberty_w == 0
liberty = count_liberty(board)
result += liberty.get(Disc::BLACK) * @eval_weight.liberty_w
result -= liberty.get(Disc::WHITE) * @eval_weight.liberty_w
end
# 現在の手番の色についてのみ、着手可能手数を数える
result += \
board.get_current_color \
* board.get_movable_pos.size \
* @eval_weight.mobility_w
return board.get_current_color * result
end
#
# @@edge_tableの生成
# generate_edge(line, 0)
#
def generate_edge(edge, count)
if count == Board::BOARD_SIZE
# このパターンは完成したので、局面のカウント
stat = EdgeStat.new
stat.get(Disc::BLACK).set(eval_edge(edge, Disc::BLACK))
stat.get(Disc::WHITE).set(eval_edge(edge, Disc::WHITE))
@@edge_table[idx_line(edge)] = stat
return
end
# 再帰的にすべてのパターンを網羅
edge[count] = Disc::EMPTY
generate_edge(edge, count + 1)
edge[count] = Disc::BLACK
generate_edge(edge, count + 1)
edge[count] = Disc::WHITE
generate_edge(edge, count + 1)
return
end
def eval_edge(line, color)
edge_param = EdgeParam.new
# ウィングなどのカウント
if line[0] == Disc::EMPTY and line[7] == Disc::EMPTY
x = 2
while x <= 5
unless line[x] == color
break
end
x += 1
end
if x == 6 # 少なくてもブロックが出来ている
if line[1] == color and line[6] == Disc::EMPTY
edge_param.wing = 1
elsif line[1] == Disc::EMPTY and line[6] == color
edge_param.wing = 1
elsif line[1] == color and line[6] == color
edge_param.mountain = 1
end
else # それ以外の場合に、隅に隣接する位置に置いていたら
if line[1] == color
edge_param.cmove += 1
end
if line[6] == color
edge_param.cmove += 1
end
end
end
# 確定石のカウント
# 左から右に走査
for x in 0..7
unless line[x] == color
break
end
edge_param.stable += 1
end
if edge_param.stable < 8
# 右からの走査も必要
7.downto(0) do |x|
unless line[x] == color
break
end
edge_param.stable += 1
end
end
return edge_param
end
#
# 隅のパラメーターを調べる。この関数は各評価時に使う。
#
def eval_corner(board)
corner_stat = CornerStat.new
corner_stat.get(Disc::BLACK).corner = 0
corner_stat.get(Disc::BLACK).xmove = 0
corner_stat.get(Disc::WHITE).corner = 0
corner_stat.get(Disc::WHITE).xmove = 0
point = Point.new
# 左上
point.y = 1
point.x = 1
corner_stat.get(board.get_color(point)).corner += 1
if board.get_color(point) == Disc::EMPTY
point.y = 2
point.x = 2
corner_stat.get(board.get_color(point)).xmove += 1
end
# 左下
point.y = 8
point.x = 1
corner_stat.get(board.get_color(point)).corner += 1
if board.get_color(point) == Disc::EMPTY
point.y = 7
point.x = 2
corner_stat.get(board.get_color(point)).xmove += 1
end
# 右下
point.y = 8
point.x = 8
corner_stat.get(board.get_color(point)).corner += 1
if board.get_color(point) == Disc::EMPTY
point.y = 7
point.x = 7
corner_stat.get(board.get_color(point)).xmove += 1
end
# 右上
point.y = 1
point.x = 8
corner_stat.get(board.get_color(point)).corner += 1
if board.get_color(point) == Disc::EMPTY
point.y = 2
point.x = 7
corner_stat.get(board.get_color(point)).xmove += 1
end
return corner_stat
end
#
# 各辺の状態についてのインデックスの計算
#
# 上辺
def idx_top(board)
index = 0
m = 1
point = Point.new(1, 8)
Board::BOARD_SIZE.downto(1) do |i|
point.x = i
index += m * (board.get_color(point) + 1)
m *= 3
end
return index
end
# 下辺
def idx_bottom(board)
index = 0
m = 1
point = Point.new(8, 8)
Board::BOARD_SIZE.downto(1) do |i|
point.x = i
index += m * (board.get_color(point) + 1)
m *= 3
end
return index
end
# 右辺
def idx_right(board)
index = 0
m = 1
point = Point.new(8, 8)
Board::BOARD_SIZE.downto(1) do |i|
point.y = i
index += m * (board.get_color(point) + 1)
m *= 3
end
return index
end
# 左辺
def idx_left(board)
index = 0
m = 1
point = Point.new(8, 1)
Board::BOARD_SIZE.downto(1) do |i|
point.y = i
index += m * (board.get_color(point) + 1)
m *= 3
end
return index
end
#
# 開放度を取得
#
def count_liberty(board)
liberty = ColorStorage.new
liberty.set(Disc::BLACK, 0)
liberty.set(Disc::WHITE, 0)
liberty.set(Disc::EMPTY, 0)
point = Point.new
for y in 1..Board::BOARD_SIZE
point.y = y
for x in 1..Board::BOARD_SIZE
point.x = x
l = liberty.get(board.get_color(point))
l += board.get_liberty(point)
liberty.set(board.get_color(point), l)
end
end
return liberty
end
#
# 石のパターンのインデックスを返す
#
# [-1, -1, -1, -1, -1, -1, -1, -1] => 0
# [-1, -1, -1, -1, -1, -1, -1, 0] => 1
# [1, 1, 1, 1, 1, 1, 1, 1] => 6560
def idx_line(line)
return 3 * (3 * (3 * (3 * (3 * (3 * (3 * (line[0] + 1) + line[1] + 1) + line[2] + 1) + line[3] + 1) + line[4] + 1) + line[5] + 1) + line[6] + 1)+ line[7] + 1
end
end
2010.05.27 Thu [長年日記]
# [Ruby] 「リバーシのアルゴリズム」をRubyで その7
間がかなり開きましたが、前回の続き。第5章「定石」と第6章ゲームの実装。これでひとまず終了。
# -*- cording: sjis -*-
# coordinatestransformer.rb
require 'point'
class CoordinatesTransformer
def initialize(first)
@rotate = 0
@mirror = false
if first.equals(Point["d3"])
@rotate = 1
@mirror = true
elsif first.equals(Point["c4"])
@rotate = 2
elsif first.equals(Point["e6"])
@rotate = -1
@mirror = true
end
end
#
# 座標f5を開始点とする座標系に正規化する
#
def normalize(point)
new_point = rotate_point(point, @rotate)
new_point = mirror_point(new_point) if @mirror
return new_point
end
#
# f5を開始点とする座標を元の座標に戻す
#
def denormalize(point)
new_point = Point.new(point.y, point.x)
new_point = mirror_point(new_point) if @mirror
new_point = rotate_point(new_point, -@rotate)
return new_point
end
#
# 座標の回転
# 半時計回り90度 1
#
def rotate_point(old_point, rotate)
rotate %= 4
rotate += 4 if rotate < 0
new_point = Point.new
case rotate
when 1
new_point.y = Board::BOARD_SIZE - old_point.x + 1
new_point.x = old_point.y
when 2
new_point.y = Board::BOARD_SIZE - old_point.y + 1
new_point.x = Board::BOARD_SIZE - old_point.x + 1
when 3
new_point.y = old_point.x
new_point.x = Board::BOARD_SIZE - old_point.y + 1
else
new_point.y = old_point.y
new_point.x = old_point.x
end
return new_point
end
#
# 左右反転
#
def mirror_point(point)
new_point = Point.new
new_point.y = point.y
new_point.x = Board::BOARD_SIZE - point.x + 1
return new_point
end
end
reversi.book(定石データ)
f5f6e6f4e3d3f3c5c4e2 f5f6e6f4e3d6g4d3c3f2 f5d6c4g5c6d3e6d7f6c7 f5d6c5f4e3c6d3f6e6d7e7c7c4b4
# -*- coding: sjis -*-
# bookmanager.rb
require 'point'
require 'coordinatestransformer'
class BookManager
BOOK_FILE_NAME = "reversi.book"
class Node
attr_accessor :child, :sibling, :point
def initialize
@child = nil
@sibling = nil
@point = Point.new
end
end
attr_accessor :root
def initialize
@root = Node.new
@root.point = Point["f5"]
File.open(BOOK_FILE_NAME) {|f|
while line = f.gets
book = []
line.chomp!
0.step(line.size - 1, 2) do |i|
point = Point[line[i, 2]]
book << point
end
add(book)
end
}
end
#
# 定石手の検索
#
def find(board)
node = @root
history = board.get_history
if history.empty?
return board.get_movable_pos
end
first = history.first
transformer = CoordinatesTransformer.new(first)
# 座標を変換してf5から始まるようにする
normalized = []
for i in 0...history.size
point = history[i]
point = transformer.normalize(point)
normalized << point
end
# 現在までの棋譜リストと定石の対応を取る
for i in 1...normalized.size
node = node.child
point = normalized[i]
until node.nil?
break if node.point.equals(point)
node = node.sibling
end
if node.nil?
# 定石を外れている
return board.get_movable_pos
end
end
# 履歴と定石の終わりが一致していた場合
if node.child.nil?
return board.get_movable_pos
end
next_move = get_next_move(node)
# 座標を元の形に変換する
next_move = transformer.denormalize(next_move)
v = []
v << next_move
return v
end
private
#
# bookで指定された定石を定石木に追加する
# book[0] ノードはf5
#
def add(book)
node = @root
for i in 1...book.size
new_point = book[i]
if node.child.nil?
# 新しい定石手
node.child = Node.new
node = node.child
node.point.y = new_point.y
node.point.x = new_point.x
else
# 兄弟ノードの探索
node = node.child
while true
# すでにこの手はデータベース中にあり、その枝を見つけた
break if node.point.equals(new_point)
# 定石手の新しい枝
if node.sibling.nil?
node.sibling = Node.new
node = node.sibling
node.point.y = new_point.y
node.point.x = new_point.x
break
end
node = node.sibling
end
end
end
end
#
# 次の一手を決める。引数nodeは現在の手数の位置に対応するノード。
# 直前にboardに打たれた手
# node.childの世代が次の手を表すノード
#
def get_next_move(node)
candidates = []
target = node.child
until target.nil?
candidates << target.point
target = target.sibling
end
index = rand * candidates.size
point = candidates[index]
return Point.new(point.y, point.x)
end
end
if __FILE__ == $0
require 'board'
bm = BookManager.new
b = Board.new
b.move(Point["f5"])
b.move(Point["f6"])
b.move(Point["e6"])
b.move(Point["f4"])
b.move(Point["e3"])
puts bm.find(b).to_s
end
# -*- encoding: sjis -*-
# reversigame.rb
require "ai"
require "consoleboard"
class GameException < Exception; end
class UndoException < GameException; end
class ExitException < GameException; end
class GameOverException < GameException; end
class Player
def on_turn(board)
end
end
class HumanPlayer < Player
def on_turn(board)
if board.get_movable_pos.empty?
puts "あなたはパスです。"
board.pass
return
end
loop do
puts "手を\"f5\"のように入力、もしくは(U:取り消し/X:終了)を入力してください:"
input = gets
input.chomp!.downcase!
case input
when "u"
puts "取り消します。"
board.undo
board.undo
board.undo while board.get_movable_pos.empty?
return
when "x"
puts "終了します。"
exit
else
begin
point = Point[input]
rescue
puts "正しい形式で入力してください。"
next
end
end
unless board.move(point)
puts "そこには置けません。"
next
end
break
end
end
end
class AIPlayer < Player
def initialize
@ai = AlphaBetaAI.new
end
def on_turn(board)
puts "コンピュータ思考中..."
e = @ai.move(board)
printf("選択:%s 評価値:%5d\n", board.get_history.last.to_s, e )
end
end
board = ConsoleBoard.new
player = {}
player[1] = AIPlayer.new
player[-1] = AIPlayer.new
loop do
board.print_board
print "黒石#{board.count_disc(Disc::BLACK)} "
print "白石#{board.count_disc(Disc::WHITE)} "
puts "空マス#{board.count_disc(Disc::EMPTY)}"
msg = board.get_current_color == 1 ? "黒(O)番" : "白(X)番"
puts "手を入力してください #{board.get_turns + 1}手目#{msg}"
player[board.get_current_color].on_turn(board)
puts
if board.is_gameover
board.print_board
puts "ゲーム終了"
puts "黒石: #{board.count_disc(Disc::BLACK)}"
puts "白石: #{board.count_disc(Disc::WHITE)}"
break
end
end
実行画面はこんな感じ。
a b c d e f g h 1 _ _ _ _ _ _ _ _ 2 _ _ _ X _ _ _ _ 3 _ _ O O O X _ _ 4 _ _ O O O O _ _ 5 _ X X X O X _ _ 6 _ X X X O _ _ _ 7 _ _ _ O O O _ _ 8 _ _ _ _ O X _ _ 黒石13 白石10 空マス41 手を入力してください 20手目白(X)番 コンピュータ思考中... 候補01 d8,評価値 566 候補02 f2,評価値 644 候補03 f6,評価値 618 候補04 c2,評価値 343 候補05 b4,評価値 525 候補06 e2,評価値 540 候補07 g5,評価値 527 候補08 g3,評価値 514 候補09 c8,評価値 473 候補10 b3,評価値 157 候補11 g8,評価値 55 選択:f2 評価値: 644 a b c d e f g h 1 _ _ _ _ _ _ _ _ 2 _ _ _ X _ X _ _ 3 _ _ O O X X _ _ 4 _ _ O X O O _ _ 5 _ X X X O X _ _ 6 _ X X X O _ _ _ 7 _ _ _ O O O _ _ 8 _ _ _ _ O X _ _ 黒石11 白石13 空マス40 手を入力してください 21手目黒(O)番 コンピュータ思考中... 候補01 f6,評価値 1107 候補02 b4,評価値 1263 候補03 e2,評価値 1131 候補04 g6,評価値 1029 候補05 a5,評価値 1081 候補06 e1,評価値 1038 候補07 c7,評価値 1148 候補08 g5,評価値 1029 候補09 d1,評価値 1001 候補10 g4,評価値 1031 候補11 c1,評価値 908 候補12 a4,評価値 1001 候補13 g3,評価値 1066 候補14 f1,評価値 949 候補15 a6,評価値 752 候補16 g8,評価値 650 候補17 b7,評価値 686 候補18 g2,評価値 714 選択:b4 評価値: 1263
# [Ruby] 「リバーシのアルゴリズム」をRubyで その8
一通り実装し終わったので、本書の感想など。
- 思考プログラムの基本が紹介されている
- 本書は200ページの薄い本ではありますが、思考ゲームプログラミングの入門として必要不可欠な要素を広く薄く紹介しています。解説は詳しくはないし私にはわかりにくかったですが、言及はされているので自分で調べる際に助かりました。ただ、機械学習については紹介されていません。
- 誤植や実装漏れが比較的多い(特にJava)
- 出版元の工学社には本書のサポートページがありません。ですが、著者によるサポートページがあり、そこに正誤表や参考文献が載っています。
著者によるサポートページ: 「リバーシのアルゴリズム」補足
本書の表紙には「C++&Java対応」とありますが筆者の方はC++で作成しているらしく、Javaのコードの方に正誤表に載っていない誤植や実装漏れが目立ちます。私が持っているのは第1版第3刷ですが修正されていませんでした。
いろいろ文句を書きましたが入門書としては良い本であると思います。値段も安いですし。
本書の他に特に役立ったのが以下のページ
リバーシプログラムの作り方 サンプル
あと、Rubyで書くに当ってよく見たのは
- Rubyリファレンスマニュアル Ruby 1.9.1版(全般)
昔のは右上に検索窓があったような気がするけど、今は無くてちと使いにくい。 - プログラミング言語Ruby第2版言語編 (第13章 デバッガ)
- オブジェクト指向スクリプト言語Ruby (第6章3節 life.rb)
2010.05.28 Fri [長年日記]
# [本] ボナンザVS勝負脳―最強将棋ソフトは人間を超えるか 保木邦仁, 渡辺明
読了。
2007年3月21日、若きタイトルホルダーと最強コンピュータの歴史的一戦。多くの人の予想を裏切り、あと一歩というところまで攻め込んだ「ボナンザ」。どのようにしてこの最強ソフトは生まれたのか?
[ボナンザVS勝負脳: 書籍: 保木邦仁 渡辺明 | 角川書店・角川グループより引用]
探索アルゴリズムについては全幅探索ということなので「リバーシのアルゴリズム」でも解説されていたミニマックス法に枝刈りの手法(alpha-beta法など)を組み合わせたものであることは本書を読む前から予想できていたので、個人的には評価関数について知りたかったのだけれどあまり書かれていなくて残念でした。
予想以上に面白かったのが渡辺明竜王のパート。プロ棋士からみた従来のコンピュータ将棋とボナンザの差、ボナンザの癖、コンピュータ将棋の今後の予想など。あと、プロ棋士が対局前にどう準備し研究をつづけているのかなど。将棋読み物として面白かったです。
日本将棋連盟への挑戦状で少し前に話題になりましたが、プロ棋士とコンピュータ将棋の対局が、今秋再び行われるそうです。相手は清水市代女流二冠。対戦するコンピュータ将棋は、ご近所電気通信大学の方も開発に携わっているらしい合議アルゴリズム「文殊」に各種コンピュータ将棋の予定(単独のほうが強ければ、単独の可能性もあり)。今回もし勝てば、今後プロ四段からトッププロ、最終的には名人か竜王と対戦もあるかもしれないそうで、今から楽しみです。
2010.05.29 Sat [長年日記]
# [スポーツ] 世界卓球2010 女子準々決勝 日本 × 韓国
平野早矢香選手の第1試合は残念ながら見逃してしまいましたが、福原愛選手の第2試合途中以降見ました。
5時間にも及ぶ大接戦、大熱戦。韓国のキム・キョンア選手の強さというかしぶとさに苦しめ続けられました。平野早矢香選手、福原愛選手とももうほんの少しだったのに・・・。でもよくがんばった。
この試合で一番心震えたのはやはり、第3試合。石川佳純選手が2ゲーム落としてからの大逆転劇。よー頑張った。おっちゃん泣いたよ。
結局平野早矢香選手・福原愛選手・石川佳純選手とも1勝しての準決勝進出。めでたい。次は男子・女子とも中国戦。厳しいとは思いますが頑張ってください。
あとテレビ東京さん。お願いだから今日みたいに途中で放送終了はやめてね。
+ 右@細野晴飲み [はじめまして。こんばんは。 Y(寄り集まって)M(ミニ鍵盤いぢる)O(おぢさん達)の主としてベース担当の右@細野晴..]
+ 滝谷 [コメントありがとうございます。東風シダックス版は本当に素晴らしいですね。 独り遊び動画も拝見しました。偽YMOさん..]