うどん県出身グーナーの呟き

神奈川県在住のグーナーです。プログラムをやっているので勉強していることや作った便利ツールを公開したいと思います。

【初心者向け】Jupyter Notebookを使ってみたよ

Jupyter を使ってみたよ

今回はプログラミング初心者から愛用者まで使うべきツールの紹介をします。

Jupyter Notebookって何?

プログラムと聞くと、何行、何十行とコードを書いて、初めて動作確認をするイメージを持つ方もいるかと思います。

特に、CやC++など作ったプログラムを実行する前にコンパイルという作業が必要な言語に慣れ親しんでいる人ほど、そういうイメージを持っているのではないでしょうか (Jupyterを知る前のわたしも同じでした。)

だけど、ご存知の通り、一気に複数行に渡るコードを追加しても想定どおりに動いてくれないことが多々あります。

逆に一発で想定通りに動いてくれたときもとてつもない不安に襲われていると思います。

Jupyterはそういう手間を省いてくれるツールになります。

Jupyterはブラウザ上で様々な言語を対話形式で動かすことができる開発環境です。

本家Wikiの言葉を借りると

Project Jupyter is a set of open source software projects that form the building blocks for interactive and exploratory computing that is reproducible and multi-language. The main application offered by Jupyter is the Jupyter Notebook, a web-based interactive computing platform that allows users to author documents that combine live code, equations, narrative text, interactive dashboard and other rich media. ...

Jupyterは多数に渡る言語をブロックで組み合わせることで対話的にそして探索的に計算処理をすることができるOpen Sourceソフトウェアプロジェクトの集合体です。 Jupyterの主な機能はJupyter Notebookと呼ばれるブラウザ上で対話的に計算処理を行うことができる環境で、ユーザにその場で実行可能なコード、数式、処理の流れなどの情報を含んだドキュメントを作成することができます。

ここでいう対話的というのは、一行実行したら、すぐに結果が出力されることをいいます。

つまり、Jupyterを使うことで得られる大きなメリットは,

  • 位置行ごとに実行確認をすることができる

  • 自分が書いたコードの意図を数式や図を使って簡単にまとめることができる

ことにあります。

もちろんその他にもいろいろなOpen Sourceを使うことなどのメリットもあります。

と、言葉で説明しましたが、おそらくイメージできていない方が多いかと思いますので、実際に使っている様子を交えながら説明したいと思います。

何はともあれ、使ってみたよ

僕のjupyter notebook のレポジトリはこちらになります。

github.com

今回はここでの処理結果を一例として示しますが、特にクローンする必要はありません。

また、細かい操作やコマンド, インストール方法についてはこの章では省略して、どういうふうに使っていて便利なのかだけ紹介したいと思います。

まずJupyter Notebookを起動するとデフォルトに設定しているブラウザにこちらのようなページが表示されます。 f:id:ryo_udon:20200606154942p:plain

ここではひとまず、既存のNotebookがどういうふうに書かれているかだけ紹介します。

一番上のkalman_filterというディレクトリをクリックすると、下のように.ipynb形式のファイルが入っているのでこれをクリックします。

f:id:ryo_udon:20200606155406p:plain

中身はこのような形になっています。

f:id:ryo_udon:20200606155432p:plain

文法はPythonそのものです。 ただここで通常のPythonコードと違うのは、最初に上げたメリット通り、

  • 各ブロックごとに計算結果を確認できる
  • 各関数に数式などで中身の意図を簡単に説明ができる

ことです。

f:id:ryo_udon:20200606155450p:plain

こちらの画像のように、関数を見ただけでは実際にノートなどに書き起こさないと数式全体を把握することが難しいものも 数式を挿入できるため簡単に表現することができます。

(ちなみに数式やコメントはこのブログを書く際に使っているMarkDownという記法で書くことができます。)

その他にも、Pythonという言語が人気な理由の一つに簡単にグラフを書くことができるという点があると思います。

Jupyter Notebookではこのグラフについても、各ブロックごとに結果を確認し、しかもそれをNotebookに表示したまま保存することができます。

これはDBSCANと呼ばれるデータのクラスタリング手法(似たような点同士にグループ分けする手法)を試したNotebookの一例です。

f:id:ryo_udon:20200606154915p:plain

そのため、Jupyter Notebookを使えば簡単なデータ解析の解析結果レポートを作成することもできるわけです。

こういった理由からJupyterは今世界中の開発者がこぞって使っています。

f:id:ryo_udon:20200606155005p:plain

すでにプログラミングをしている人からこれから始める人まで、これまでよりも効率的にプログラムを構築できる環境、それがJupyterなわけです。

どうやって使うの?

JupyterはOpen Sourceなので何もライセンス等を購入する必要はありません。

ただ、 公式ページ に従って一つコマンドを打つだけでインストールすることができます。

ただ、それでもプログラムにあまり触れたことのない方にはハードルが高く感じられる方もいるかもしれませんので、順番に説明していきます。


注意

先に注意書きとして、Pythonがすでに手元のパソコンにインストールされているものとして説明していきます。 もしPythonをインストールされていないようでしたら、 こちらのサイトこちら を見ていただければ、マウスでクリックするだけでインストールできるので、簡単にPythonユーザになれると思います。

今、巷にはPython2とPython3が存在しています。この2つは同じPythonながら記法が微妙に違ったりしますが、

今からインストールするのであればPython3一択だと思います。

理由はPython2はサポートがもうすぐ終了すること、最新のライブラリは基本Python3を対象にしているからです。


インストール方法

ではここからはPythonの環境が出来上がったものとして説明していきます。

f:id:ryo_udon:20200606154919p:plain

ここではインストール方法として二種類説明されています。

  1. Conda(Anaconda)を使う場合
  2. pipを使う場合

もしPythonに初めて触るという方の場合は、2. pipを使う場合のインストール方法を個人的におすすめします。

Anacondaはnative環境に手を加えずに済み、パソコンの中の環境をクリーンに保つことができるので個人的におすすめですが、

そのあたりがよくわからない方にはただ、ハードルを上げてしまうだけなので、素直にpipを使えばいいと思います。

またJupyterについてはおそらく、pythonのバージョンが変わっても使い続けると思いますので、そういう意味でも何も気にせずpipでインストールしてください。

では、インストール方法を決めたら、Macならターミナル、Windowsならコマンドプロンプト(Mingwでも大丈夫です)を開きます。

pipコマンドはディレクトリはどこでも問題ないので、起動後、下のコマンドを今開いたターミナル、コマンドプロンプトにコピペします。

pip install notebook

もしインストール時に権限がありませんなどのエラーがでてインストールできなかった場合は以下の方法を試してください。

  • Windowsの方:

    左下のwindowsマークを右クリックします。するとメニューが出てくると思うので、その中からコマンドプロンプトを管理者権限で実行するという項目があるのでそれをクリックしてください。

    許可を求めるポップアップがでたら臆せずはいをおして、再度上のコマンドを打ち直してください。

  • Macの方:

    Macだとsudoコマンドが使えます。なので、頭にsudoを加えて打ち込んでみてください。

      sudo pip install notebook
    

    その後、パソコンにログインする際のパスワードを打ち込めばインストールが開始するはずです。

これでJupyterのインストールは完了です。

使い方

1. ディレクトリ(フォルダ)への移動

Jupyter Notebookを使うには、まずターミナルかコマンドプロンプトを起動します。(以降、面倒なのでターミナルと言います。)

次にターミナルでJupyter Notebookを開きたいディレクトリに移動します。

例えば以下のようなディレクトリ(フォルダ)にNotebookを開きたい、保存したいということだったら、


f:id:ryo_udon:20200606155718p:plain


Windowsであれば

cd デスクトップ
cd Jupyter_test

で,

Macであれば、

cd Desktop
cd Jupyter_test

で移動できます。

余談ですが、もうディレクトリができているので、2つのコマンドをまとめても大丈夫です。

cd Desktop/Jupyter_test

また、今回Desktop上にディレクトリを作ることを例として上げましたが、別に場所に指定はありません。

