ぐるっとぐりっど

日曜プログラマがいろいろ試してみたことを、後の自分のためにまとめておく場所

emacs on WSLでSuper/Hyperキーを使う方法

はじめに

この前の東京Emacs勉強会で雑談してたときに、Superキー、Hyperキーの話になり、Macは修飾キーが多くてうらやましかった。 キーバインド覚えやすいところうまっていて、追加で悩む状況でもあるし。

ということで自分もSuper/Hyperデビューすべく、WSLでのやりかたを調べてみた。

tokyo-emacs.connpass.com

環境

  • wsl
  • wsl上で動くEmacs
  • X410 (Xサーバ)

なので、emacs for windowsとか、mingwemacsではないです。その場合は、また他の手段を取る必要があるはず 1

なお、wslで使うXサーバといえば、xmingかvcxsrvが有名ですが、emacsとの相性がよくない(※)ので別の有償のXサーバを使ってる。 X410は、ストアアプリとして購入できるが、しょっちゅう80%Offセールしているので割引タイミングを狙いましょう。

(※) emacsとの相性がわるい問題については以下のスライドを

speakerdeck.com

目指すゴール

無変換キーをSuperキーとして、変換キーをHyperキーとして使えるようにする。

一般的には、WindowsキーがSuperキーにあたると思うし、なんなら設定なしでもそう動いてしかるべきなのだけど、そもそもOS側にトラップされてアプリケーションまでうまく届かなかったのと、Windows10になってWin+○のショートカットキーが増えてそれを無効にするのも不便だったので、上記の設定をおこなうことにした。

前置きは以上。

Superキー/Hyperキーを設定する

手段その1 AutoHotKeyを使う

多分これが一番早いと思います。 サンプルも多いはず。

自分は、なんとなくWindowsユーザランドでキーいじるやつが嫌だったので採用しなかった。 (CapsとCtrlの入れかえで、結局レジストリいじらないとうまくいかなかったりというのに以前ぶつかったりしたので)

手段その2 xmodmapを使う

xmodmapはlinuxというか、Xの世界でキーマップ変更するためのもの。調べてみると最近は別のがあるっぽいけど、xmodmapでうまく動いたので気にしない。

そうそうキーコードなんて変わらないと思うので、同じ設定で同じように動くはず。

手順1. キーコードを特定する

xev というコマンドを使うと、Xのアプリが起動して、そこでもろもろのイベントを標準出力に出して確認することができる。 これを使うと、変換と無変換のキーコードは以下のとおり、変換が129、無変換が131であることがわかる。

KeyRelease event, serial 33, synthetic NO, window 0x800001,
    root 0xf4, subw 0x0, time 426541921, (67,97), root:(179,232),
    state 0x0, keycode 131 (keysym 0xff22, Muhenkan), same_screen YES,
    XLookupString gives 0 bytes:
    XFilterEvent returns: False

KeyPress event, serial 33, synthetic NO, window 0x800001,
    root 0xf4, subw 0x0, time 426596187, (843,330), root:(955,465),
    state 0x0, keycode 129 (keysym 0xff23, Henkan_Mode), same_screen YES,
    XLookupString gives 0 bytes:
    XmbLookupString gives 0 bytes:
    XFilterEvent returns: False

手順2. xmodmapの動作確認する

キーコードがわかったら、xmodmap コマンドで一時的にキーシムを書きかえて動作確認してみる。

# xmodmap -e "keycode 131 = Super_L Super_L"
# xmodmap -e "keycode 129 = Hyper_L Hyper_L"

実行後、再度xevを使って期待通り変わっていることを確認する。

KeyPress event, serial 33, synthetic NO, window 0x800001,
    root 0xf4, subw 0x0, time 426819890, (125,123), root:(263,284),
    state 0x0, keycode 131 (keysym 0xffeb, Super_L), same_screen YES,
    XKeysymToKeycode returns keycode: 115
    XLookupString gives 0 bytes:
    XmbLookupString gives 0 bytes:
    XFilterEvent returns: False

先程はキーコード131が無変換だったのが、Superに変わっていることがわかる。

手順3. xmodmapの設定を作成する

