LifeWrite

気が向いたら書きます

グラブルのリアルタイムtwitter救援取得アプリを作ってみた1

 

gbf-raiderが閉鎖したので作ってみました。

 

※面倒なので携帯で見るの考慮してません。

dockerとかreactの練習と、nodejsのsocket使ってます。

 

下記URL

gbf-relief

 

テストなので四大天司のみです。

過去の物は出てきません、あくまでリアルタイムで取得します。

最初は何もないですが、とりあえず待ってればいつか救援出てきます。

 

gyazo.com

 

 

今後の予定(gbf-raiderにあった機能)

・デスクトップ通知したい

・音声で伝えたい

・見たい救援IDの選択

・もうちょっとグラブル感出したい

 

※サーバーのスペックがごみなので、負荷かけると多分すぐ落ちます。落ちたら増強考えます。(むしろ面白そうなので常識的な範囲で負荷かけて下さい)

 

 

 

docker(docker-compose)で10分でnginx+mysql+php-fpmの環境を作る

docker-composeを使ったら非常に簡単にWEBの環境が作れました

ホスト環境

centos7

docker-compose、dcokerインストール

yum update
#dockerインストール
curl -sSL https://get.docker.com/ | sh

#docker-composeインストール
curl -L https://github.com/docker/compose/releases/download/1.11.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

ファイル構造(最終的にdata/php/publicをweb上に公開します)

# tree
.
├── containers
│   ├── mysql
│   │   └── Dockerfile
│   ├── nginx
│   │   ├── conf
│   │   │   └── default.conf
│   │   └── Dockerfile
│   └── php-fpm
│       ├── conf
│       │   └── php.ini
│       └── Dockerfile
│   
├── data
│   ├── mariadb
│   └── php
│        └── public
└── docker-compose.yml

各ファイル

docker-compose.yml

#docker-compose.yml
version: "2"

services:
  php:
    build: ./containers/php-fpm
    volumes:
      - "./data/php:/var/www/html"
    environment:
      DB_HOST: db
      DB_PORT: 5432
  nginx:
    build: ./containers/nginx
    ports:
      - 80:80
      - 443:443
    links:
      - php
  mysql:
    build: ./containers/mysql
    volumes:
      - "./data/mariadb:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_PASSWORD: ryouzi01
      MYSQL_USER: abot
      MYSQL_DB: abot
    expose:
     - "3306"

php Dockerfile

FROM php:fpm
WORKDIR /var/www/html
RUN apt-get update && docker-php-ext-install mbstring mysqli pdo_mysql
ADD conf/php.ini /usr/local/etc/php/conf.d

nginx Dockerfile

FROM nginx:1.9.12
ADD conf/default.conf /etc/nginx/conf.d

mysql Dockerfile

FROM mariadb:10.1.18

VOLUME /etc/localtime:/etc/localtime:ro

RUN sed -i -e "s/^#bind-addresss*=s*0.0.0.0/bind-address=0.0.0.0/" /etc/mysql/my.cnf

nginx.conf