2. Jupyter Notebookの起動

ディレクトリへの移動ができたら、 公式ページ の言う通り、以下のコマンドを打ち込み(コピペし)ます

jupyter notebook

これでお使いのブラウザにJupyter Notebookが自動的に開かれるはずです。

3. 自分のJupyter Notebookを作る

f:id:ryo_udon:20200606155804p:plain

さて、ブラウザに移動すると、おそらく最初は何もnotebookを作っていないので、上の画像のように表示されているはずです。

ではここから、新しくnotebookを作っていきます。

f:id:ryo_udon:20200606154923p:plain

こちらの画像のように右上のNewのボタンをクリックすると新しく作成したいNotebookの種類を選ぶプルダウンが出てきます。

今回はPython3を使いたいので、一番下にあるPython3を選びます。

するとこのような画面に移行して、早速コードを書くことができるようになります。

これでnotebookの作成は完了です。 あとはFileから好きな名前で保存しておきましょう。

4. コードを試しに書いてみる

f:id:ryo_udon:20200606155829p:plain

試しにコードを書いてみましょう。 Jupyter Notebookを編集する際にはいくつか覚えることがあります。

  1. ブロックを編集する際にはクリックするか、カーソルをあわせてEnter(Return)
  2. ブロックを実行したいときは Shift + Enter(Return)
  3. Markdown(Code)にしたいときは、ブロックを選択し、メニューのCell -> Cell Type -> Markdownを選択 (ショートカットキーもあります)
  4. ブロックを下に追加したいときはブロックを選択してキーボードのBを入力する(Shiftはいりません。Bだけです) 逆に上に入れたいときはAを押します。

基本的にはこれさえ覚えていればあとは使っていくうちに覚えます。

また、ショートカットについてもメニューのHelpから見ることができますので、ぜひそちらも確認してみてください。

終わりに

今回はJupyterについて、本当の導入部分を説明しました。

正直、ここからどういうコードを書けばいいのか、わからないという人もいるかと思います。

実は、本当はもっと別のことを書きたかったのですが、そのためにはJupyterの説明をする必要があり、

慌てて作った次第です。

とはいえ、ここで放置するのもあれなので、そんな方向けの簡単なコードを随時追加していきたいと思います。

また、Pythonで書いたものをC,C++に移植して公開もしていきたいと思っているので、またそちらも投稿していきたいと思います。

ではでは。

ポータブルSSDの自作を試しにやってみたよ

ポータブルSSDの自作を試しにやってみたよ

概要

来週には消費税の増税がなされるということで自分も駆け込み購入を色々としておりました。

その中で以前からやりたいと思っていたのが、パソコンとps4の外付け記憶媒体をHDD -> SSDにすること。

しかしSSDは高い。特に1TBでさらに通信速度も、となると2万円台コース。 (もちろん、通信速度を気にしない人なら1万2~3000円で購入できます)

なので、ここは以前から気になっていたポータブルSSDを自分で組んでみようと思った次第です。

参考

以下のサイトを参考にさせていただきました。

外付け(として使う)SSDって今何買えばいいのか調べたメモ

何を購入したのか

今回はあまり奇をてらわず、以下の2つを購入しました。

CT1000P1SSD8JP Crucial Crucial M.2 2280 NVMe PCIe Gen3x4 SSD P1シリーズ 1.0TB

価格:12,980円
(2020/8/1 21:49時点)
感想(6件)

私が購入したときは、SSD:10,887円, ケース:4,500円でした。 ケースについてはもっと安価なものもありますし、予算がという方はここを削るのもありかと思います。 (ただ、安かろう悪かろうの可能性もありますので、ご注意を)

組み立て方

ケース開封

まずケースの方を開封すると以下のようになっています。 f:id:ryo_udon:20190924115124j:plain さらに中を覗いてみるとType-C <-> USB3.0, Type-C <-> Type-C ケーブルと組み立ての際に使用するネジとドライバー、 説明書が入っています。 f:id:ryo_udon:20190924115137j:plain

基本的には説明書に沿えば、簡単に組み上がるようになっていますが、ここはちょっと丁寧に画像をつかって組み立て方を説明します。 まずは、ケースの蓋をType-C端子側にずらして外します。 するとこのようにボードが顔を出します。 f:id:ryo_udon:20190924115259j:plain このボードも少し爪をかけて軽く上に持ち上げて上げると簡単に外れます。 f:id:ryo_udon:20190924113730j:plain 次に、このボードにSSDを取り付けていきます。

SSD取り付け

次に、SSDを取り付けていきます。 f:id:ryo_udon:20190924115411j:plain SSDもまず箱から取り出すとこのようなプラスチックケースの中に入っています。 f:id:ryo_udon:20190924115423j:plain これを取り出してボードに取り付けていきます。 ここでコツとしては、真横から指すのではなく、少し斜めにして少し前後に軽く動かしながら指して上げると簡単に刺さりやすいようです。 ボードに差すとこうなります。 f:id:ryo_udon:20190924113819j:plainf:id:ryo_udon:20190924113807j:plain ただ、このままではケースの中に入らない + 強度的に不安ですので、 付属のネジとスペーサで固定していきます。 画像の金色のパーツがスペーサです。 f:id:ryo_udon:20190924113830j:plain このスペーサの中にはネジが切られているのでボードの裏側からネジで固定することができます。 f:id:ryo_udon:20190924113845j:plain これでSSDの取り付けは完了です。

ケースを閉じる

最後にケースにこれをしまっていきます。 取り外した時と同じく簡単に取り付けられるのですが、Type-C端子がしっかりケースの穴にハマっているか確認してください。 それが確認できると、最後、ケースの蓋を締めて、端子と反対側にあるネジ穴にネジを閉めれば完成です。 f:id:ryo_udon:20190924113858j:plain f:id:ryo_udon:20190924113912j:plain

所感

一応、速度を試しに計測してみましたが、パソコン内部に搭載されているSSDとほぼ同等の書き込み、読み込み速度が得られました。 本体搭載のSSD f:id:ryo_udon:20190924115711p:plain 今回用意したポータブルSSD f:id:ryo_udon:20190924115718p:plain

本体の4倍の容量をこの価格で手に入れられたと考えればかなりお得感があります。

またPS4でも試してみましたが、体感でも分かるぐらい圧倒的にロードが速くなっていました。 試したのはMonster Hunter Worldですがクエスト受注とステージへの移動にかなり時間がかかっていましたが、それがほぼ一瞬でロードできました。 (例として、クエスト受注は猫メシを食べてる間に、クエストステージへの移動は10秒ほど)

唯一懸念点があるのが、発熱の問題。 もともと、このタイプのSSDは発熱がすごいことで有名なので、多少危惧していましたが、案の定でした。 ちょっと触るのも躊躇う温度なので、注意して使っていきたいと思います。 (私は基本的にバックアップはHDDで取っているので、最悪どうにかなるとおもっています。)

ただ、こんなに簡単に作れるので、今後、SSDやHDDは中身とケースを別々に購入していこうかなと思っています。

以上、ポータブルSSDの自作をやってみたよ、でした。

【Python】Discord用トーナメント表自動作成BotをPythonで作ったよ。

TeamMakerボット作成日記 第二弾

概要

第一回はこちら

ryo-udon.hatenadiary.jp

2019/12/05

ご指摘がありましたので追記します。

現在、Discord.pyはver 1.0 ~ になっていますが、現状このTeamMakerボットはver 0.7より以前のもののみ対応しています。

理由は下のリンクにありますが、変数名やクラス構成が大きく変わったことが原因となります。

現在、Discord.pyのversion確認をしつつ、1.0以降も対応できるように変更中です。

変更が完了次第、こちらの記事も追記したいと思います。

discordpy.readthedocs.io

2019/12/27 追記 python 3.8.0, discord.py version 1.2.5に対応したことを確認しました。 ご指摘いただきありがとうございました。

最近は便利になりました。

私の子供の頃では、チーム分けをするにもじゃんけんで延々とアイコを繰り返したり、時間をかけてくじを作ったりととりあえず時間がかかりました。