調べると、xmodmap -pke を実行して、必要なところだけ書きかえましょう。というのが出てくるのだが、実際のところ必要な設定だけ書けばよかったので、いきなり .Xmodmap ファイルを作成する。 WSLで手持ちのXサーバ使う分には別にファイル名は何でもいいと思うのだが、ここは慣例に従っておく。(startxコマンドでx起動するときは、雛形で.Xmodmapを詠み込むのでファイル名重要)

自分の設定は、こんなかんじ。

https://github.com/grugrut/dotfiles/blob/master/.Xmodmap

clear  mod3
clear  mod4
!<muhenkan>
keycode 129 = Hyper_L Hyper_L Hyper_L Hyper_L
!<henkan>
keycode 131 = Super_L Super_L Super_L Super_L
add    mod3 = Hyper_L
add    mod4 = Super_L Super_R

デフォルトの状態だと、SuperキーとHyperキーが同じ修飾キーとしてあつかわれていて、Hyperキー単体でうまくうけとれないので使われていないmod3にHyperキーを割当ておいた。

手順4. 自動で適用されるようにする

xmodmap ~/.Xmodmap とコマンド実行すればよいのだけど、注意点が一つ。 xmodmapはXサーバに対して設定をおこなうコマンドなので、Xサーバが起動していない状態ではうまく動かない。 LinuxBSD使ってるときにもxmodmap使ってたけど、当時は常にxorg-serverが起動してたので今回はじめてそのこと知った。

ついでなので、OSログイン時にXサーバを起動すべく適当なbatを作成した。 https://github.com/grugrut/dotfiles/blob/master/_windows/startx.bat

start /b x410.exe

ubuntu.exe run "DISPLAY=127.0.0.1:0.0 xmodmap ~/.Xmodmap"

x410.exeが使っているXサーバ。バックグラウンド実行させたいので、/b オプションをつけてる。 そして、ubuntu.exe run をすることでwslでコマンド実行できる。

これを Windowsshell:startup に配置することでスタートアップ時に自動実行することができる。 ただし、直接おくよりもショートカットを配置することをおすすめする。直接配置すると、実行時にコマンドプロンプトが一瞬表示されてうっとうしいが、ショートカットであれば最小化して実行することができるので気にならないからだ。

まとめ

xmodmapを使うことで、他のアプリには影響なくwslのX使うアプリだけにいろいろ手をいれられることが確認できた。他にもその手の機能で便利なのありそう。


  1. たぶん w32-lwindow-modifier とかが使えるはず

cephを1ノードで構築する

最近作ったkubernetesクラスタのPersistent Volumeを何にするか考えていて、NFSでもいいのだけど、せっかくだからdynamic provisioningを体験するべく、cephを導入してみることにした。

とはいえ、冗長構成にすると、VMだらけになってうっとうしいので、1ノード(All-in-one)で作ってみた。

参考にしたページ

環境

いつものHyper-V上のfedora30のVM。 なんか1ノード構成でもディスク複数用意するっぽいのでそのようにした。ドキュメントみても複数ノード前提で書かれてて、1ノードの場合にディスクが本当に複数必要なのかは調べてない。

導入手順

はじめ、qiitaでRed Hatの中の人の手順でそのままやろうとしたのだけど、 ansible-playbook コマンドがうまく動かなかったので、そうそうにあきらめ。たぶん設定もれなのだろうけど。

で、ceph-deployを使うことにした。

dnfで入らなかった(pythonまわりの依存関係がおかしかった)ので、pipでインストール

# pip install ceph-deploy

あとはドキュメントどおりなのだけど

# mkdir my-cluster
# cd my-cluseter
# ceph-deploy new storage01.mshome.net

これで、 ceph.confが作成されるので、ちょっと編集。以下の行を追加した。

[osd]
osd_pool_default_size = 1
osd_crush_chooseleaf_type = 0

あとは、ceph-deploy installのときに、fedoraの場合、うまく存在するrepositoryをみにいってくれなかったので、直接指定

# ceph-deploy install --repo-url https://download.ceph.com/rpm-nautilus/el7/noarch/ceph-release-1-1.el7.noarch.rpm  storage01
# ceph-deploy mon create-initial
# ceph-deploy admin storage01
# ceph-deploy mgr create storage01
# ceph-deploy osd create --data /dev/sdb storage01
# ceph-deploy osd create --data /dev/sdc storage01
# ceph-deploy osd create --data /dev/sdd storage01

確認コマンド実行してみると、ちゃんとできたっぽい。

