【Ruby】将棋ウォーズの棋譜から自分の苦手な戦法を割り出す

みなさん、将棋ライフは楽しんでいるでしょうか。

現在自分はウォーズ2級で伸び悩んではやふた月。

漫然とさしててもしょうがないと思い、しっかりと自分の棋譜を分析することにした。

自分の戦法は嬉野流一本。効率よく勝率を上げるために相手の戦型ごとの勝率を出して、優先的に対策を立てるべき戦型を割り出すことに。

 

大まかに以下の流れ。

  1. ウォーズから棋譜リンク取得
  2. converterでkif形式に変換
  3. 相手の飛車の振り先を集計、分析

まずはウォーズから棋譜のリンクを取得する。 2019年8月までは棋譜取得ツールが使えたようだが、仕様変更で現在は利用不可だそう。 のでクローラーを自作。 注意点としてはウォーズにログイン済のユーザプロファイルが必要。 一度Chromeでウォーズにログインし、その際に利用したユーザプロファイルを指定する。

require 'selenium-webdriver'


class ShogiWarsScraper
    def initialize(username)
        @username = username
    end

    def geturls(url)
        urllist = []
        options = Selenium::WebDriver::Chrome::Options.new
        options.add_argument("--headless")
        options.add_argument("--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36")
        options.add_argument('--start-maximized')
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-setuid-sandbox")
        #ウォーズに認証済のブラウザのプロファイルが必要。
        options.add_argument("--user-data-dir=~/Library/Application Support/Google/Chrome/Profile 1")
        driver = Selenium::WebDriver.for :chrome, options: options

        driver.navigate.to(url)             
            wait = Selenium::WebDriver::Wait.new(:timeout => 3000) # seconds
            elements = wait.until {  driver.find_elements(:class, 'game_replay') }
                elements.each do |v|
                    urllist << v.find_element(:xpath, ".//a")[:href]
                end

        driver.quit
        urllist
    end

    def makewarslist(max_page) #warsより棋譜リンクを取得。max 8
        urllist = []
        for i in 1..max_page do
            url = "https://shogiwars.heroz.jp/games/history?&locale=ja&user_id=#{@username}&page=#{i}"
            #s = ShogiWarsScraper.new
            urllist << self.geturls(url)
            sleep(2)
        end

        urllist.flatten!.each do |v|
            file = File.open("warsurllist.txt", "a") do |f|
                f.puts(v)
            end
        end
    end 
    
end