それこそ人数が10人を超えてくるとそれはもう大変です。

じゃんけんなんか早々決まるわけがないので(私はグッパージャス派でした)、結局チームわけで時間がかかって肝心の遊びの時間がなくなってしまう。そんな悲しい経験を何度もしました。

それに比べて今はどうでしょう?

1人一台スマホやパソコンを持つ時代になり、スマホやパソコンにはチーム分けのアプリがあり、劇的にその時間は短縮されました。

だけど、ちょっとまってください。

皆さんは本当にそれで満足していますか?

チームを作るたびにアプリにプレイヤーの名前を打ち込んで、あるいは参加する人の名前を膨大なリストから選んで。

(リストの人数が少ないって? それならそのままでいいと思いますよ)

意外と時間かかっていないですか?

特に大人数で協力プレイをするゲームをやる方ならなおさらこれを感じているのではないでしょうか?

かくいう私もその1人です。 そんな私が作ったのがこちら。

自動チーム分けBot その名もTeamMaker!!

今回はそんなTeamMakerに新機能を追加したので第二弾としてこのサイトでも公開したいと思います。

github.com

追加機能の概要

今回追加した機能は、トーナメント表の自動作成機能になります。

メンバーを登録すると、その人数に合わせいい感じにシードなども作ってくれるトーナメント表作成機能です。

今回、この機能を実行するに当たり、新たなライブラリが必要となりますので、そのインストールを行います。

sudo pip install pillow

これは作成したトーナメント表を画像として作成するために使用しています。

今回追加した機能の使い方自体は簡単。以下の2つのコマンドを打つだけ。

  • !cup
  • !cupw

それぞれのコマンドが何をするのかを説明します。

!cup

友人たちと熱いトーナメント戦をしたい!! だけどいちいちトーナメント表を作るのは面倒くさい! そんなあなたに魔法のコマンド!

以下の画像のように !cupと打ってみてください。 [f:id:ryo_udon:20190803000649p:plain] するとGeneralに参加したメンバを参加者としたトーナメント表を自動で作成して貼り付けてくれます。

!cupw

ちなみにこのトーナメント表、勝ち負けの判定もできます。

コマンドで !cupw [トーナメント番号] [ラウンド番号] [買った人のID] [4-2などのスコア]を打ってもらうと、 下のようになります。 [f:id:ryo_udon:20190803000654p:plain] これで面倒な記録も必要なし! いつでもどこでも手軽にトーナメント戦ができるようになります!

今回追加したものの構成について

今回は以前作成したTeamMakerに付け加える形で作成しましたが、TeamMaker bot専用の機能にする気はなく、 他のものにも使い回せるようにというのを意識して作成しました。

そこで作成したのが以下になります。


ここにはいずれクラス図を勉強して追加します。 (正しい表記の仕方がわからない)


主に追加したものは * トーナメント表を作る * トーナメント表を描く * 何個のトーナメント表を作るかを判断する * トーナメントを管理する の4つです。 これらの機能を合計で4つのクラスで表現しています(つもりです) また、これに合わせてTeamMakerクラスのスリム化を行いました。

構成としてはBotの根幹をなすBOT_BASEクラスの中にチーム分けをするTeamMakerクラスとトーナメント表の作成などを行う今回新たに作成したCUP_MAKERクラスが内包されているという構成になっています。

BOT_BASEクラスはただそれぞれのクラスを管理しているだけですので、このページではCUP_MAKERのみ説明していきます。

CUP_MAKER

CUP_MAKERクラスを構成する上で重要なクラスが2つあります。

ROUND():

このクラスがトーナメント作成での参加者及び、参加者が誰と対戦するのかを管理するこのCUP_MAKERに格納される最小のクラスになります。 トーナメント表での一番左端の参加者名しか書かれていないところも参加者が1人しかいないラウンドとして管理していきます。 このクラスはGraph構造をイメージしながら、色々と手抜きをしたものになります。

# @brief トーナメント内の各ラウンドの構造体。全てプレイヤーはラウンドの中に格納されて管理される
class ROUND():
    def __init__(self,stageNum, id, members):
        self.roundID = id
        self.stageNum = stageNum
        self.parentID = 0
        self.childID = []

        # それぞれのラウンドの座標を保存
        # ここではしたの*の座標を格納
        #
        # ---|
        #    *---|
        # ---|   |
        #
        self.x = 0
        self.y = 0

        self.hasChildFlag = 0
        self.PositionSetFlag = 0
        self.SetMember(members)
        self.winner = -1
        self.player_id = -1
        self.score = ""


    def SetMember(self, members):
        self.memberList = members
        if len(self.memberList) == 1:
            self.hasChildFlag = 1

    def CreateChildRound(self,roundID):
        if self.hasChildFlag == 0:
            self.hasChildFlag = 1
            halfLine = int(len(self.memberList) / 2)
            child1List = self.memberList[:halfLine]
            child2List = self.memberList[halfLine:]
            newStage = self.stageNum + 1
            child1Round = ROUND(newStage, roundID+1,child1List)
            child2Round = ROUND(newStage, roundID+2,child2List)
            self.childID = [roundID+1, roundID+2]
            child1Round.SetParentID(self.parentID)
            child2Round.SetParentID(self.parentID)
            return [child1Round, child2Round], roundID+2
        return [0], roundID

    def SetParentID(self, parentID):
        self.parentID = parentID

    def SetPosition(self, x, y):
        self.x = x
        self.y = y
        self.PositionSetFlag = 1

    def SetWinnerID(self, id, score):
        self.winner = id
        self.score = score

    def SetPlayerID(self, id):
        self.player_id = id

    def GetWinnerID(self):
        return(self.winner)

各ラウンドには区別するためのIDが割り振られており、ラウンド同士の繋がりを保持するために、親と子のIDをもたせています。

aaaa ---┐ ①
        ├---┐
bbbb ---┘   |②
            ├---
            |

この図を例に説明すると、aaaa, bbbb, , はどれも同じROUNDクラスで表現されています。

①の親は②であり、子はaaaa,bbbbの2つになります。

なので、各ラウンドのメンバの数を確認することでそれが参加者を示すラウンドかどうかを確認することができます。

基本的に勝敗、スコア、参加者名などの情報は全てこのクラスの中に格納されます。

本当はprivate変数などにして外部から直接編集できないように管理すべきところではありますが、ここではかなり手抜きをしています。

TOURNAMENT_MAKER()

次にこのクラスで先程のROUNDを取りまとめてトーナメントの形に整えて上げるクラスになります。

# @brief トーナメント作成用の構造体.
# ここではあくまで登録されたメンバー全員を入れたトーナメント表を作成する機能と勝ち負けの登録のみにしている。
# なので、人数が多い場合や、トーナメントを分けてあげたい場合は複数個のクラスを呼んで上げる必要がある
class TOURNAMENT_MAKER():
    def __init__(self, members):
        self.members = members
        self.tournament = []
        self.start_pos = [ 120, 30]
        # 線を描く際のオフセット値
        self.y_space = 40
        self.x_space = 50

    def CreateNormalTournament(self):
        roundID = 0
        FinalRound = ROUND(0,roundID,self.members)

        self.tournament.append(FinalRound)
        self.largestStageNum = 0

        num = 0
        while len(self.tournament) > num :
            newRounds = [0]
            newRounds, roundID = self.tournament[num].CreateChildRound(roundID)
            if len(newRounds) == 2:
                self.tournament.extend(newRounds)
            if  self.largestStageNum <  self.tournament[num].roundID:
                self.largestStageNum = self.tournament[num].roundID
            num += 1

        self.SetPosition()

    def GetLargestStageNum(self):
        return(self.largestStageNum)

    def SetFirstRoundPosition(self):
        dst_firstRound = []
        for round in self.tournament:
            if len(round.memberList) == 1:
                dst_firstRound.append(round.roundID)
        self.firstRound = []

        for name in self.members:
            for round_id in dst_firstRound:
                if name == self.tournament[round_id].memberList[0]:
                    self.firstRound.append(round_id)
                    break

        for i in range(len(self.firstRound)):
            self.tournament[self.firstRound[i]].SetPosition(self.start_pos[0], self.start_pos[1] + i * self.y_space)
            self.tournament[self.firstRound[i]].SetPlayerID(i)

    def CalcNewCenterPosition(self, roundID):
        childID = self.tournament[roundID].childID
        x = max([self.tournament[childID[0]].x, self.tournament[childID[1]].x]) + self.x_space
        y = 0
        for id in childID:
            y += self.tournament[id].y
        self.tournament[roundID].SetPosition(x, int(y / 2))

    def SetPosition(self):
        self.SetFirstRoundPosition()
        stageNum = self.largestStageNum
        roundList = range(len(self.tournament))
        while stageNum >= 0:
            for round in roundList:
                if self.tournament[round].stageNum == stageNum:
                    if self.tournament[round].PositionSetFlag == 0:
                        self.CalcNewCenterPosition(round)
            stageNum -= 1

    def SetMatchScore(self, roundID, winnerID, score):
        self.tournament[roundID].SetWinnerID(self.firstRound[winnerID], score)