# ceph health
HEALTH_OK
# ceph -s
  cluster:
    id:     c067a00a-46f7-474f-90f3-5f244b5d695f
    health: HEALTH_OK
 
  services:
    mon: 1 daemons, quorum storage01 (age 7m)
    mgr: storage01(active, since 3m)
    osd: 3 osds: 3 up (since 11s), 3 in (since 11s)
 
  data:
    pools:   0 pools, 0 pgs
    objects: 0 objects, 0 B
    usage:   3.0 GiB used, 375 GiB / 378 GiB avail
    pgs:     

以上

kubernetesクラスタはDHCPは利用せずstatic ipで作成すべし

件名以上のことはなく、まあ普通の感性ならそうするよね、ということしかない

grugrut.hatenablog.jp

で作ったクラスタだが、残念ながらOS再起動時にぶっこわれてしまった。
手抜きしてDHCPでやっていたら、etcdやらapiserverやらが、元のIPアドレスに探しにいってしまい、必要コンポーネントがまるで起動しない。

ログをみてみると、
```
Jul 3 18:30:56 master01 kubelet[1437]: E0703 18:30:56.729641 1437 controller.go:115] failed to ensure node lease exists, will retry in 7s, error: Get https://lb01.mshome.net:6443/apis/coordination.k8s.io/v1beta1/namespaces/kube-node-lease/leases/master01.mshome.net?timeout=10s: dial tcp: lookup lb01.mshome.net on 192.168.217.225:53: read udp 192.168.217.233:46188->192.168.217.225:53: i/o timeout
```
みたいな感じで古いIPアドレス(192.168.217.233)を使おうとしているのがわかる。

/etc/kubernetes/manifests/etcd.yaml とかを見ると確かにIPアドレスがベタ書きされてるので、修正してみたけども状況あまりかわらず。
ホスト名解決で失敗しているので、coreDNSがあやしいのだけど。

いずれにしても、ipかわるたびにもろもろ修正するの手間でしかないので、static ipで作り直すことにする。
クラスタを構成するノードの情報がどこに、どのように格納されているかは気になるので、正しく動いたらおさえておきたいね。

そもそもDHCPでもいっか、て判断したのが、ドキュメントに
```
It is not recommended to use an IP address directly in a cloud environment.
```
とあったので、クラウドだとたしかにIP制御できないし、ホスト名で解決できる環境ならいいんだなって判断してしまったのだけど、これLoadBalancerの条件だったので、誤読ですね。

やっぱminikubeとかマネージドに頼らない自前クラスタ組んでみると、いろいろ予期せぬところではまって興味深いです

高可用性k8sクラスタをkubeadmで構築してみる

はじめに

最近コンテナに入学したので、おうちクラスタを作ってみることにした。PCのメモリ64Gまで増強したので仮想マシンばんばん建てる体力もあるし。

といいつつ、シングルマスタのクラスタは、昔はやったラズパイクラスタ作るときに体験済で、せっかくなのでマルチマスタクラスタを組んでみることにした。 苦労はしたけど、苦労したポイントが

  • ドキュメントを右往左往させられる
  • タイポしてた

が大半なので、ほぼドキュメントへのリンク集。

構成

同一WindowsマシンのHyper-V上の仮想マシンとして構築した

用途 ホスト名 OS CPU メモリ *1
マスターノード master01.mshome.net Fedora 30 2コア 2GiB
master02.mshome.net Fedora 30 2コア 2GiB
master03.mshome.net Fedora 30 2コア 2GiB
コンピュートノード node01.mshome.net Fedora 30 4コア 4GiB
node02.mshome.net Fedora 30 4コア 4GiB
node03.mshome.net Fedora 30 4コア 4GiB
ロードバランサ lb01.mshome.net Fedora 30 2コア 2GiB

どこの設定か調べてないけど、mshome.netにするとホストのWindows機からも、追加設定なしに名前解決できて便利だったので、FQDNはそのように設定した。

インストール

仮想マシンの作成

Hyper-V仮想マシンの複製が、(簡単には)できないので、めんどくさい。

docker-ceのインストール

基本的にはドキュメントの通り。fedoraではyumのかわりにdnfが採用されて、基本的にyumコマンドが使えるのだけど、yum-utils は使えないのでレポジトリ追加は、 yum-config-manager のかわりに、