server {
    listen       80 default;
    server_name  localhost;
    charset utf-8;
    root /var/www/html;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    #rewrite ^(.+)/$ $1;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        root /var/www/html/public;
        fastcgi_pass   php:9000;
        fastcgi_index  index.php;
        #fastcgi_split_path_info ^(.+\.php)(.+)$;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

php.conf

#以下php.iniで使うものを設定
date.timezone = Asia/Tokyo

上記のファイルが全部そろったらdockerコンテナ起動

#docker-compose.ymlがある階層で
docker-compose build
docker-compose up -d

docker-compose stop

以上、非常に簡単に環境が作れて、かつ再構築も簡単

グラブルのwiki救援を自動取得するアプリを作った(四大天司対応)

ーーーーーーーーーーーーーーーーーーーーーーーーーー

2017/06/05

機能はこっちに引き継ぎました。

abot-s.hatenablog.com

ーーーーーーーーーーーーーーーーーーーーーーーーーー

 

以下概要

 

f:id:abot_s:20160901100400p:plain

 

上記の様な簡単アプリで自動で裏でwikiの内容を取得してるだけ。

様はヤフーのtwitterリアルタイム検索のwiki版で、自動的にクリップボードに救援IDを設定する。

リアルタイム検索はtwitterAPI使ってると思うけどこっちは力技です。

WebBrowserコントロールを使っています。

 

仕組みは以下の通り

グラブルのマルチ(グランデ、よわバハ)の救援ページ

通常マルチバトル救援募集板 - グランブルーファンタジー(グラブル)攻略wiki

 

・ページの仕様

1.上記の内容は全部inputタグで囲まれている

2.救援のINPUTタグにはclass="pcmt"が設定されている(救援以外もあるが)

3.各救援は昇順

 

・アプリ仕様

1.HtmlElementCollectionにINPUTタグを検索して格納、

そのINPUTのclassタグにpcmtが設定されていればそれを救援と認識

それを行ごとに画面出力

2.最新の行から\s[0-9a-z]{8}で救援IDを取得してクリップボードに張り付ける。(簡単に貼り付けで救援に入れるように)

3.取得間隔は10秒

4.新しい救援取得時にWindowsのNotifyIconを使いタスクトレイに通知

 

・問題

1.サイトのURLが変更されたらいちいち更新が面倒

2.サイトの表示が変わったらおしまい

3.使う人の書き方が変わったらおしまい

 

・利点

これを使ってからグランデに入れなかったことありません(キリッ

 

・・・としょうもないものを作ってしまった。

iphoneでの自動取得のほうが結構需要ありそうだけど、都度のクロールのせいで通信食いそう。

直接アプリに救援ID張り付けようと思ったけど、さすがにアプリに介入するといろいろまずそうなのでここまで。ほしい人いるだろうか・・・。

 

・追記

dropboxに上げてみました。
取得間隔は10秒固定です。(wikiに負荷をかけてほしくないため)

グランデ、よわバハのページだけです。要望があれば作るかもしれませんが・・・

使用は自己責任でお願いします。いかなる不利益を被っても責任は負いかねます。

サイトの構造が変わったりしてidが取得できなくなった場合、連絡いただければ調べる

(かも)しれませんが期待しないでください。

Dropbox - グラブルwiki救援自動クリップボード貼り付けアプリ.zip

 

・追記2 09/14 12:02

1.正規表現でのID抽出部分を変更(全角でID書く人とか居たのと、先頭にID書いた場合に抽出できなかったので)

2.初回起動時、IDが見つからない場合は通知しないように修正

 

・追記3 09/21 18:57

特定環境において起動時にエラーが出ていたので、dllも付属するように変更。

どのようなDLLかは↓の方の記事を参照

mshtml.dllの配布に関するメモ - wave1008の日記

 

追記4 2017/03/18

なんかアクセスめっちゃ増えていて、また開発環境も手元に戻ってきたので改良してます。(自分も使いたいので)

多分四大天司の影響だと思いますが・・・。

ちなみに引退してましたがバハ引いたので復帰しました。( ・´ー・`)

こんな感じになると思います。

f:id:abot_s:20170318195841p:plain

 

・抽出ワードは「ウリエル」と入れた場合にサイトに下記の行があったとき「ウリエル」の行だけ認識するものです。(表示自体は全行出します)

ウリエル 416500C6

ラファエル e6c1c3d0

・保存は次回起動時に覚えておくため用です。radioボタンを変えてもすぐに反映されます。 

 

※通常マルチも要望がありましたがあまり更新されないので、twitterで検索したほうがいいと思います。

また、コメントにスマホのアプリとありましたがそれ用に作っていないので多分無理だと思います。最近twitterでどなたかが作っているアプリ見かけた気がするけど・・・。

 

追記5 2017/03/19 四大天司対応させました。

https://www.axfc.net/u/3788133

パスワードは wiki です。

ソース残ってなかったから新規に作り直したので、不具合出たらご連絡ください。

意見もお待ちしております。

グラブルのフレンドも募集中です ID:6372142 

 

..etc 

いまの10秒更新だとほぼ無理ですね・・・。

3秒更新だとID入力した後の表示人数1/30人で入ることが3連続で出来たので、落ち着いたら配布します。

6人HELLは上手くいってないです。

 

 

 

 

 

 

意外に簡単?swiftで動的にカメラ内の顔を検出して処理する(CIDetector)

最近流行りのVRもいいけど、現実が全部嫁になればいいのにと日々思っているこの頃

そんな幻想を抱きながら顔を置き換える処理を、Swiftで簡単に顔検出が出来るみたいなのでやってみました

class TestViewController: UIViewController,UIGestureRecognizerDelegate,AVCaptureVideoDataOutputSampleBufferDelegate  {
    let captureSession = AVCaptureSession()
    let videoDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    let audioDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio)
    //let fileOutput = AVCaptureMovieFileOutput()
    var videoOutput = AVCaptureVideoDataOutput()
    var hideView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //各デバイスの登録(audioは実際いらない)
        do {
            let videoInput = try AVCaptureDeviceInput(device: self.videoDevice) as AVCaptureDeviceInput
            self.captureSession.addInput(videoInput)
        } catch let error as NSError {
            print(error)
        }
        do {
            let audioInput = try AVCaptureDeviceInput(device: self.audioDevice) as AVCaptureInput
            self.captureSession.addInput(audioInput)
        } catch let error as NSError {
            print(error)
        }
        
        self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)]
        
        //フレーム毎に呼び出すデリゲート登録
        let queue:dispatch_queue_t = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
        self.videoOutput.setSampleBufferDelegate(self, queue: queue)
        self.videoOutput.alwaysDiscardsLateVideoFrames = true
        
        self.captureSession.addOutput(self.videoOutput)
        
        let videoLayer : AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession) 
        videoLayer.frame = self.view.bounds
        videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
        
        self.view.layer.addSublayer(videoLayer)
        
        //カメラ向き
        for connection in self.videoOutput.connections {
            if let conn = connection as? AVCaptureConnection {
                if conn.supportsVideoOrientation {
                    conn.videoOrientation = AVCaptureVideoOrientation.Portrait
                }
            }
        }
        hideView = UIView(frame: self.view.bounds)
        self.view.addSubview(hideView)
        self.captureSession.startRunning()    
    }
    func imageFromSampleBuffer(sampleBuffer: CMSampleBufferRef) -> UIImage {
   //バッファーをUIImageに変換
        let imageBuffer: CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
        CVPixelBufferLockBaseAddress(imageBuffer, 0)
        let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)
        let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer)
        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = (CGBitmapInfo.ByteOrder32Little.rawValue | CGImageAlphaInfo.PremultipliedFirst.rawValue)
        let context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, bitmapInfo)
        let imageRef = CGBitmapContextCreateImage(context)
        
        CVPixelBufferUnlockBaseAddress(imageBuffer, 0)
        let resultImage: UIImage = UIImage(CGImage: imageRef!)
        return resultImage
    }
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        //同期処理(非同期処理ではキューが溜まりすぎて画像がついていかない)
        dispatch_sync(dispatch_get_main_queue(), {
            
            //バッファーをUIImageに変換
            var image = self.imageFromSampleBuffer(sampleBuffer)
            let ciimage:CIImage! = CIImage(image: image)
            
            //CIDetectorAccuracyHighだと高精度(使った感じは遠距離による判定の精度)だが処理が遅くなる
            var detector : CIDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options:[CIDetectorAccuracy: CIDetectorAccuracyLow] )
            var faces : NSArray = detector.featuresInImage(ciimage)

            // 検出された顔データを処理
            for subview:UIView in self.hideView.subviews  {
                subview.removeFromSuperview()
            }
            
            var feature : CIFaceFeature = CIFaceFeature()
            for feature in faces {
                
                // 座標変換
                var faceRect : CGRect = feature.bounds
                var widthPer = (self.view.bounds.width/image.size.width)
                var heightPer = (self.view.bounds.height/image.size.height)
                
                // UIKitは左上に原点があるが、CoreImageは左下に原点があるので揃える
                faceRect.origin.y = image.size.height - faceRect.origin.y - faceRect.size.height
                
                //倍率変換
                faceRect.origin.x = faceRect.origin.x * widthPer
                faceRect.origin.y = faceRect.origin.y * heightPer
                faceRect.size.width = faceRect.size.width * widthPer
                faceRect.size.height = faceRect.size.height * heightPer
                
                // 顔を隠す画像を表示
                let hideImage = UIImageView(image:UIImage(named:"trump.jpg"))
                hideImage.frame = faceRect
                
                self.hideView.addSubview(hideImage)
            }
        })
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}


左が参考オバマ、右が処理後

感想

・今の状態だとiphone越しなのでVRみたいにステレオマッチングしたら面白そう、ゴーグルかけて隠す画像も3Dモデルにして、顔の向きに応じて3Dモデルも回転したら・・・

・実際上書きしたところを想像すると、もしストローとかポッキーを相手が咥えていたら刺さりそう

・正面画像の検出精度はLowでもなかなかだが、横顔はかなり厳しい印象

・細かい精度を求めないのであれば、かなり簡単なので結構応用出来そう

・CIDetectorではなくOpenCVだと顔以外も検出できるらしい


参考

swiftでAVCaptureVideoDataOutputを使ったカメラのテンプレート - Qiita

100均Webカメラ2台でステレオマッチングやってみた - 銀の弾丸

Swiftで笑顔認識をやってみた - Qiita

twilioを使って友達がいるアピールをしよう!

twilioについては↓

Twilio for KDDI Web Communications | クラウド電話API

 

これを使い自分に電話を掛け、あたかも友達から電話が掛ってきたように見せかける。

 

まず契約だが、twilioには1番号につき月108円(友達料)が必要だ。

缶ジュース1本で友達が出来るなら安いだろう。

取得した番号を携帯の連絡先に登録することも忘れるなよ!

 

番号を取得した後 AccountSid と AuthToken が貰えるので控えておこう。

 

今回はPHPを利用する。

 

1.自分のサーバー上に↓のライブラリを入れる。(サーバーない人はAWSやらHerokuやらgoogle cloud platformやらで作っておこう)

twilio/twilio-php · GitHub

 

2.ライブラリを呼び出し電話をかけるphpを書く

<?

require('/path/to/twilio-php/Services/Twilio.php');

$account_sid = '[取得したAccountSid]';

$auth_token = '[取得したAuthToken]';

$client = new Services_Twilio($account_sid, $auth_token);

$call = $client->account->calls->create(
  '[取得した電話番号]', ※日本だと+81変換必要
  '[自分の携帯の電話番号]'※日本だと+81変換必要
  '[xmlのurl(3で作る)]' ,
  array(
    'Method' => 'GET',
    'FallbackMethod' => 'GET',
    'StatusCallbackMethod' => 'GET',
    'Record' => 'false'
  ) 
);
echo $call->sid;

?>

 

3.xmlファイルを作りに電話でなにをするか書く。

<?xml version="1.0" encoding="UTF-8"?>

<Response>
<Say  language="ja-jp">はじめまして</Say>
</Response>

 

↑の例では機械音声を流しているだけだが、女の子が入っている音声ファイルを流せば彼女と会話しているようにも出来るぞ!

 

4.自分で2のphpにアクセスする、またはクーロンとかで自動で指定時間にphpを実行するようにする。

※自分でアクセスする場合はセキュリティに気をつけろ!不正利用されて高額請求が来ても知りません。

 

5.自分の携帯に電話がかかってくるので、友達(録音)と話すだけ!

 

これで友達がいるアピールがいつでもできる!やったね!

 

・・・。

 

カードヒーローという最高のゲーム

記事の目的

古いゲームですが面白いので、もっといろんな人にプレイしてほしい

 

※カードゲームのプレイ歴はポケモンカード遊戯王大貝獣物語 THE MIRACLE OF THE ZONE、hearthstoneぐらい。ギャザとかカルドセプトとはやったことないです。

 

カードヒーローとは?

『トレード&バトル カードヒーロー』は、任天堂が2000年2月21日に発売したゲームボーイ用のコンピュータゲームソフト。

トレーディングカードゲームを題材としたオリジナルゲームである。ゲームボーイカラー対応。

現在バーチャルコンソールで販売中。

2007年12月20日に続編が発売。(wiki参照)

 


バーチャルコンソール トレード&バトル カードヒーロー|ニンテンドー3DS|Nintendo

 

 何が面白いの?

カードゲームの内容だけでなく、コンピュータゲーム全体として良かった点をあげてみます。

 

1.オートセーブ機能

データが一つしかなく常にオートセーブされています。

対戦中に電源を切っても、切ったターンから始まります。

逆に言うと、どんなに負けそうでも対戦をなかったことにはできません。

64のシレン2のオートセーブと同じようなものです。(こっちはコントローラーパックという手もありますが・・・

しかも勝率が付きます。

 

それまでのコンピュータゲームでは、負けそうになってもセーブデータを読み込みなおせば対戦をやり直すことが出来ました。

しかし、オートセーブのおかげでより対戦に緊張感が生まれます。

人生みたいなものですね。何回もやり直しできる人生もつまらなそうです。

人によってこの機能は賛否両論だとは思いますが、個人的にはこの機能のおかげでかなり面白さが増していると思います。

 

2.ストーリー

当時においては、他のコンピュータカードゲームに比べるとカードゲームをして楽しむ、というだけでなく物語としても続きが気になり楽しめる内容になっていました。(ネタばれにつき省略)

現在においては、古いゲーム+オリジナルということで、登場人物やストーリー等何の情報も得ないままプレイすることができます。

最近の有名なゲームであれば事前情報がある程度出された状態でゲームすることが多く、ネット上には広告が出されまったく知らないままプレイすることが難しい時代になってきています。個人的にはすごく大事。

(シュタゲネタばれしたやつ一生許さんからな!!)

 

3.覚えやすさ(ネタばれ含む)

新しくカードゲームを覚えるというのはとてもコストがかかります。

カードの効果一つにしても遊戯王やギャザだと何百、何千とあります。

カードヒーローの場合110枚しかありません。

その分戦略の幅が少ないと言われるかもしれませんが、カードヒ―ローの最大の面白さは駆け引き(後述)にあると思ってます。

ルールにおいても、チュートリアルがとてもしっかりしていて、「このカードどう使うの?」といったことにはなりません。

 

 4.配置型カードゲーム(ネタばれ含む)

初期の手札は5枚

カードの種類は

  • マスターカード(プレーヤーの分身)
  • モンスターカード
  • マジックカード(手札から直接使うカード)

があります。

そして下記のような形のゲーム版で行われます

 

   

 相 

-------------

前 自 

後   

 

自/相は各プレーヤーの分身のマスターカードが置かれ、

前/後にモンスターカードを置いて戦います。

モンスターカードには攻撃できる距離が設定されており、

どこに配置するかを後々の行動も含め考えるて戦う必要があります。

 

マスターカードにはHPがあり(3~10)、先に0にした方が勝利です。

 

そして一番大事なストーンという概念がこのゲームがあります。

各ターンに3個ずつ配られ、そのストーンを使いつつ対戦します。

   ・消費

 モンスターを場に出すのにストーンを消費

 モンスターのレベルアップでストーンを消費

 マスターカードの特殊能力でストーンを消費

 マジックカード使用時にストーンを消費 

 

 ・増える

 各ターンに3個ずつ増える

 マスターカードダメージを受けるとストーンが増える

 自分のモンスターがやられるとストーンが増える

 マスターカードの特殊能力でストーンが増える

 

このように、ストーンの増えたり減ったりを計算して対戦していくのがとても楽しい。

よくあるのカードゲームにある、カードを出して強さを競うだけというのとは違いここに駆け引きが生まれます。

ここで相手を攻撃するとストーンが増えるので攻撃しないでおこうとか。

逆に、相手に攻撃をさせるよう仕向けたり等さまざまあります。

 

5.細かい要素(ネタばれ含む)

 ゲーム内で細かい要素が多々あります。

  • 現実時間で1日が経過するとお母さんから1日50円もらえる
  • カード同士を合成して別のカードを生み出せたり出来る
  • ゲーム内にカードショップがあり実際にパックを買ったり売ったりもできる
  • ゲーム内のプレイヤーとのランキング機能

などなど

 

いいことばかり言ってるけど悪い点も言えよ!

もちろん、今から始めるにあたり思うところはあるので上げていきます。

 

・やっぱりカードの種類が少ない

昔のゲームということもありしょうがないとも言えますが、遊戯王やギャザのように長く遊ぶという点においてはいささか物足りなさがあります。

 

・ユーザーとの対戦がまずできない

カードゲーム自体の面白さはここにあると思います。

ですが人対人というのがもうできません。

昔は続編でWIFIバトルなどができましたが今では出来ないです。(リアルのカードも昔はありました)

なので、デッキを作ったので誰かと勝負しようというわけにはいきません。

 

・グラフィックの問題

GBなので現在においてはグラフィック面はかなりさびしいです。

そこがいいという場合もありますが。

 

・CPUの考える時間

今の時代どんなことも早く早くという時代になってきました、

そういう人にとって当時のCPUの考える時間はとても長く感じるかもしれません。

 

まとめ

悪いことを後に書くと悪い方に目が行きがちですが、それを差し引いてもとても面白いです。

「思いで補正じゃない?」と言われればそれまでですが、すこしでも多くの人に楽しんでもらえれば嬉しいです

また、昔やったことある人も思い出してもらえたら嬉しいです。