このクラスがやっていることは、

  1. 参加者のリストから必要な数のROUNDクラスを呼び出して、トーナメントの形になるように親、子の関係を作ってあげること

  2. 全てのラウンドの設置位置を計算してあげること

  3. ROUNDクラスに勝ち負けをつける

の3つになります。

そのため、関数は複数個ありますが、実質外部から呼び出して上げる必要があるのは、

* CreateNormalTournament()
* SetMatchScore()

の2つ。

CreateNormalTournament()が1,2番の役割を、SetMatchScore()が3番の役割を果たしています。

  1. については、トーナメントに参加するメンバをどんどん2で割ってゆき、割れなくなったら、トーナメントの追加を終えるという方法にしています。

  2. については、参加者それぞれを格納するラウンドに最初に座標を割り当ててあげ、各ラウンドの中心をそのラウンドの子2つのちょうど真ん中になるようにy座標だけ計算してあげています。(CalcNewCenterPosition())

DRAW_SHAPE()

このクラスでは今回はpillowを使って絵を描きましたが、今後、他のライブラリを使いたいと思うことがあるかと思い、CUP_MAKER()の中でpillowの描画する関数を直接呼び出すのではなく、DRAW_SHAPE()を間に挟んであげています。

これによっていざ変えるときは、DRAW_SHAPE()の中のみを変更すればCUP_MAKER()やこのクラスを使っている他のクラスを全く変更する必要がありません。

import os, sys
from PIL import Image, ImageDraw, ImageFont

class DRAW_SHAPE():
    def __init__(self, image_name):
        self.size_x = 512
        self.size_y = 512
        self.image_name = image_name
        self.SetConfigure()

    def SetConfigure(self):
        self.SetRectangleSize()
        self.SetRectangleFillColor()
        self.SetEllipseFillColor()
        self.SetLineFillColor()
        self.SetEdgeColor()
        self.SetTextSize()
        #self.GetNewImage()

    def GetCenterPoint(self):
        return([self.size_x/2, self.size_y/2])

    def SetCenterPoint(self, x, y):
        self.size_x = x * 2
        self.size_y = y * 2


    def GetNewImage(self):
        self.im = Image.new("RGB",(int(self.size_x), int(self.size_y )),(255,255,255))
        self.draw = ImageDraw.Draw(self.im)


    def SetRectangleSize(self, dx=100, dy=20):
        self.rectangleXSize = dx
        self.rectangleYSize = dy

    def SetRectangleFillColor(self, color="white"):
        self.recColor = color

    def SetLineFillColor(self, color="black"):
        self.lineColor = color

    def SetEllipseFillColor(self, color="white"):
        self.ellipseColor = color

    def SetEdgeColor(self, color="Black"):
        self.edgeColor = color

    def DrawRectangle(self, x, y):
        self.draw.rectangle([(x,y),( x+self.rectangleXSize, y+self.rectangleYSize)], fill=self.recColor, outline=self.edgeColor, width=2)

    def DrawLine(self, x1,y1, x2, y2, width):
        self.draw.line((x1,y1,x2,y2), fill=self.lineColor, width=width)

    def DrawZigZagXtoY(self, x1, y1, x2, y2, width=10):
        self.DrawLine(x1,y1,x2,y1, width)
        self.DrawLine(x2,y1,x2,y2, width)

    def DrawZigZagYtoX(self, x1, y1, x2, y2, width=10):
        self.DrawLine(x1,y1,x1,y2, width)
        self.DrawLine(x1,y2,x2,y2, width)


    def DrawEllipse(self, x1,y1, x2,y2):
        self.draw.ellipse(( x1, y1, x2, y2), fill=self.ellipseColor, outline=self.edgeColor)

    def SetTextSize(self, textSize=10):
        self.textSize = textSize

    def SetModestTextSize(self, char, targetWidth):
        width = self.textSize * len(char)
        if width < targetWidth:
            self.textSize = targetWidth / len(char)

    def SetFontSize(self):
        self.font = ImageFont.truetype(size=self.textSize)

    def SetImageSize(self, x, y):
        self.size_x = x
        self.size_y = y

    def DrawText(self, x1, y1, text):
        self.draw.multiline_text((x1,y1), text, font=self.font, fill=(0,0,0,255))

    def SetFontFile(self, fontFileName, size=10):
        fileDir = os.path.dirname(os.path.abspath(__file__)) + "/../font/" + fontFileName
        self.font = ImageFont.truetype(fileDir, size)


    def ShawImage(self):
        self.im.show()

    def SaveImage(self):
        img_dir = os.path.dirname(os.path.abspath(__file__)) + "/../../image/"
        if os.path.exists(img_dir):
            ret = 1
        img = img_dir + self.image_name + ".jpg"
        self.im.save(img)
        return img

CUP_MAKER()

ここではこれまで説明したクラスを統括し、何個のトーナメントを作ればいいかを判断するクラスであるCUP_MAKER()を説明します。

# @brief TOURNAMENT_MAKER()にメンバーを登録する前の下処理と画像作成をしてあげるクラス
# TOURNAMENT_MAKERを内包しているので、ラッパーとして勝ち負けを登録する関数などを加えている
class CUP_MAKER():
    def __init__(self, member, tournament_name):
        self.tournament = []
        self.start_pos = [-100,0]
        self.line_width = 3
        self.tournament_name = tournament_name
        self.member = member
        self.member_size_array = self.CalcTournamentNum(member)


    def CreateCupTournament(self):
        for member_size in self.member_size_array:
            self.tournament.append(TOURNAMENT_MAKER(self.member[:member_size]))
            if len(self.member) > member_size:
                self.member = self.member[member_size:]
        for i in range(len(self.tournament)):
            self.tournament[i].CreateNormalTournament()

    def CalcTournamentNum(self, member):
        list_size_array = []
        divide_num = len(member)/12
        min = 100
        divide_size = len(member)
        if divide_num >= 1 and len(member)%12 != 0:
            for i in range(6, 13):
                remain = len(member) % i
                if min > remain:
                    min = remain
                    divide_size = i
            divide_num=len(member)//divide_size
            list_size_array = [divide_size for i in range(divide_num)]
            for i in range(min):
                list_size_array[i]+=1
        else:
            list_size_array.append(len(member))
        return(list_size_array)

    def SetRoundWinner(self, tournament_num, round_id, winner_id, score):
        self.tournament[tournament_num].SetMatchScore(int(round_id), int(winner_id), score)

    def DrawTournamentImage(self):
        tournament_num = 0
        image_list = []
        for tournament in self.tournament:
            image_name = self.tournament_name + str(tournament_num)
            ds = DRAW_SHAPE(image_name)
            ds.SetFontFile("font_1_honokamarugo_1.1.ttf", size=12)
            ds.GetNewImage()
            ds.DrawText( 0, 0,self.tournament_name+str(tournament_num))
            for t in tournament.tournament:
                width = self.line_width
                if t.stageNum == 0:
                    # 決勝戦は先にラウンドがないけど横線を引きたい
                    if t.winner != -1:
                        # 勝者のいる場合は太線で描画
                        width *= 2
                    ds.DrawLine(t.x, t.y, 50 + t.x, t.y, width=width)

                if len(t.childID) > 1:
                    for child in t.childID:
                        width = self.line_width
                        if t.winner != -1:
                            # もしこのラウンドの勝者がいる場合,太さを二倍とスコアを表記
                            for member in tournament.tournament[child].memberList:
                                if member == tournament.tournament[t.winner].memberList[0]:
                                    width *= 2
                                    ds.DrawText(t.x + 20, t.y - 20, t.score)
                        # ジグザグの線を描画
                        ds.DrawZigZagYtoX(t.x, t.y, 5+tournament.tournament[child].x, tournament.tournament[child].y, width=width)
                        ds.DrawText(t.x + 5, t.y - 20, "[" + str(t.roundID) + "]")
                if len(t.memberList)==1:
                    ds.DrawRectangle(self.start_pos[0]+t.x,self.start_pos[1]+t.y - 10)
                    member_name_text = " " + str(t.player_id) + "." + t.memberList[0]
                    ds.DrawText(self.start_pos[0]+t.x,self.start_pos[1]+t.y-5,member_name_text)
            image_dir = ds.SaveImage()
            image_list.append(image_dir)
            tournament_num += 1
        return(image_list)