ウォーズの棋譜については、csa形式ライクな棋譜がページに埋め込まれている。 良さげな変換機がぱっと見で見つからなかったので、一旦webツール(http://swks.sakura.ne.jp/wars/kifusave/)を利用させてもらうことにする。

require 'selenium-webdriver'


class ConverterScraper
    def doTranslate(url)
        begin
        transtool = "http://swks.sakura.ne.jp/wars/kifusave/"
        options = Selenium::WebDriver::Chrome::Options.new
        options.add_argument("--headless")
        options.add_argument("--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36")
        options.add_argument('--start-maximized')
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-setuid-sandbox")
        options.add_argument("--user-data-dir=/Users/K/Library/Application Support/Google/Chrome/Profile 1")
        driver = Selenium::WebDriver.for :chrome, options: options
        driver.navigate.to(transtool) # URLを開く

        wait = Selenium::WebDriver::Wait.new(:timeout => 3000) # seconds
        element = wait.until {  driver.find_element(:class, 'keyword2') } # XPathで指定 
        element.click
        element.send_keys(url)
        
        sendbutton = driver.find_element(:id, 'searchbtn1').click
        savebutton = driver.find_element(:class, 'btn1').click

        kif = driver.find_element(:xpath, '/html/body/pre').text
        driver.quit
        rescue
        end
        kif
    end
end

これでようやくkifファイルができた。

詳細な分析はやねうら王を使わせてもらうので、kifを分類するために相手の戦型判断は相手の手の開始10手以内の飛車の振り先でのみ判定。

class Kifbunseki
    def initialize(pn, file)
        setPlayername(pn)
        setfile(file)
    end
    
    def setPlayername(pn)
        @playername = pn
    end

    def setfile(file)
        @filename = file
        
    end

    def getPlayerTeban
        @file = File.open(@filename, "r")
        array = @file.to_a
        sente = array[4].to_s
        gote = array[5].to_s
        if sente.include?(@playername)
            @teban = 0
        elsif gote.include?(@playername)
            @teban = 1
        else
            raise "Invalid PlayerName Error"
        end
    end

    def extractSashite(sentegote) #0:先手 1:後手
        @file = File.open(@filename, "r")
        @sSashite =[]
        @gSashite =[]
        array = @file.to_a
        array -= array[0..6]
        a_teb =[]
        array.each do |v|
            teb = ""
            v = v.split("")
                v.each do |vv|
                teb += vv.match(/[^ -~。-゚]/).to_s.chomp
            end
            a_teb << teb
        end
        c = a_teb.count
        for num in 0..c do
            if num % 2 == 0
            @sSashite << a_teb[num]
            else
            @gSashite << a_teb[num]
            end
        end
        if sentegote == 0
            @sSashite
        else
            @gSashite
        end
    end


    def identifySenkei(teban, sashite) 
        analyze_t = sashite[0..10]
        senkei = ""
        t = analyze_t.map do |v|
            v if v.include?("")
        end
        if teban == 1 #sente
            # case Boolean
            if t.include?("7二飛") then
                senkei = "sode"
            elsif t.include?("3二飛") then
                senkei = "sanken"
            elsif t.include?("4二飛") then
                senkei = "shiken"
            elsif t.include?("5二飛") then
                senkei = "nakabisya"         
            elsif t.include?("2二飛") then
                senkei = "mukaibisya"                
            elsif t.include?("6二飛") then
                senkei = "migishiken"
            else
                senkei = "ibisya"
            end


        else #gote
            # case t
            if t.include?("3八飛") then
                senkei = "sode"
            elsif t.include?("7八飛") then
                senkei = "sanken"
            elsif t.include?("6八飛") then
                senkei = "shiken"
            elsif t.include?("5八飛") then
                senkei = "nakabisya"             
            elsif t.include?("8八飛") then
                senkei = "mukaibisya"
            elsif t.include?("4八飛") then
                senkei = "migishiken"
            else
                senkei = "ibisya"
            end
        end
        return senkei
    end


    def win?(sashite)
        if sashite.include?("投了")
            return 0
        else
            return 1
        end

    end
end

実行結果はこんな感じ。

summary==========================

分析対象:119局

相手戦型:ibisya 勝利数:28 敗北数:36 勝率:43.75%

相手戦型:sode 勝利数:1 敗北数:3 勝率:25.0%

相手戦型:sanken 勝利数:11 敗北数:9 勝率:55.0%

相手戦型:shiken 勝利数:12 敗北数:5 勝率:70.59%

相手戦型:mukaibisya 勝利数:2 敗北数:4 勝率:33.33%

相手戦型:migishiken 勝利数:0 敗北数:1 勝率:0.0%

相手戦型:nakabisya 勝利数:3 敗北数:4 勝率:42.86%

 

 

居飛車は得意だと思っていたが、んなことはなかったわ!

 

 

 

コマンド実行完了時にSlack通知を飛ばす

ディスクコピーやhomebrewのアップデートなど、時間が少しかかる処理が終わった際にSlackに通知を飛ばしたくなった。
macの通知センターに通知を飛ばすのだと意外と気づかなかったりする。

slacknoti.rb

require 'net/http'
require 'uri'
require 'json'

class SlackClient
	def postMessage(uri, text)
		uri = URI.parse("#{uri}")
		https = Net::HTTP.new(uri.host, uri.port)

		https.use_ssl = true
		req = Net::HTTP::Post.new(uri.request_uri)
		req["Content-Type"] = "application/json"
		payload = {
			"text" => "#{text}" 
		}.to_json
		req.body = payload
		res = https.request(req)
	end
end

#send slack notification
slack_url = "slack_api_url"
text = "command executed."
sc = SlackClient.new
sc.postMessage(slack_url, text)

bash_profile

function snoti(){
    ruby 'scrypt_path/slacknoti.rb'
}

終わったらsource ~/.bash_profileを忘れずに。
これでsnotiでslackに通知が飛ぶようになりました。

始動プログラム2019参加者に選出されたのでこれからの意気込みを残しておこう。

やったぜ!!

 

ということで、受付時間最後の一分でギリギリ提出が間に合った事業プランで始動プログラム2019に参加させていただくこととなった。

 

始動「Next Innovator」プログラムとは

「始動Next Innovator」は、「シリコンバレーと日本の架け橋プロジェクト」の一環として次世代のイノベーションの担い手を育成することを目的に2015年度に立ち上がりました。過去4年間で約500名の多種多様なイントレプレナー・アントレプレナーが活躍しています。

より速く、より遠く、より力強く、ダイナミックに世の中を変えるには、グローバルにマーケットを捉え共に進むパートナーを見つけることが必要です。 「始動Next Innovator」はシリコンバレーに代表されるスタートアップの方法論を体得し、選抜者には実際にシリコンバレーに赴いて本場のスタートアップエコシステムを体感することで、世界に通用するイノベーターを育成。 参加者は日本のイノベーションエコシステムの一員として、日本の成長戦略に寄与することが期待されます。

本プログラムは、国内プログラム(全11日間)及び選抜者を対象としたシリコンバレープログラム(全10日間)、Demo Day(2020年2月下旬予定)で構成されており、様々な講師ならびにメンターとともに実践的な活動を遂行することで、イノベーターとしてのマインドセットとスキルセットの体得を目指します。

 始動プログラムHPより(https://sido2019.com/#toppage)

要するに、経産省主催、WiL運営の起業家育成プログラム。

過去記事検索してヒットする過去参加者の方々が、錚々たる経歴をお持ちの方ばかりで始まる前から萎縮しそうだ。

 

去年度(2018年)の倍率は

全国から345名の応募があり、一次選抜を通過した起業家や大企業の新事業担当者など126名が6か月の国内プログラムに参加しました。

経産省HPより(https://www.meti.go.jp/press/2018/01/20190111001/20190111001.html)

2.73倍ほどだった模様。

 

はっきり言って資料は調査の甘さが目立つし、個人審査動画は結膜炎でとんでもない顔してるしでこれは落ちてしまったかな、、と8割方思っていたのでホッと一安心。

 

参加審査通過のために詰めるべきは

応募期日前日に知ってそれから編集を始めたため特に提出資料は客観的にみてひどい出来だったはずだが、ポイントを絞って伝えるようにしたのがよかったように思う。

 

事業案を伝えるのはもちろんだが、それ以上に応募フォーマットに記載されている審査基準に合致する内容に絞って資料作成を行った。

 

審査基準の中でも特に重視されていると思われるのは過去記事や自身の経験から

その事業を行うにあたって自分が"腹落ち"しているか?だろう。

 

どんな優れたプランであってもそれを推進しようとしている人が途中で折れてしまっては実現することなく過去の "頑張った思い出" になってしまう。

 

なので、事業プランが有望だと思われる(少なくとも自分自身は有望だと信じられる)ものであることは前提として、いかに自分が粘っこく、このプランに執着しており、実現することに本気であるか?を伝えられる内容になっているとよいのではないだろうか。

おそらく審査の合否を分けたのはそこの部分だと思う。

だって結局我々が思いつく事業案はすでに誰かが考えはしたが実現していない、もしくはなんらかの壁にぶち当たりスケールできずにいるかのどちらかであると考えるべきなのだから。

それをぶち抜き切ることができる人材ですよ!というアピールは必要不可欠だ。

 

このプログラムに自分が求めること

さて、このプログラムに参加することになり、自分が8ヶ月間で何を成し遂げたいかをまとめておこう。

  1. シリコンバレー派遣者に選ばれること
  2. 事業に邁進するために必要と思われるメンターとのネットワークを築くこと
  3. 同世代のライバルを見つけること

 

1. はこのプログラムに参加するなら言わずもがな。

ただ、過去のシリコンバレー派遣者の肩書きをみると、企業勤務者ないしはすでに法人として活動している起業家しか見当たらない。事業化にむけて具体的に体制を整えていることはおそらく必須に近い条件になると推測される。

 

2. 事業に邁進するために必要と思われるメンターとのネットワークを築くこと

このプログラムに参加する最大のメリットはここだろう。HPをみてもらえればわかるが、メンターの質と幅が圧倒的に高い。さすがは経産省主催である。無論事業に必要であれば個別に何が何でもコネクションを作ろうという努力は必要だが、これだけの人たちと接し、自分の事業プランをアピールする機会が用意されている状況というのはまず、ない。

つまり、チャンスである。

 

 3.  同世代のライバルを見つけること

自分は現在24歳だが、これくらいの年代、もっと言えばより歳が下のライバルがいると個人的に燃える。負けられないな!と思うDoerとしての強力なエンジンの一つになるだろう。

 

まとめ

派遣対象者は、Day11:12月21日(土)に選抜結果を発表します。

ふえぇ、、シリコンバレーいきたいよぅ、、

同窓生となる120数名のみなさま、メンターの皆様、運営の皆様、何卒よろしくお願いします。