$ sudo dnf config-manager --add-repo url

とする必要がある。

Yum-utils package has been deprecated, use dnf instead. See 'man yum2dnf' for more information.

adding repo from: https://download.docker.com/linux/centos/docker-ce.repo YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. http://dummy

みたいな出力なので、docker-ceのrepoファイルがおかしいのかと思ってたら、そうではなかった。

master/computeの構築

ドキュメントのとおり。後述の問題のとおり、weaveを使う場合は、構築自体は完了するが、その後問題がおきるので要注意。

Creating Highly Available clusters with kubeadm - Kubernetes

ロードバランサの設定

apiをmasterに分散するために、ロードバランサが必要になる。今回はhaproxyを使った。 /etc/haproxy/haproxy.cfgに適当に設定書いた。

frontend k8s
    bind *:6443
    mode tcp
    default_backend k8s

backend k8s
    balance     roundrobin
    mode        tcp
    option      tcp-check
    server master01 master01.mshome.net:6443 check
    server master02 master02.mshome.net:6443 check
    server master03 master03.mshome.net:6443 check

問題点

corednsが動かない

kubectl get podで見てみる、corednsだけが動いてない(きづかずkubernetes-dashboardもdeployしてから気付いた)

$ kubectl get all -n kube-system
NAME                                              READY   STATUS              RESTARTS   AGE
pod/coredns-5c98db65d4-phqkw                      0/1     ContainerCreating   0          71m
pod/coredns-5c98db65d4-tznld                      0/1     ContainerCreating   0          71m
pod/etcd-master01.mshome.net                      1/1     Running             0          70m
pod/etcd-master02.mshome.net                      1/1     Running             0          32m
pod/etcd-master03.mshome.net                      1/1     Running             0          33m
pod/kube-apiserver-master01.mshome.net            1/1     Running             0          70m
pod/kube-apiserver-master02.mshome.net            1/1     Running             0          32m
pod/kube-apiserver-master03.mshome.net            1/1     Running             0          33m
pod/kube-controller-manager-master01.mshome.net   1/1     Running             1          70m
pod/kube-controller-manager-master02.mshome.net   1/1     Running             0          32m
pod/kube-controller-manager-master03.mshome.net   1/1     Running             0          33m
pod/kube-proxy-kdfbj                              1/1     Running             1          54m
pod/kube-proxy-ndcdt                              1/1     Running             1          55m
pod/kube-proxy-qvpkr                              1/1     Running             1          54m
pod/kube-proxy-spwnw                              1/1     Running             0          33m
pod/kube-proxy-vhjtg                              1/1     Running             0          32m
pod/kube-proxy-vw4w5                              1/1     Running             0          71m
pod/kube-scheduler-master01.mshome.net            1/1     Running             1          70m
pod/kube-scheduler-master02.mshome.net            1/1     Running             0          32m
pod/kube-scheduler-master03.mshome.net            1/1     Running             0          33m
pod/kubernetes-dashboard-7d75c474bb-9rp8z         0/1     ContainerCreating   0          28m
pod/weave-net-5kf5b                               2/2     Running             2          54m
pod/weave-net-5rbgf                               2/2     Running             2          55m
pod/weave-net-69v4x                               2/2     Running             2          54m
pod/weave-net-bc4sb                               2/2     Running             0          32m
pod/weave-net-jql64                               2/2     Running             0          59m
pod/weave-net-k6d2h                               2/2     Running             0          33m

より詳しく見てみるとこんな感じ

$ kubectl describe po coredns-5c98db65d4-phqkw -n kube-system
Name:                 coredns-5c98db65d4-phqkw
Namespace:            kube-system
Priority:             2000000000
Priority Class Name:  system-cluster-critical
Node:                 master01.mshome.net/192.168.112.71
Start Time:           Sun, 23 Jun 2019 09:07:10 +0900
Labels:               k8s-app=kube-dns
                      pod-template-hash=5c98db65d4