このクラスがやっているのは、TOURNAMENT_MAKER()を何個呼び出せばいいのかという判断と、それぞれのトーナメント表をDRAW_SHAPE()クラスを呼び出して上げて画像に変換してあげるところです。

そのため実はこのクラスではあまり説明することがありません。

もしトーナメント表のデザインなどが好みでないなどがあれば変更するのがこのクラスになると思います。

画像の送信

今回、トーナメント表をjpgファイルで作成し、作成した画像のパスを渡してDiscordの各サーバーに送信しています。 その部分について簡単に説明します。

if image_list != "null":
    for img in image_list:
            await client.send_file(message.channel, img)

私が作成したBOT_BASE()クラスは送信するメッセージ(String型)のみか、 メッセージと画像のパスを格納したリスト型のimage_listを返してきます。

そこで、そのリストに格納されたパスの先にある画像を送ってくれるのが、 send_file()関数です。この関数では送信する先のChannelと送信するファイルを指定するだけで簡単に送信することができます。

まとめ

今回は以前作成したチーム分けのbotにトーナメント表を作成する機能を追加しみました。

かなり手を抜いてしまいましたが、構成としてはそこそこのものができたと思っています。

ただ、今後、これを拡張していく上で、Discord側から受け取るコマンドに対し、どの関数を呼び出すのかというif文が増えていってしまうという問題を抱えているので、今後はそれをまずは対処しつつ、ROUND()クラスを純粋なGraph構造に近づけるように改造したいと思います。

次に加える新機能としては、まずリーグ戦を作れるようにすること。

次にリーグ戦の後にトーナメントを行うような場合に対応させることです。

以上、TeamMaker Botの話でした!!

ブログをスケジュール通りに更新するためにTrelloを使ってみるよ

概要

最近、私生活の忙しさからか、なかなかブログが更新できません。(というより、更新するネタができながらない) そこでいい加減ちゃんとやりたいことを管理して行こうかなと思い、その決意を込めてこれを導入しました。 f:id:ryo_udon:20190617181821p:plain

Trello

Trelloはここ数年で主にIT業界などで使われるようになったプロジェクト管理ツールです。 このツールの特徴は主に以下の5つだと思います。 * 無料で使える * マウスのドラッグアンドドロップで基本操作ができるため、使い方がシンプル * 機能ごとにラベルを分けることもできる * 複数人でプロジェクトの進捗を管理できる * Eleganttを追加することで自動でガントチャートを作ってくれる

特に、5つ目の機能は、視覚的に期限がパッとわかるのはとても便利です。

では使い方を説明していきます。

簡単な使い方

今回、私が公開したプロジェクトは以下になります。 Trello-RYO_Udon

使い方の説明といってもやることは、 * リストの作成 * カードの追加 * カードに期限を設定 * ガントチャートの生成 * カードごとに進捗に合わせ進捗状況の更新とリスト移動

です。 まず、それぞれのエリアの名称からです。 主にエリアは上下に大きく2つに分かれています。 上がガントチャート、下がボードと呼ばれる、やることをメモしたカードを貼り付けるエリアになります。 f:id:ryo_udon:20190617181125p:plain ではそれぞれのエリアをどのようにして使うのかを説明したいと思います。

リストの作成

まずプロジェクトを新たに立ち上げるとこのような画面になります。 f:id:ryo_udon:20190617181156p:plain

まずやることを書いたカードを貼る前に、進捗状況を把握するためのリストを作る必要があります。 イメージとしては

やることリスト一覧 ー> 今取り掛かっていること ー> 終わったこと

のようにリストを作成し、カードの進捗状況に応じて、これらのリストを行き来させて上げるという感じです。

今回はサンプルとしてこのようにリストを作りました。 f:id:ryo_udon:20190617181251p:plain

最後のPending(保留)は進行中に一度入れたものを何らかの理由で一定期間、作業しない、できない状態になったカードを入れるところにしています。

リストができるとあとは今やりたいと思っていることを細かく機能ごとに分けてカードに書いてあげる作業になります。

カードの追加・設定

カードの追加方法は二段階に分かれています。 まず、カードを追加する際はカードの追加をクリックします。

ここではカードの名前を設定します。 今回はこの記事を書くというカードを作ってみたいと思います。 以下のような名前でカードを作成しました。 f:id:ryo_udon:20190617181406p:plain このカードをダブルクリックすると以下のようなウィンドウがピックアップされます。 f:id:ryo_udon:20190617181440p:plain

次にどのプロジェクトなのか区別するためにタグをつけていきます。 タグはデフォルトで十種類ほどの色のタグが用意されており、そのタグに文字を追加することで簡単に区別やソートができるようになります。 f:id:ryo_udon:20190617181546p:plain 次に必要であれば、チェックリストを追加します。

今回だとブログの文章を書くために以下のようなリストを作成しました。 f:id:ryo_udon:20190617181615p:plain ちなみにこのチェックリストはチェックを入れるとチェックリスト内の達成率を示すパーセンテージが変わっていきます。

ちょっと達成感がでていいですね。

次に期限についてですが、これについては普通のGoogleなどのスケジュール表と同じくカレンダーで始まりと終わりを選択するだけです。

これをするだけで、簡単にやることリストを管理することができます。

ガントチャート

最後に視覚的にわかりやすくするガントチャートの追加方法です。 とはいっても追加方法は簡単です。 画面の上の方にある Eleganttと書かれている横をOFFからONに変更してあげるだけです。 最初は登録が必要ですが、一度登録すれば他のプロジェクトでもこの操作一つで再登録の必要もなく簡単にガントチャートを作成することができます。 f:id:ryo_udon:20190617181745p:plain

 最後に

とこのような感じで簡単にプロジェクトの管理ツールの紹介をしました。

これを使って、なるべくスケジュール通りに更新したいなー

メカニカルキーボードを買ってみたよ

カニカルキーボードを買ってみたよ

なぜ購入したのか

以前からメカニカルキーボードには興味がありました。 やっぱり打ち心地もキースイッチによってかなり違うし、 キーキャップの種類も様々なので、パソコンを日頃から触る人間として 自分だけのオリジナルキーボード、という単語には中々の魅力を感じていました。

そして、この間のGW、旅行で疲れた体と頭でネットサーフィンをしていた私は ついついポチってしまったのです。

購入したもの一覧

購入したのは以下の2つ

  • キーボード
  • キーキャップ

キーボード

まず、キーボードは以下のものを購入しました。

実際に購入したものは一つ型が古いものですが、上記の赤軸と呼ばれる、メカニカルキーボードのスイッチでも軽いタッチかつ、あまり音がならないものを選んで、たまたま安く売られていたメルカリで購入しました。 f:id:ryo_udon:20190512193113j:plainf:id:ryo_udon:20190512193126j:plainf:id:ryo_udon:20190512193139j:plain

ちなみにスイッチの種類や一般的なキーボード(メンブレンタイプ)との違いについてはこちらをご覧ください。 メカニカルキーボードを購入する上でこのスイッチは凄く重要なので、もし購入される際は一度、家電量販店のゲーミングキーボードのコーナなどで試し打ちをしてみるといいと思います。

私が今回、このキーボードを選んだ理由は主に以下の2つです。

  • 一度ぐらいUSキーボードを買ってみたい
  • コンパクトで取り回しのしやすいキーボードがほしい!!

その中で見つけたのがこのキーボードでした。 Fnの列を削り、5列にしているにもかかわらず、Fnキーを押しながらs,d,f,eを押すことで手を動かすことなくカーソル移動ができるという変態仕様に惚れて購入を決意しました。

キーキャップ

上のような理由で購入するキーボードは決まったのですが、如何せん、

すっごく、地味!!

というわけで、今回はキーボードの購入金額とほぼ同額で以下のキーキャップを購入しました。

f:id:ryo_udon:20190512194917j:plainf:id:ryo_udon:20190512194945j:plainf:id:ryo_udon:20190512195006j:plain

今回はAliExpressで購入しました。 結構これ以外にも色々とデザイン性に富んだラインナップなので、見ているだけで楽しいです。

いざ、装着

では、実際に装着していきます。
今回は、中古品を購入したので、キーキャップを変更するついでに掃除もしたいと思います。 キーキャップの取り外しにはこのような器具を使います。 f:id:ryo_udon:20190512200524j:plain

通常はキーボード購入時、あるいはキーキャップセットを購入した際に付属していますので、購入の際に確認してください。

まずこの器具を使ってバックスペースの部分から外してみます。 f:id:ryo_udon:20190512200230p:plain 今回のスイッチの種類を示す赤い軸と一緒に大きなホコリの塊が出てきました。 これをティッシュでちょこちょこ取ってあげながら一行ずつ付け替えて行きます。 f:id:ryo_udon:20190512200832j:plain

そしてその地味な作業を黙々と進めること5分ほどで、こうなりました。

じゃじゃーん f:id:ryo_udon:20190512201045j:plain

中々いい感じの色合いです。 ただ、お気づきの方もいるかと思いますが、実はこいつ、フランケンシュタインなんです。 そう、私の注意力不足で、まさかのキーのサイズと数が一部足らず、もともとのものを一部そのまま使っています。

実はこのツイートした後に、VとBが逆になっていることに気づき、慌てて修正しました。。。

終わりに

さて、今回早速購入したキーボードでこの記事を書いておりますが、 中々私にちょうどいいキーストロークでかなり気に入っています。 一部、フランケンシュタインになってしまったのが残念ですが、これもいい経験だと思って、 また新しいキーキャップを購入するさいには、サイズや数などについて、しっかり確認してから購入するようにしたいと思います。

Football Analyzer開発日記_その2

Football Analyzer開発日記_その2

github.com

概要

自分でサッカーを分析できるツールを作ろうという見切り発車な企画第二弾。

今回は第一弾にてmatplotlibを使ってフットボールコートを描く方法を説明した続き、そのコートに指定したフォーメーションでプレイヤーを描く部分の紹介になります。

プレイヤーを設置してみよう

まずは実行してみよう

まずは実行してみます。 今回は前回紹介したdrawFootballCourt.py, main.pyに加え、 以下のsetPlayer.pyを使用します

import sys,os
import numpy as np
from drawFootballCourt import *


COURT_SIZE = [72.5, 45]
#POSITION_LIST = "positionName.json"

class PLAYER():
    def __init__(self):
        self.name = ""
        self.uniNum = 0
        self.playerPos = ""
        self.playerID = 0
        self.pos = [0,0]

class TEAM():
    def __init__(self, homeAway):
        self.homeAway = homeAway
        self.member = [PLAYER() for i in range(11)]
        dir = os.path.abspath(__file__)
        #self.positionList = json.loads(dir[:-len("setPlayer.py")] + "positionName.json")
        self.SetPlayerPosition("4231")
        self.teamColor = ""

    def SetPlayerPosition(self, formation):
        lineMemberNum = []
        lineMemberNum.append(1)
        for i in range(len(formation)):
            lineMemberNum.append(int(formation[i]))

        #コートの端からペナルティスポットまでを引いたもの
        verticalLength  = COURT_SIZE[0] - 6.0
        verticalRes     = (verticalLength-5) / (len(lineMemberNum)-1)

        horizontalLength = COURT_SIZE[1] * 2

        i = 0
        memberIDNum = 0
        for memberNum in lineMemberNum:
            horizontalRes = horizontalLength / (memberNum+1)
            x = verticalLength - verticalRes * i
            for j in range(memberNum):
                y = COURT_SIZE[1] - horizontalRes * (j+1)
                self.member[memberIDNum].pos = [x,y]
                memberIDNum += 1
            i += 1


class PLAYER_SERVER():
    def __init__(self, fig, ax):
        self.fig = fig
        self.ax = ax
        self.dc = DRAW_COURT(fig, ax)
        self.dc.DrawCourt()

    def SetFullMember(self):
        self.teams = []
        self.teams.append(TEAM("home"))
        self.teams.append(TEAM("away"))

        self.teams[0].teamColor="red"
        self.teams[1].teamColor="blue"

    def DrawPlayers(self):
        for team in self.teams:
            for member in team.member:
                x = member.pos[0]
                y = member.pos[1]
                if(team.homeAway=="home"):
                    x *= -1
                    y *= -1

                self.dc.DrawPlayerCircle(x,y,team.teamColor)

    def Show(self):
        self.dc.Show()

実行するmain.pyは以下のように変更します。

import matplotlib.pyplot as plt
from setPlayer import *

home = 0
away = 1

# for drawing court
#if __name__ == '__main__':
#    fig, ax = plt.subplots()
#    dc = DRAW_COURT(fig, ax)
#    dc.DrawCourt()
#    dc.Show()

# for drawing player formation
if __name__ == '__main__':
    fig, ax = plt.subplots()
    ps = PLAYER_SERVER(fig, ax)
    ps.SetFullMember()

    ps.teams[home].SetPlayerPosition("4231")
    ps.teams[away].SetPlayerPosition("343")
    ps.DrawPlayers()
    ps.Show()

実際に実行してみると以下のような結果が得られます。 f:id:ryo_udon:20190504141632p:plain

コードの中身を解説

全体の構成としては、第一弾で説明したフットボールを描くDRAW_COURTクラスを持つPLAYER_SERVERがホーム、アウェイのチーム全体のプレイヤーの位置を管理しています。

class PLAYER_SERVER():
    def __init__(self, fig, ax):
        self.fig = fig
        self.ax = ax
        self.dc = DRAW_COURT(fig, ax)
        self.dc.DrawCourt()

    def SetFullMember(self):
        self.teams = []
        self.teams.append(TEAM("home"))
        self.teams.append(TEAM("away"))

        self.teams[0].teamColor="red"
        self.teams[1].teamColor="blue"

    def DrawPlayers(self):
        for team in self.teams:
            for member in team.member:
                x = member.pos[0]
                y = member.pos[1]
                if(team.homeAway=="home"):
                    x *= -1
                    y *= -1

                self.dc.DrawPlayerCircle(x,y,team.teamColor)

    def Show(self):
        self.dc.Show()

チームは別途TEAMというクラスを作成していて、11人のプレイヤーの格納と一括でポジションを設定するSetPlayerPosition()を持っています。
この関数はフィールドのサイズを入力されたフォーメーションの数字列に合わせてプレイヤーを配置します。
例えば4-2-3-1の場合はキーパーも加えた5列でペナルティエリアからハーフウェイラインまでの距離を分割し、
横方向はフィールドの横幅を各ラインの人数で分割して設置するようになっています。
これによって、3-4-3や4-4-2などのキーパーを入れて4列のフォーメーションでも、 一時期話題になった1-1-7(3-4)-1などのネタフォーメーションも表現できるようになっています。