Annotations:          <none>
Status:               Pending
IP:
Controlled By:        ReplicaSet/coredns-5c98db65d4
Containers:
  coredns:
    Container ID:
    Image:         k8s.gcr.io/coredns:1.3.1
    Image ID:
    Ports:         53/UDP, 53/TCP, 9153/TCP
    Host Ports:    0/UDP, 0/TCP, 0/TCP
    Args:
      -conf
      /etc/coredns/Corefile
    State:          Waiting
      Reason:       ContainerCreating
    Ready:          False
    Restart Count:  0
    Limits:
      memory:  170Mi
    Requests:
      cpu:        100m
      memory:     70Mi
    Liveness:     http-get http://:8080/health delay=60s timeout=5s period=10s #success=1 #failure=5
    Readiness:    http-get http://:8080/health delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:  <none>
    Mounts:
      /etc/coredns from config-volume (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from coredns-token-kwrq4 (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             False
  ContainersReady   False
  PodScheduled      True
Volumes:
  config-volume:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      coredns
    Optional:  false
  coredns-token-kwrq4:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  coredns-token-kwrq4
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  beta.kubernetes.io/os=linux
Tolerations:     CriticalAddonsOnly
                 node-role.kubernetes.io/master:NoSchedule
                 node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason                  Age                    From                          Message
  ----     ------                  ----                   ----                          -------
  Warning  FailedScheduling        60m (x11 over 74m)     default-scheduler             0/1 nodes are available: 1 node(s) had taints that the pod didn't tolerate.
  Normal   Scheduled               60m                    default-scheduler             Successfully assigned kube-system/coredns-5c98db65d4-phqkw to master01.mshome.net
  Warning  FailedCreatePodSandBox  60m                    kubelet, master01.mshome.net  Failed create pod sandbox: rpc error: code = Unknown desc = [failed to set up sandbox container "cfeba2e58c4cbca0af13ce92e804804077f336ca4f5f11bfb8aca05f1822164a" network for pod "coredns-5c98db65d4-phqkw": NetworkPlugin cni failed to set up pod "coredns-5c98db65d4-phqkw_kube-system" network: failed to find plugin "loopback" in path [/opt/cni/bin], failed to clean up sandbox container "cfeba2e58c4cbca0af13ce92e804804077f336ca4f5f11bfb8aca05f1822164a" network for pod "coredns-5c98db65d4-phqkw": NetworkPlugin cni failed to teardown pod "coredns-5c98db65d4-phqkw_kube-system" network: failed to find plugin "portmap" in path [/opt/cni/bin]]
  Normal   SandboxChanged          5m29s (x257 over 60m)  kubelet, master01.mshome.net  Pod sandbox changed, it will be killed and re-created.
  Normal   SandboxChanged          15s (x12 over 2m20s)   kubelet, master01.mshome.net  Pod sandbox changed, it will be killed and re-created.

failed to find plugin "portmap" in path [/opt/cni/bin]とあるので、そうなんでしょう、と調べてみると、今回使った(ドキュメントどおりにやってくと導入される)CNIは、weave netなんだけど、これはportmapが不要なので導入しても一緒には入らないので、必要なら自分で入れろ、ということのようだ。

https://www.weave.works/docs/net/latest/kubernetes/kube-addon/

バイナリダウンロードしてきて配置して無事起動した(正確には、loopbackも必要だった)

ダッシュボード

デフォルトではWebUIは存在せず、cliだけなので、ダッシュボードを入れた

https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/

NodePortかなんか使えば外から見れると思うけど、一旦,kubectl proxyして、sshポートフォワーディングで確認。

f:id:grugrut:20190623142246p:plain

とりあえず今日のところはここまで

*1:動的メモリの設定してるので参考程度。これ書きながら確認したらマスタに3GiB割り当てられてた。

:ensure tを:straight tにおきかえるところから、straight.elを使ってみよう。MELPAでもgithubでもどこのパッケージも取得できるよ

自分は use-package で、emacsのパッケージ管理および設定管理をしている。

use-packageは、遅延ロードとかを意識しなくてもよしなにやってくれるし、なにより :ensureキーワードを使うことで、MELPAからダウンロードも自動でやってくれるので、パッケージの取得と設定を一元管理することができるのがよい。

ただ、そんなuse-packageだけど、不満点がひとつあり、 package.el をラッピングしてるだけなので、当然のことながら package.el の対象のサイトである、elpa, melpaなどからしかパッケージを取得することができない。

なので、githubで管理されていて、melpaのレシピが作成されていないものとか、melpaにあるんだけどバグがあってフォーク版がgithubにあるとか、そういったものを管理するのがめんどくさい。 自分でダウンロードして、load-pathに配置して、という作業が必要になってしまうのだ。

かといって、そのためにcask使ってみるのもなあ。。。と思っていた。quelpaとかも使ってみたのだけどイマイチぱっとしなかったり。

そんななか、straight.elの存在を知って使ってみたのですが、これがけっこういいかんじ

github.com

何がいいって、use-packageのキーワードに追加してくれるので、何も考えなければ、これまで :ensure t と書いてたところを :straight t と一括置換するだけで、とりあえず乗り換えが完了するのです。

(use-package dash
  :straight t)

こんな感じで、あまり移行を意識しなくてすむのがよい。

melpaで管理されているもののフォーク版を使いたいものの例としては、key-chordがあります。

qiita.com

これも、 :straightキーワードをちょっと変えて取得先を指定するだけなので、非常に簡単に書けます。

(use-package key-chord
  :straight (:host github :repo "zk-phi/key-chord" :branch "master"))

正直、ここまで意識せずに移行できると思わなかったので、なぜ早く乗り換えなかったのか悔むばかり。以前からstraight聞いてたけど、readme.mdがちょっと読みにくくて、どこがキモかよくわからなかったのよね。。。まあ、だからこそこのエントリを書くにいたったのですが。

use-packageで最近知った機能3選

最近、.emacsの整理をしていて、use-packageがデフォルトでdiminishを付属させないことを知りました。

で、ついでにREADME.orgをあらためて読みかえしてたら、これまで知らなかった面白い機能がいくつかあることを知ったので紹介。
(package.elを使うと、emacs上でアップデートができてしまうので、破壊的な変更に気付かずに、いつのまにか挙動変わってる...ってことがしばしばあるのが厄介ですね)

ちなみに、use-packageの基本的な使いかたは、kai2nenobuさんの、この記事がお勧め。

qiita.com

imenuに対応させる

個人的に今回かなりのヒットだったのがこれ。

(setq use-package-enable-imenu-support t)
(require 'use-package)

とすると、emacslisp-modeの場合に、use-packageの各パッケージ設定がimenuに載ってきてくれる。
ポイントは、requireの前に書くこと。

f:id:grugrut:20190105202730p:plain
use-packageをimenuに対応

これまで、init.elから目的の設定を探すのに、isearchしてみたり、helm-swoopしてみたりと苦戦していたのだけど、一発でジャンプできるようになった。

use-packageの状況を確認する

(use-package-compute-statistics t)

しておくと、M-x use-package-reportで、各パッケージの読み込み状況(:configまで進んだとか、:initまでとか)がわかる。

f:id:grugrut:20190105203002p:plain
use-package-report

状況が変化した日時と時間がわかるのだけど、多分累計じゃないと思う。org-modeのloadがこんな短いわけがないので。

key-chordを設定する

key-chordは、emacsに同時押しを実現するパッケージです。
use-packageのマクロにこの設定を組み込むことができるらしい。

(use-package use-package-chords
  :ensure
  :config (key-chord-mode 1))

(use-package view
  :ensure
  :defer t
  :chords (("jk" . view-mode)))

こんな感じで、:chordsキーワードが使えるようになる。
ただ残念なのが、使えるのがオリジナルのkey-chordなので、暴発問題が残っている。
こちらの改良版の方が便利だと思うので使いどころかも(うまく上書きできればよさげだけど)

qiita.com

ちなみに、このマクロ拡張のための機能が提供されているようで、純正のもの以外にも、hydraを使えるようにするuse-package-hydraなんてのもあるみたい。

おわりに

README.orgを読むと、他にもいろいろ書いてある(:afterキーワードでandとかorとか使えるってことも知らなかった。使いどころは?だけども)ので、当然のことながら、原典をまずは読むべし。

Emacsで簡単にゲームが作れるgamegrid.el

この記事は、Emacsアドベントカレンダー 18日目の記事です。

qiita.com

ちょっとEmacsで、わけあって *1 ドット絵的なものを書きたく、いろいろ調べてるうちにgamegrid.elに出会いました。
gamegrid.elは、Emacs本体に同梱されている、まさにgridなgameを作るためのパッケージです。
gamegrid.elで調べると、いろいろ出てきますが、emacsに内蔵されているpongとかsnake、tetrisなんかに使われています。

そして、使いかたがよくわからず、ぐぐっても使ってみた的記事があっても使い方がよくわからなかった(みつからなかったけど、infoに該当セクションあったりするのかな?)ので、実際使われているものをソースコードリーディングしながら使ってみました。

ソースコードリーディングする上では、snakeゲームのソースコードがわかりやすかった。tetrisはちょっと複雑。
github.com

作ったもの

github.com

f:id:grugrut:20181217225251g:plain
作ったもの

よくある上から降ってくるキャラを避けるゲーム。キー入力周りのところが若干おかしい。

内容と解説

理解しきれてないところもあるので、間違いあればマサカリください。

どうやって描画するか

(defconst sample1-player 0)
(defconst sample1-block 1)
(defconst sample1-blank 7)
(defconst sample1-border 8)
(defconst sample1-space 9)

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L39-L43

gamegridの名のとおり、gridベースなので、x, y座標に対して、そのセルが何であるかを指定することになる。その指定のための定数定義。

(defvar sample1-player-options
  '(((glyph colorize)
     (t ?\040))
    ((color-x color-x)
     (mono-x grid-x)
     (color-tty color-tty))
    (((glyph color-x) [1 0 0])
     (color-tty "red"))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L83-L90

(defun sample1-display-options ()
  "."
  (let ((options (make-vector 256 nil)))
    (dotimes (c 256)
      (aset options c
            (cond ((= c sample1-blank)
                   sample1-blank-options)
                  ((= c sample1-player)
                   sample1-player-options)
                  ((= c sample1-block)
                   sample1-block-options)
                  ((= c sample1-border)
                   sample1-border-options)
                  ((= c sample1-space)
                   sample1-space-options)
                  (t '(nil nil nil)))))
    options))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L127-L143

先の定数あたりも使って、どのセルをどう描画するかを決めるところ。Xの場合は、 [1 0 0]のようにvectorで、r g bを0から1の範囲で設定する。
ターミナルの場合は、"red"のように、指定できる。ターミナルの場合になにが使えるのかは調べてない。
また、Xを使う場合は、画像をxpm形式で指定できるはず。未指定の場合は、デフォルトのブロック画像が使われる。

この辺を駆使するため、以下の初期化処理でいろいろ設定する。

初期化処理と毎ターンの描画

基本的にはメジャーモード書いて作る感じ。それ以外のgamegrid特有の内容だと以下な感じ

(defun sample1-start-game ()
  "."
  (interactive)
  (sample1-reset-game)
  ;;(use-local-map sample1-mode-map)
  (gamegrid-start-timer sample1-tick 'sample1-update-game))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L154-L159

gamegrid-start-timerで何ミリ秒間隔でどの関数をコールするかを設定できる。このタイマー間隔がFPS維持するような仕組になってるかどうかは未調査。
sample1-reset-gameの中では、 (gamegrid-kill-timer)でタイマーを念の為リセット + 以下のような感じで、gamegrid-init-bufferにより、gamegridで描画する範囲と初期値を指定している。

(defun sample1-init-buffer ()
  "."
  (gamegrid-init-buffer sample1-width (+ 2 sample1-height) sample1-space)
  (let ((buffer-read-only nil))
    (dotimes (y sample1-height)
      (dotimes (x sample1-width)
        (gamegrid-set-cell x y sample1-space)))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L173

ついでに、gamegrid-set-cellを使うことで、指定したセルを別の内容に塗り替えることができる。
なので、updateの内容も、変更点を計算して、キャラが移動する場合は、前の時刻のセルを消して、新しい座標のセルを描画する、とかの処理をすることになる。

ゲームの終了

(defun sample1-stop-game ()
  "."
  (interactive)
  (gamegrid-kill-timer)
  (gamegrid-add-score "sample1-scores" sample1-total-cell))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L210-L214

gamegrid-add-scoreを使うことで、ハイスコアランキング(ただしローカル)を簡単に作れて便利

その他

グリフをブロック以外に自分が描画したものも使えるらしいので、試してみたい。

まとめ

なんか旨い使いかたないかなーと思うのですが、更新が激しいものを作るには適してない(結構チューニングが必要)ようなので、ピクロス的なゲームとか作るのには向いてそうです。

*1:挫折したけど途中まで自分も秋口ぐらいからNESエミュレータ作ろとせっせと書いてた。なので、gongoさんのアドベントカレンダーがとても楽しみです