class TEAM():
    def __init__(self, homeAway):
        self.homeAway = homeAway
        self.member = [PLAYER() for i in range(11)]
        dir = os.path.abspath(__file__)
        self.SetPlayerPosition("4231")
        self.teamColor = ""

    def SetPlayerPosition(self, formation):
        lineMemberNum = []
        lineMemberNum.append(1)
        for i in range(len(formation)):
            lineMemberNum.append(int(formation[i]))

        #コートの端からペナルティスポットまでを引いたもの
        verticalLength  = COURT_SIZE[0] - 6.0
        verticalRes     = (verticalLength-5) / (len(lineMemberNum)-1)

        horizontalLength = COURT_SIZE[1] * 2

        i = 0
        memberIDNum = 0
        for memberNum in lineMemberNum:
            horizontalRes = horizontalLength / (memberNum+1)
            x = verticalLength - verticalRes * i
            for j in range(memberNum):
                y = COURT_SIZE[1] - horizontalRes * (j+1)
                self.member[memberIDNum].pos = [x,y]
                memberIDNum += 1
            i += 1

プレイヤーはPLAYERというクラスを別途作って管理しています。
(現在はposしか使っていませんが、追々、プレイするポジションや選手名、背番号なども表示できるようにしたいと考えています。 後、できれば採点などをした際にはその選手のレーティングとMoMには星マークを描く、などの機能を実装していく予定です。)

class PLAYER():
    def __init__(self):
        self.name = ""
        self.uniNum = 0
        self.playerPos = ""
        self.playerID = 0
        self.pos = [0,0]

最後にコート上にプレイヤーを描く関数を紹介します。 まず、丸そのものを描く関数はdrawFootballCourt.pyにあります。

    def DrawPlayerCircle(self, x, y, color):
        circle = plt.Circle( xy=(x,y), radius=2, ec="w", fc=color, fill=True, zorder=2)
        self.ax.add_patch(circle)

中身はただ、matplotlibの円を描く関数を呼び出しているだけです。

今回、プレイヤーを描くために工夫したのは次のPLAYER_SERVE内の DrawPlayers() 関数です。

    def DrawPlayers(self):
        for team in self.teams:
            for member in team.member:
                x = member.pos[0]
                y = member.pos[1]
                if(team.homeAway=="home"):
                    x *= -1
                    y *= -1

                self.dc.DrawPlayerCircle(x,y,team.teamColor)

ここで、チームがhomeかawayかで正負を反転させることで、 プレイヤーの座標を同じ座標系で扱えるようにしました。

このようにした理由は、home, awayで同じサイドを扱おうとしたとき、それぞれでグラフでいうy座標系の条件が反転してしまうのが嫌だったからです。(これは追々、変えるかもしれません)

では最後に、以上のことを考えながら作ってみたものを色々なフォーメーションで試してみたいと思います。

色々なフォーメーションで試してみよう

4-2-3-1 vs 3-4-3 f:id:ryo_udon:20190504141632p:plain 4-3-3 vs 5-3-2 f:id:ryo_udon:20190511174347p:plain 4-4-2 vs 3-5-2 f:id:ryo_udon:20190511174351p:plain ちょっと5の部分をもっとジグザグにしたいなー

終わりに

今回は、まずいプログラムでフィールドとフォーメーションを描いてみました。 次はこの選手をマウスで移動させたり、一人が動くと他の11人も合わせて移動するような機能を作りたいと思います。

(後、もう少しフォーメーションにジグザグ感をプラスしたい)

以上!!

何かアドバイスやコメントなどもお待ちしています。

Football Analyzer開発日記_その1(サッカーコートを描いてみたよの巻)

Football Analyzer開発日記_その1

概要

自分でサッカーを分析できるツールを作ろうという見切り発車な企画第一弾。 まずはpythonの中でも大人気なグラフ作成ツール、matplotlibを使って、 簡単にサッカーのコートとその中にホーム、アウェイ両チームのポジションを描けるプログラムを 作ってみましたので、紹介したいと思います。 f:id:ryo_udon:20190504141632p:plain

github.com

matplotlibとは

matplotlibとはpythonで グラフを描画して画像を作成したり、簡単なアニメーションを作成することが可能なライブラリであり、 pythonが人気言語に数えられる大きな要因の一つとなっています。 基本的なライブラリの使い方は上のリンク先に書いてあるのですが、いざ自分で使おうと思うと いろいろとつまずくことがあるので、その点を踏まえて説明していきたいと思います。

インストール方法

ここではすでにpythonがインストールされていることを前提にお話します。 お使いのパソコンでターミナル(windowsならコマンドプロンプト)を開いていただき、

pip install matplotlib

と入力して貰えればインストールは完了です。 基本的にpythonをインストールした際に自動的にpipもインストールされるはずですが、 もし、pipがないと言われた場合はこちらを参考に インストールしてください。

matplotlibを使ってフットボールコートを描いてみる

コードを実行

drawFootballCourt.py を参照

import matplotlib.pyplot as plt
import numpy as np

COURT_SIZE = [72.5, 45]

class DRAW_COURT():
    def __init__(self, fig, ax):
        self.fig = fig
        self.ax = ax

    def DrawCourt(self):
        self.ax.set_xlim( -COURT_SIZE[0], COURT_SIZE[0])
        self.ax.set_ylim( -COURT_SIZE[1], COURT_SIZE[1])
        self.ax.set_aspect("equal")
        self.fig.set_facecolor("white")
        self.ax.set_facecolor("green")
        self.ax.set_alpha(0.7)
        plt.tick_params(labelbottom=False,
                        labelleft=False,
                        labelright=False,
                        labeltop=False)
        self.DrawCourtCircle()
        self.DrawCourtLine()

    def DrawCourtCircle(self):
        #センターサークル
        circle = plt.Circle( xy=(0,0), radius=9.150, ec='w', fc='w', fill=False, zorder=1)
        self.ax.add_patch(circle)

        #ペナルティスポット
        circle = plt.Circle( xy=( 61.5, 0), radius=0.5, ec="k", fc="k", fill=True, zorder=1)
        self.ax.add_patch(circle)
        circle = plt.Circle( xy=(-61.5, 0), radius=0.5, ec="k", fc="k", fill=True, zorder=1)
        self.ax.add_patch(circle)

    def DrawPlayerCircle(self, x, y, color):
        circle = plt.Circle( xy=(x,y), radius=2, ec="w", fc=color, fill=True, zorder=2)
        self.ax.add_patch(circle)


    def DrawCourtLine(self):
        #センターライン
        self.DrawLine( 0, 0,-COURT_SIZE[1], COURT_SIZE[1], zo=1)

        #ゴールエリア
        self.DrawLine( 67.0, 67.0,-9.16, 9.16, zo=1)
        self.DrawLine(-67.0,-67.0,-9.16, 9.16, zo=1)
        self.DrawLine( 67.0, COURT_SIZE[0], 9.16, 9.16, zo=1)
        self.DrawLine( 67.0, COURT_SIZE[0],-9.16,-9.16, zo=1)
        self.DrawLine(-67.0,-COURT_SIZE[0], 9.16, 9.16, zo=1)
        self.DrawLine(-67.0,-COURT_SIZE[0],-9.16,-9.16, zo=1)

        #ペナルティエリア
        self.DrawLine( 56.0, 56.0,-20.16, 20.16, zo=1)
        self.DrawLine(-56.0,-56.0,-20.16, 20.16, zo=1)
        self.DrawLine( 56.0, COURT_SIZE[0], 20.16, 20.16, zo=1)
        self.DrawLine( 56.0, COURT_SIZE[0],-20.16,-20.16, zo=1)
        self.DrawLine(-56.0,-COURT_SIZE[0], 20.16, 20.16, zo=1)
        self.DrawLine(-56.0,-COURT_SIZE[0],-20.16,-20.16, zo=1)


    def DrawLine(self, x1, x2, y1, y2, color="w-", zo=3):
        line = plt.plot([x1,x2],[y1,y2],color,lw=2, zorder=zo)


    def Show(self):
        plt.show()

実行部分が main.py を以下の状態にしてもらうとこちらのような結果が得られます。

import matplotlib.pyplot as plt
from setPlayer import *

# for drawing court
if __name__ == '__main__':
    fig, ax = plt.subplots()
    dc = DRAW_COURT(fig, ax)
    dc.DrawCourt()
    dc.Show()

# for drawing player formation
#if __name__ == '__main__':
#    fig, ax = plt.subplots()
#    ps = PLAYER_SERVER(fig, ax)
#    ps.SetFullMember()
#    ps.teams[0].SetPlayerPosition("4231")
#    ps.teams[1].SetPlayerPosition("343")
#    ps.DrawPlayers()
#    ps.Show()

コードの中身を解説

描画領域の宣言

まず、matplotlibは以下のような仕組みで描画を行っています。 詳細はこちらのページに載っていますが、ここでは簡単に説明します。

まず、main.pyの中から説明します。

import matplotlib.pyplot as plt
# for drawing court
if __name__ == '__main__':
    fig, ax = plt.subplots()

まず一行目の

import matplotlib.pyplot as plt

このpythonプログラムにmatplotlibの点や線などをプロットする ライブラリをpltという名前で呼び出しています。 この

import *** as ~~~

という呼び出し方はpythonでは良く使われるので覚えているといいと思います。 次に、

# for drawing court
if __name__ == '__main__':
    fig, ax = plt.subplots()

この部分で今回、グラフを描画するウィンドウ(Figure)とそのウィンドウの中でグラフを描くエリアを示す(Axes)を宣言しています。 ここではグラフを一つしか描かないため、引数は何も指定していませんが、 もし1つのウィンドウに例えば縦2列、横2列でグラフを描きたい場合は、

fig, ax_lst = plt.subplots(2, 2)

と、宣言するとax_lstの中にaxesがリストで格納されます。

描画時の初期設定

描画の初期設定は drawFootballCourt.py の中では

    def DrawCourt(self):
        self.ax.set_xlim( -COURT_SIZE[0], COURT_SIZE[0])
        self.ax.set_ylim( -COURT_SIZE[1], COURT_SIZE[1])
        self.ax.set_aspect("equal")
        self.fig.set_facecolor("white")
        self.ax.set_facecolor("green")
        self.ax.set_alpha(0.7)
        plt.tick_params(labelbottom=False,
                        labelleft=False,
                        labelright=False,
                        labeltop=False)

このDrawCourt()関数の中でいろいろと宣言しています。 まず、

self.ax.set_xlim( -COURT_SIZE[0], COURT_SIZE[0])
self.ax.set_ylim( -COURT_SIZE[1], COURT_SIZE[1])

では、グラフのx軸(横軸)、y軸(縦軸)の表示範囲を指定しています。 今回はグラフ全体を使ってフットボールコートを描きたいと思っているので、 COURT_SIZE の中に縦、横のコートの半分のサイズを配列として格納しています。

次にこちらの4行についてです。

self.ax.set_aspect("equal")
self.fig.set_facecolor("white")
self.ax.set_facecolor("green")
self.ax.set_alpha(0.7)

set_aspect("equal") はx,y軸の分解能を実際のピクセルで同じ幅にするという宣言です。 matplotlibの場合、これを宣言しないとx,yそれぞれで分解能が違うグラフが描かれるようになっています。 次に、ax.set_facecolor(), fig.set_facecolor() についてです。 これについては字の通り、figure, axesそれぞれの色を設定しています。 今回はfigureを白、axesを緑に設定しています。 set_alpha()は透明度を設定する部分になるので、これはあくまで好みになります。

最後に、軸に目盛りをつけない設定を以下の通りにしています。

plt.tick_params(labelbottom=False,
                labelleft=False,
                labelright=False,
                labeltop=False)

これも字の通り、グラフの上下左右の軸には何も表示してほしくないのでFalse を設定しています。

描画時の初期設定は以上になります。

センターサークルを描く

センターサークルとペナルティスポットを描くのが以下の関数になります。

    def DrawCourtCircle(self):
        #センターサークル
        circle = plt.Circle( xy=(0,0), radius=9.150, ec='w', fc='w', fill=False, zorder=1)
        self.ax.add_patch(circle)

        #ペナルティスポット
        circle = plt.Circle( xy=( 61.5, 0), radius=0.5, ec="k", fc="k", fill=True, zorder=1)
        self.ax.add_patch(circle)

        circle = plt.Circle( xy=(-61.5, 0), radius=0.5, ec="k", fc="k", fill=True, zorder=1)
        self.ax.add_patch(circle)

大まかに流れを説明するとmatplotlibのCircleを描く関数を呼び出し、 各種パラメータを設定した後、返り値として返ってきた構造体を axesに加えています。 matplotlibで複数の図形を描く際、add_patch() にて追加された図形の構造体が自動的に描画される仕様になっています。

今回はセンターサークルなどの円に加え、プレイヤーを示す円形もこの関数を利用しています。 plt.Circle() 内のパラメータを簡単に説明すると、

ec 外枠の色
fc 塗りつぶし部分の色
fill 塗りつぶすかどうか
zorder 描く優先順位

ここで一番重要なのがzorderです。

zorderは描く優先順位を示しており、数字が大きいほど優先順位が高くなります。 なので、センターサークルやこの後に説明するラインはなるべく低い数字を設定しています。 これをしなければ、ラインと重なったプレイヤーの丸がラインの下に潜りこむというなんとも微妙な見栄えになってしまいます。 これは単純にグラフを描く際にも使うことが多いので覚えておいても損はないかと思います。

次にラインを引いて行きます

ラインを引いてみる

    def DrawCourtLine(self):
        #センターライン
        self.DrawLine( 0, 0,-COURT_SIZE[1], COURT_SIZE[1], zo=1)

        #ゴールエリア
        self.DrawLine( 67.0, 67.0,-9.16, 9.16, zo=1)
        self.DrawLine(-67.0,-67.0,-9.16, 9.16, zo=1)
        self.DrawLine( 67.0, COURT_SIZE[0], 9.16, 9.16, zo=1)
        self.DrawLine( 67.0, COURT_SIZE[0],-9.16,-9.16, zo=1)
        self.DrawLine(-67.0,-COURT_SIZE[0], 9.16, 9.16, zo=1)
        self.DrawLine(-67.0,-COURT_SIZE[0],-9.16,-9.16, zo=1)

        #ペナルティエリア
        self.DrawLine( 56.0, 56.0,-20.16, 20.16, zo=1)
        self.DrawLine(-56.0,-56.0,-20.16, 20.16, zo=1)
        self.DrawLine( 56.0, COURT_SIZE[0], 20.16, 20.16, zo=1)
        self.DrawLine( 56.0, COURT_SIZE[0],-20.16,-20.16, zo=1)
        self.DrawLine(-56.0,-COURT_SIZE[0], 20.16, 20.16, zo=1)
        self.DrawLine(-56.0,-COURT_SIZE[0],-20.16,-20.16, zo=1)

これまでいろいろと説明してきましたが、ここが一番簡単な箇所になります。 というのも、この関数のなかではネットで調べた寸法をもとに線を引く関数を呼び出しているだけです。 今回はlinewidth(線の幅)を固定にしたかったので、別途DrawLineという関数を設けています。

ここまでを実行してみる

ここまで説明したソースコードを実行してみると以下の実行結果が得られます。 f:id:ryo_udon:20190504141525p:plain

コートにプレイヤーを配置してみる

少し長くなってしまったので、別の記事として後日投稿します。

/*コードブロックに言語名を表示*/ pre.code:before { content: attr(data-lang); display: inline-block; background: white; color: #666; padding: 3px; position: absolute; margin-left: -10px; margin-top: -30px; } pre.code { padding-top: 30px !important; }