SevenAtOneDebug

プログラミングとネットワーク、セキュリティについて書くつもりです

vimからneovimに移行した

概要

筆者は,コーディングのためにvimを愛用してきましたが,使いたいプラグインの多くがneovimに集中しているため,移行することにしました.本記事では,それに伴うneovimの設定や,各種プラグインの設定手順について紹介します.

移行理由

長らくvimを使ってきたのですが,neovimに移行することにしました.プラグイン周りがその理由です.ClaudeCodeとの連携を含め,新しいプラグインがneovimに集中している印象を受けます.また,YouComplemeMeをはじめ,旧来から存在するプラグインのパフォーマンスがなかなか改善されないこともストレスに感じており,移行要因の1つとなっています.

vimの設定

neovimでも,できる限りvimと同じ操作感で使いたいため,基本設定については同様の内容とします.また,プラグインについても,vimで導入しているプラグインに操作感が近いものを選定したいです.その上で,neovimで利用できる有用なプラグインについては積極的に導入します.

現状のvimrcを以下に示します.

vimrc

" general
syntax on  " syntax highlight
set number " line number
filetype plugin on

" language
let $LANG='en_US'

" indentation 
set autoindent   " auto indent"
set shiftwidth=4 " Shift width -> 4 spaces
set tabstop=4    " tab ->  4 spaces
set expandtab    " replace tab to spaces

" backspace
set backspace=indent,eol,start

" ruler 
set colorcolumn=80 " a border line. We shouldn't write more per line.
set cursorline     " highlight cursor line
set cursorcolumn   " highlight cursor column

" wild menu 
set wildmenu
set wildignore=*.docx,*.jpg,*.png,*.gif,*.pdf,*pyc,*.exe,*.flvb,*.img,*.xlsx

" fold code with a syntax of a language
set foldmethod=syntax

" spelling
set spell spelllang=en_us

" disable the preview window
set completeopt-=preview

" search
set hlsearch   " highlight search results
set ignorecase " don't distinguish between upper and lower

" Color scheme"
colorscheme monokai 

" set undo file and directory
set undofile
if !isdirectory(expand("$HOME/.vim/undodir"))
    call mkdir(expand("$HOME/.vim/undodir"),"p")
endif
set undodir="$HOME/.vim/undodir

" vim airline"
let g:airline#extensions#tabline#formatter='unique_tail'

" when vim start, vim open NERDTree
autocmd VimEnter * execute 'NERDTree'

" load all plugins 
packloadall
" load help files for all plugins
silent! helptags ALL

" vim-plug manages these plugins
call plug#begin()
Plug 'vim-airline/vim-airline'         " a status line / a tab line
Plug 'vim-airline/vim-airline-themes'  " a status line / a tab line
Plug 'preservim/nerdtree'              " a file system explore
Plug 'preservim/nerdcommenter'         " a plug-in for code-commenting function
Plug 'ycm-core/YouCompleteMe'          " a code-completion engine
Plug 'dense-analysis/ale'              " a plug-in for providing linting
call plug#end()

セットアップ手順

本ガイドにおける,neovimの設定手順を以下に示します.

  1. neovim本体をインストールする

  2. 追加で必要なパッケージをインストールする

  3. 設定ファイルをgithubからcloneする

  4. cloneしたリポジトリに対してsymblic linkを設定する

  5. neovimを起動し,プラグインをインストールする

それぞれの作業について,順番に説明します.

1. neovim本体をインストールする

パッケージマネージャを使い,公開リポジトリに登録されているneovimパッケージをインストールします.debian系のOSの場合は,aptを使ってインストールします.

apt install neovim

注意:ubuntuのリポジトリ上では,古いバージョンのneovimが登録されていることがあるようです.0.10よりも古い環境では,導入するプラグインが正常に動作しませんので,以下のコマンドを事前に実行してください.

sudo add-apt-repository ppa:neovim-ppa/unstable

Arch Linuxの場合は,pacmanを使ってインストールします.

pacman -Ss neovim

2. 追加で必要なパッケージをインストールする

次に,導入予定のプラグインが依存しているパッケージをインストールします.今回の設定では,以下の4つのパッケージが必要です.

  • nerdfont
  • claudecode
  • ripgrep
  • (macism)

macismに関しては,後述するim-selectというプラグインを利用しない場合は,必須ではありません.なお,このガイドでは,macismおよびim-selectの設定については詳述しません.macismの導入方法については,ZSL氏の以下の記事を参考にすると良いでしょう.

https://zeroshotlog.com/blog/neovim-macos-setup-guide/

3. 設定ファイルをgithubからcloneする

筆者の設定ファイル用リポジトリを,githubからcloneします.リポジトリには以下のリンクからアクセスできます.

https://github.com/HoriK816/horik_dotfiles

cd ~
git clone git@github.com:HoriK816/horik_dotfiles.git

4. cloneしたリポジトリに対してsymbolic linkを設定する

neovimは,デフォルトでは"~/.config/nvim/"から設定を読み込みます.このディレクトリは自動では作られないため,手動で作成する必要があります.ただし今回の場合は,cloneしたリポジトリの中に既にnvimディレクトリがありますので,これに対するリンクを.config内に配置すればよいです.

cd ~/.config
ln -s ~/horik_dotfiles/nvim .nvim

macismを導入しない場合は,im-selectの設定ファイルは削除しておきます.

rm ~/horik_dotfiles/nvim/lua/plugins/im-select.lua

5. neovimを起動し,プラグインをインストールする

"nvim"とタイプしてneovimを起動します.neovimが起動すると,パッケージマネージャのLazyが起動し,設定ファイルが存在するプラグインを自動的にインストールしてくれます.

プラグインのインストールが完了したら,neovimの設定は完了です.

設定の内容

本リポジトリの設定ファイルの内容について,簡単に説明します.

全体図

まずは,ディレクトリ構造を以下に示します.

[main][~/horik_dotfiles/nvim]$ tree
.
├── init.lua
├── lazy-lock.json
└── lua
    ├── config
    │   └── lazy.lua
    ├── keymaps.lua
    ├── nvim_tree.lua
    ├── options.lua
    └── plugins
        ├── blink.lua
        ├── claudecode.lua
        ├── comment.lua
        ├── everforest.lua
        ├── flash.lua
        ├── im-select.lua
        ├── lualine.lua
        ├── nvim-tree.lua
        ├── nvm-autopairs.lua
        ├── nvm-ts-autotag.lua
        ├── telescope.lua
        ├── treesitter.lua
        └── which-key.lua

4 directories, 19 files

neovimは,はじめにinit.luaをロードします.筆者の設定では,init.luaが項目ごとに別の設定ファイルを参照しています,このため,neovimはそれらを順次ロードし,最終的に設定全体を反映する形になります.init.luaが参照する設定ファイルは次の4つです.

  • options.lua
  • keymaps.lua
  • config.lazy
  • nvim_tree.lua

options.luaは,行番号の表示形式やインデントの幅など,neovim全体の基本設定に関する設定ファイルです.keymapsはキーの割り当て設定をまとめたファイルです.config.lazyはパッケージマネージャのLazyに関する設定ファイルです.nvim_treeは,後述するnvim-treeというプラグインに関する設定ファイルです.

各プラグインの設定ファイルは,/lua/plugins配下にまとめて配置されています.Lazyがこれらの設定ファイルをロードし,プラグインを有効化します.

init.luaが参照する設定ファイル

init.lua

-- basic options
require('options')

-- keymaps
require('keymaps')

-- plugins manager
require("config.lazy")

-- configs for plugins
require("nvim_tree")

init.luaの内容については,前述のとおりです.options.lua,keymaps.lua,config/lazy.lua,nvim_tree.luaの4つの設定ファイルを読み込んでいます.

options.lua

local opt = vim.opt

-- genral settings
opt.number   = true
opt.autoread = true

-- indentation
opt.autoindent = true
opt.tabstop    = 4
opt.shiftwidth = 4
opt.expandtab  = true

-- appearance
opt.colorcolumn       = "80"
opt.cursorline        = true
opt.cursorcolumn      = true
vim.opt.termguicolors = true

-- spelling
opt.spelllang = "en_us"

-- search
opt.hlsearch   = true
opt.ignorecase = true

options.luaでは,nvimの基本設定を記述しています.タブはスペース4つで置き換えるようにしています.また,カーソルの上下左右にガイド線を表示することで,カーソル位置を視認しやすくしています.さらに,筆者の環境では80文字目の列にルーラを表示しています.これは,多くのコーディング規約において,1行あたりの最大文字数が80文字と設定されていることに対応しています.

keymaps.lua

local keyset  = vim.keymap.set
local options = { noremap = true, silent = true }

-- moving between windows
keyset("n", "<C-h>", "<C-w>h", options)
keyset("n", "<C-j>", "<C-w>j", options)
keyset("n", "<C-k>", "<C-w>k", options)
keyset("n", "<C-l>", "<C-w>l", options)

-- telescope
keyset("n", "<leader>ff",
       function()
           require('telescope.builtin').find_files()
       end,
       { desc = 'Telescope find files' })

keyset("n", "<leader>fg",
       function()
           require('telescope.builtin').live_grep()
       end,
       { desc = 'Telescope live grep' })

keyset("n", "<leader>fb",
       function()
           require('telescope.builtin').buffers()
       end,
       { desc = 'Telescope buffers' })

keyset("n", "<leader>fh",
       function()
           require('telescope.builtin').help_tags()
       end,
       { desc = 'Telescope help tags' })

keymaps.luaでは,キーの割り当てに関する設定を記述しています.具体的には,ウィンドウ間の移動と,telescopeの機能の呼び出しについての,キー割り当てがあります. ウィンドウ間の移動については,neovimがノーマルモードの時に,CTRLキーを押しながらhjklを押すことで,対応する方向のウィンドウにカーソルが移るようにしています. telescopeは,ファイルや文字列を探すプラグインです.詳細については後述しますが,このファイルの設定によって,バックスラッシュが<leader>キーとなっており,以下の順番にキーを押すことで,対応する機能を呼び出すことができます.

  • \ff : ファイル検索
  • \fg : テキスト検索
  • \fb : バッファ検索
  • \fh : ヘルプドキュメントの検索

    /lua/config/lazy

-- load lazy

local nvim_path  = vim.fn.stdpath("data")
local lazy_path  = nvim_path .. "/lazy/lazy.nvim"

if not (vim.uv or vim.loop).fs_stat(lazy_path) then

    -- clone lazy from the remote repository
    local lazy_remote_repo = "https://github.com/folke/lazy.nvim.git" 
    local out = vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "--branch=stable",
        lazy_remote_repo,
        lazy_path })

    -- print error strings when lazy fails to load plugins 
    if vim.v.shell_error ~= 0 then
        vim.api.nvim_echo({
          { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
          { out, "WarningMsg" },
          { "\nPress any key to exit..." },
        }, true, {})
        vim.fn.getchar()
        os.exit(1)
    end
end

-- at first, neovim load lazy 
vim.opt.rtp:prepend(lazy_path)


-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"

-- Setup lazy.nvim
require("lazy").setup({

  -- import your plugins
  spec = {
    { import = "plugins" },
  },

  -- automatically check for plugin updates
  checker = { enabled = true },
})

config/lazyは,プラグインマネージャのLazyに関連する設定です.公式ページのインストールガイドから入手したものを整形して使っています.公式のインストールガイドについては,以下のリンクを参照してください. https://lazy.folke.io/installation

nvm-tree.lua

-- disable netrw and enable the plugin
vim.g.loaded_netrw       = 1
vim.g.loaded_netrwPlugin = 1

-- replace "Ex" command 
local trigger_word  = "Ex"
local nvim_tree_cmd = function()
                          vim.cmd("NvimTreeToggle")
                      end

vim.api.nvim_create_user_command(trigger_word, nvim_tree_cmd, {})

nvm-tree.luaは,nvm-treeというエクスプローラの呼び出しに関する設定ファイルです.neovimにおいては,netrwというエクスプローラがデフォルトで実装されており,これがExコマンドが発行された際に起動します.しかし,ここでは,netrwの代わりにnvm-treeを使いたいため,Exコマンド発行時にはnvm-treeが起動するようにしたいです.この設定ファイルでは,Exコマンドで実行される機能を上書きすることにより,それを実現しています.

pluginsの設定ファイル

ここからは,各プラグインの設定について紹介します.導入プラグインの一覧を以下に示します.

  • everforest
  • lualine
  • nvim-tree
  • nvm-autopairs
  • nvm-ts-autotag
  • comment
  • blink
  • telescope
  • flash
  • treesitter
  • which-key
  • im-select
  • claudecode

    plugins/everforest.lua

-- reference
-- https://github.com/sainnhe/everforest/blob/master/doc/everforest.txt

return {
    {
        "sainnhe/everforest",
        priority = 1000,
        lazy = false,
        config = function()

            -- custom settings
            vim.g.everforest_background                = "hard"
            vim.g.everforest_enable_italic             = 1
            vim.g.everforest_disable_italic_comment    = 0 
            vim.g.everforest_cursor                    = "orange" 
            vim.g.everforest_transparent_background    = 0
            vim.g.everforest_dim_inactive_windows      = 0
            vim.g.everforest_sign_column_background    = 'none'
            vim.g.everforest_spell_foreground          = 'none'
            vim.g.everforest_ui_contrast               = 'high'
            vim.g.everforest_show_eob                  = 1
            vim.g.everforest_float_style               = 'dim'
            vim.g.everforest_diagnostic_text_highlight  = 1
            vim.g.everforest_diagnostic_line_highlight  = 0
            vim.g.everforest_disable_terminal_colors    = 1

            -- load corlorscheme
            vim.cmd.colorscheme("everforest")

            vim.opt.guicursor = "n-v-c-sm:block-Cursor",
                                "i-ci-ve:ver25-Cursor",
                                "r-cr-o:hor20-Cursor",
                                "t:block-TermCursor"
        end,
    },
}

everforestは緑を基調としたカラースキームです.DarkとLightの2種類のモードと,Hard, Medium, Softの3種類のコントラスト設定があります.筆者はDarkモードのHardを使用しています.これによって,neovim上では,以下のような外観が得られます.

plugins/lualine.lua

return {
    'nvim-lualine/lualine.nvim',
    dependencies = { 'nvim-tree/nvim-web-devicons' },
    config = function()
        local lualine = require("lualine")

        lualine.setup({
            options = {
                theme = "gruvbox",
            },
            sections = {
                lualine_a = {"mode"},
                lualine_b = {"branch"},
                lualine_c = {"filename"},
                lualine_x = {"filetype"},
                lualine_y = {"encoding"},
                lualine_z = {"location"},
            }

        })
    end,
}

lualineは画面下部に補足的な情報を表示するプラグインです.筆者の設定では,以下の情報を左から順番に表示します.

  • neovimのモード
  • 現在の作業ブランチ
  • ファイルの名前
  • ファイルの種類
  • ファイルのエンコーディング
  • 現在のカーソルの位置

これによって,neovim上では以下のような表示が得られます.

plugins/nvim-tree.lua

return {
        "nvim-tree/nvim-tree.lua",
        version      = "*",
        lazy         = false,
        dependencies = {
           "nvim-tree/nvim-web-devicons",
        },
        config = function()
            local tree = require("nvim-tree")

            tree.setup({

                -- display position
                view = {
                    side  = "left",
                    width = 30,
                },
            })
            
            vim.api.nvim_create_autocmd("VimEnter", {
                callback = function()
                    require("nvim-tree.api").tree.open()
                end,
            })
        end,
}

nvim-treeは,neovim上で動作するファイルエクスプローラです.筆者は,vim上でNERDTreeを愛用していますが,nvim-treeでも類似する操作感が得られます.nvim-treeでファイルを開くためには,対象のファイルにカーソルを合わせてEnterキーを押します.すると,ファイルの内容を表示するウィンドウの選択を求められるので,ウィンドウに対応するアルファベットを入力します.これにより,指定したウィンドウに任意のファイルを表示することができます.

筆者は,neovim起動時に画面左側にnvim-treeを開くように設定しています.これは,Visual Stduioをはじめとする,多くのIDEの標準的な画面構成を意識したスタイルです.

plugins/nvm-autopairs.lua

return{
    "windwp/nvim-autopairs",
    event = "InsertEnter",
    config = true
}

nvm-autopairsは入力補完プラグインの一種です.引用符や括弧を入力した際に,自動的に対応する引用符や括弧を追加し,入力負担を軽減します.'の場合にはもう一つ',括弧の場合には,対応する閉じ括弧が補完されます.以下の画像で示しているのは,(の入力時に,対応する)が補完されている状況です.

plugins/nvm-ts-autotag.lua

return{
    "windwp/nvim-ts-autotag",

    config = function()
        require("nvim-ts-autotag").setup({
            opts = {
                enable_close          = true,
                enable_rename         = true,
                enable_close_on_slash = false
            },
        })
    end,
}

nvm-ts-autotagも入力補完プラグインの一種です.開始タグを入力した際に,自動的に対応する閉じタグを追加し,入力負担を軽減します.以下の画像で示しているのは,開始タグの<div>の入力に対して,終了タグの</div>が自動的に補完されている状況です.

plugins/comment.lua

return{
    'numToStr/Comment.nvim',
    opt = {
    },
}

commentはコメンティングプラグインの一種です.筆者は,vim上でNerdCommenterを愛用していますが,commentでも類似の操作感が得られます.commentを導入することで,コードの一時的なコメントアウトが簡単に実施できます.特定領域をコメントアウトするには,Visual Modeで範囲を選択した上で,"gc"と入力します.そして,再度同じ範囲を選択して"gc"と入力すると,コメントアウトが解除されます.以下の画像で示しているのは,172~180行目をcommentでコメントアウトした様子です.

plugins/blink.lua

return {
    'saghen/blink.cmp',
    dependencies = {},

    -- use a release tag to download pre-built binaries
    version = '1.*',

    ---@module 'blink.cmp'
    ---@type blink.cmp.Config
    opts = {

        -- compilation keys 
        keymap = { 
            preset = 'super-tab',

            -- push Shift+Tab to cycle candidates
            ["<S-Tab>"] = {"select_next", "fallback"},
        },
        appearance = {
          nerd_font_variant = 'mono'
        },

        -- (Default) Only show the documentation popup when manually triggered
        completion = { 
            documentation = {
                auto_show = false
            },

            list = {
                selection = {
                    preselect   = false,
                    auto_insert = true,
                },
            },
        },

        -- sources of candidate data
        sources = {
            default = {'lsp', 'path', 'buffer'},
        },

        -- fuzzy mathcher
        fuzzy = { 
            implementation = "prefer_rust_with_warning"
        },

    },

    opts_extend = { 
        "sources.default",
    },
}

blinkは文字列補完のプラグインです.文字列を入力すると,以下の画像のように補完の候補文字列が列挙されます.

筆者の設定では,Shift + TABで候補を巡回できます.所望の候補にカーソルが来たら,TABキーを押し,補完を実行します.以下の画像に示しているのは,"token_c"の入力から,"token_counter-1"を補完した様子です.

blinkの詳細については,以下のページを確認してください. https://cmp.saghen.dev/

plugins/telescope.lua

return {
    'nvim-telescope/telescope.nvim', version = '*',
    dependencies = {
        'nvim-lua/plenary.nvim',
        { 'nvim-telescope/telescope-fzf-native.nvim', build = 'make' },
    }
}

telescopeはプロジェクト内のあいまい検索をするためのプラグインです.ファイルや関数名などをうろ覚えの状態で探すことがありますが,完全一致検索では対象を見つけるのに時間がかかってしまいます.その点,telescopeは部分一致検索が使えますし,ケースの揺れも吸収してくれるので,検索が容易です.検索の対象としては,ファイル,テキスト,バッファ,ヘルプドキュメントを指定できます.筆者の設定では,以下のキー入力により,対象を指定した検索を実行できます.

  • \ff : ファイル検索
  • \fg : テキスト検索
  • \fb : バッファ検索
  • \fh : ヘルプドキュメントの検索

ファイル検索の様子を,以下の画像に示します.ここでは,ファイル名に"test_"という文字列を含むファイルを検索しています.telescopeでは,文字列の入力に合わせて候補ファイルが再検索されるうえに,右側のウィンドウで中身の確認もできるため,目的のファイルを簡単に発見できます.

コーディングの際には,関数や構造体などの定義を確認したいことがよくあります.そうした状況で,telescopeはテキストの全文検索ができるので,手早く検索ができて便利です.以下の画像で示しているのは,"typedef"というワードで検索をかけて,構造体の宣言を確認している様子です.

plugins/flash.lua

return {
  "folke/flash.nvim",
  event = "VeryLazy",
  ---@type Flash.Config
  opts = {},
  keys = {
    { "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" },
    { "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" },
    { "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
    { "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" },
    { "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" },
  },
}

flashはneovim内のカーソル移動を効率化するプラグインです.flashでは,移動先に存在する文字列を目印として,カーソルを移動します.これは,neovim本体が備える,カーソル移動の機能とは異なるアプローチです.

以下の画像群で,flashによるカーソル移動の様子を示します.例として,1枚目の画像のように,カーソルが58行目にある状況で,85行目の変数"margin"の宣言部に移動する際の操作を示します.

筆者の環境では,まず's'キーを押し,flashの検索を有効にします.この状態で,移動先に存在する文字列を入力します.ここでは,"margin"と打ちます.すると,2枚目の画像のように,"margin"に一致する箇所がハイライトされて,後ろにアルファベットが1文字表示されます.ここで,今回移動する先は85行目なので,85行目の"margin"の後ろに付与されている'q'を打ちます.

これによって,3枚目の画像のように,"margin"の位置にカーソルを動かすことができました.

plugins/treesitter.lua

return{
    "nvim-treesitter/nvim-treesitter",
    branch = "master",
    lazy  = false,
    build = ":TSUpdate",
    config = function()
        local tresitter = require("nvim-treesitter.configs")

        tresitter.setup({

            -- sytax highlight
            highlight = {
                enable = true,
            },
            
            -- indentation            
            indent = {
                enable = true,
            },
            
            -- language parsers 
            ensure_installed = {
                -- script language
                "bash", 
                "python",
                "javascript",
                "typescript",
                "lua",

                -- compiled language 
                "c",
                "cpp",
                "java",
                "vim",
                "rust",

                -- data 
                "query",
                "json",
                "yaml",
                "html",
                "css",
                "markdown",
                "markdown_inline",
                "vimdoc",
                "dockerfile",
                "gitignore"
            },
        })
    end,
}

treesitterは,言語の構文に合わせたシンタックスハイライトを実現するプラグインです.treesitterは正確なハイライトのためにプログラムの構文を解析しますので,あらかじめパーサをインストールしておく必要があります.これについては,設定ファイル中のensure_installedの項目に言語名を記載しておくことで,起動時にインストールされます.

plugins/which-key.lua

return{
  "folke/which-key.nvim",
  event = "VeryLazy",
  opts = {},
  keys = {
    {
      "<leader>?",
      function()
        require("which-key").show({ global = false })
      end,
      desc = "Buffer Local Keymaps (which-key)",
    },
  },
}

which-keyは,割り当て済みのキー操作をガイドするプラグインです.プラグインを色々と導入すると,キーの割り当てがわからなくなりがちです.which-keyは画面下部に,それぞれのキーバインドがどの操作を呼び出すか表示してくれます.キーバインドを暗記するまでの間は,which-keyを有効化しておくと良いでしょう.

plugins/im-select.lua

return{
  "keaising/im-select.nvim",
  config = function()
    require("im_select").setup({
      default_im_select = "com.apple.keylayout.ABC",
    })
  end
}

neovim上で日本語ファイルの編集をする際に,im-selectがあると便利です.この設定によって,insert modeからnormal modeに戻る際の言語切り替え操作が自動化されます.なお,im-selectを使うために,macOSではmacismを導入しておく必要があります.

plugins/claudecode.lua

return {
  "greggh/claude-code.nvim",
  dependencies = {
    "nvim-lua/plenary.nvim", -- Required for git operations
  },
  config = function()
        require("claude-code").setup({
  -- Terminal window settings
  window = {
    split_ratio = 0.3,      -- Percentage of screen for the terminal window (height for horizontal, width for vertical splits)
    position = "vertical",  -- Position of the window: "botright", "topleft", "vertical", "float", etc.
    enter_insert = true,    -- Whether to enter insert mode when opening Claude Code
    hide_numbers = true,    -- Hide line numbers in the terminal window
    hide_signcolumn = true, -- Hide the sign column in the terminal window
    
    -- Floating window configuration (only applies when position = "float")
    float = {
      width = "80%",        -- Width: number of columns or percentage string
      height = "80%",       -- Height: number of rows or percentage string
      row = "center",       -- Row position: number, "center", or percentage string
      col = "center",       -- Column position: number, "center", or percentage string
      relative = "editor",  -- Relative to: "editor" or "cursor"
      border = "rounded",   -- Border style: "none", "single", "double", "rounded", "solid", "shadow"
    },
  },
  -- File refresh settings
  refresh = {
    enable = true,           -- Enable file change detection
    updatetime = 100,        -- updatetime when Claude Code is active (milliseconds)
    timer_interval = 1000,   -- How often to check for file changes (milliseconds)
    show_notifications = true, -- Show notification when files are reloaded
  },
  -- Git project settings
  git = {
    use_git_root = true,     -- Set CWD to git root when opening Claude Code (if in git project)
  },
  -- Shell-specific settings
  shell = {
    separator = '&&',        -- Command separator used in shell commands
    pushd_cmd = 'pushd',     -- Command to push directory onto stack (e.g., 'pushd' for bash/zsh, 'enter' for nushell)
    popd_cmd = 'popd',       -- Command to pop directory from stack (e.g., 'popd' for bash/zsh, 'exit' for nushell)
  },
  -- Command settings
  command = "claude",        -- Command used to launch Claude Code
  -- Command variants
  command_variants = {
    -- Conversation management
    continue = "--continue", -- Resume the most recent conversation
    resume = "--resume",     -- Display an interactive conversation picker

    -- Output options
    verbose = "--verbose",   -- Enable verbose logging with full turn-by-turn output
  },
  -- Keymaps
  keymaps = {
    toggle = {
      normal = "<C-,>",       -- Normal mode keymap for toggling Claude Code, false to disable
      terminal = "<C-,>",     -- Terminal mode keymap for toggling Claude Code, false to disable
      variants = {
        continue = "<leader>cC", -- Normal mode keymap for Claude Code with continue flag
        verbose = "<leader>cV",  -- Normal mode keymap for Claude Code with verbose flag
      },
    },
    window_navigation = true, -- Enable window navigation keymaps (<C-h/j/k/l>)
    scrolling = true,         -- Enable scrolling keymaps (<C-f/b>) for page up/down
  }
})
  end,
}

claude-codeは,AIコーディングエージェントのClaude Codeをneovimに統合するプラグインです.コマンドモードで"ClaudeCode"と入力することで,neovim内に,新しいウィンドウが作成され,Claude Codeが起動します.起動後はターミナルのClaude Codeと同様の操作でプロンプトを入力し,作業を進めます.

まとめ

neovimの初期設定を実施し,作業を快適に進めるためのプラグインも導入することができました.新環境でのインストール作業自体は,簡単な手順を辿るだけであり,短時間で実施できます.今後はキーバインドの最適化や,linux環境でのIMEの設定などを進めたいと思います.

macOSで使用可能なKensington TBドライバの代替を探す

購入したモデル

Kensington社の"Exepert Mouse Wireless Trackball"を購入しました.このトラックボールマウスは,以下のような特徴を持っています.

  • 直径55mmの大型ボール
  • ボールを取り囲む物理的なスクロールリング
  • 4つの物理的なボタン(2ボタンの同時押しに対応)
  • 無線接続に対応
  • リストレストが付属

製品ページ

複数本の指で大型ボールを操作できるので,親指でボールを操作するタイプのトラックボールよりも細かい操作が容易です.また,一般的なマウスでは,長距離のスクロールに複数回のホイール操作を要しますが,このマウスではスクロールリングによって一度の操作で長距離のスクロールが可能であるため,手の疲労度を抑えられます.

総じて,ユーザの負担の少ないデバイスを目指してデザインされた,優秀なハードウェアといえます.一方で,ドライバソフトウェアに関してはトラブルが多く,これによりハードウェアの長所が帳消しになっています.この記事の目的は,そうしたドライバソフトウェアの代替法を探すことです.

発生した問題

筆者の作業環境はApple Silicon上で動作するmacOSです.これに対してKensington社は以下の2つのドライバソフトウェアを提供していますが,どちらも適切に機能しません.

発生する問題について詳述します.

Kensington Worksで発生する問題

Kensignton WorksはKensingon製品の公式ドライバソフトウェアですが,デバイスの認識と設定の有効化に問題があります.また,ドライバが実現する動作についても,特に2ボタンの同時押しの使用感が悪いです.

筆者の環境では,デバイスの初期設定の際に,適切な権限が付与されているにも関わらずデバイスを認識しませんでした.筆者はBluetooth接続とUSB recieverによる接続を切り替えながら再起動を繰り返すことでこの問題を解決できましたが,30分をここで空費しました.

そして,初期設定完了後は,今度はOS起動時にそれが有効化されない問題が発生しました.ドライバソフトウェア上では正常に接続されたことになっていても,ボタンの割り当てやカーソルの移動速度が適切に反映されません.この問題についても,再起動の繰り返しが有効ですが,毎朝PCを起動する度にその儀式が発生するのが非常にストレスフルです.

さらに,無事に設定が反映されても,ドライバの仕様自体に問題があるため快適に利用できません.特に問題があるのが,2ボタンの同時押しです.このマウスは,2ボタンの同時押しに対して機能を割り当てることができます.ただし,同時押しの受付時間が非常に短かく,コンマ数秒の精度での同時押しをしないと,意図通りの機能が呼び出せません.筆者の場合は,上半分の2ボタン同時押しをmacのMission Controlに割り当てていましたが,1週間ほど使用しても機能の呼び出しの成功率が上がらず,最終的にキーボード側で呼び出した方が早いという結論に達しました.

Kensington Konnectで発生する問題

Kensington KonnectはKensington Workの後継として開発されたドライバソフトウェアです.しかし,事態は改善されるどころかむしろ悪化しています.筆者の環境では,Kensignton Konnectによってデバイスが適切に設定できないだけでなく,マウスが操作不可能になり,そのうえ,キーボード入力も受け付けない状態に陥りました.仕方がないので,PC側の電源ボタンを使って強制終了させました.

以上より,Kensington WorksもKensington KonnectもApple Silicon上で動作するmacOS環境下ではトラックボールマウスを適切に制御できませんでした.

ドライバソフトウェアなしの動作

macOSが提供しているマウス用の標準ドライバだけでも,マウスを制御できます.そのため,Kensignton公式のドライバがなくてもトラックボールマウスを使用可能ですが,以下の問題があるため,快適とは言い難いです.

(a) カーソル移動量を柔軟に設定できない

(b) 物理ボタンのうち2ボタンが無効

(c) 2ボタンの同時押しが使用不可

(a),(b)については日常作業に支障をきたすため,回避策を見つける必要があります.(c)は,不幸中の幸いで,筆者はマウスにショートカットをあまり割り当てない主義であるため,(a)や(b)よりは深刻度は相対的に低いといえます.

(a)については,macOSのシステム設定アプリでも設定が効きます.しかし,筆者としては,OSで設定可能な最大の移動量を指定しても,なお足りません.1回の操作あたりのカーソル移動量が小さいと,必然的にボールの操作量は増えますから,かえって疲労度が増加してしまいます.手の疲労度を抑えるためにトラックボールマウスを導入したのですから,これでは本末転倒です.

(b)について,公式ドライバなしでこのマウスを使うと,4つの物理ボタンのうち下半分の2ボタンだけが有効になります.左下が左クリックで,右下が右クリックです.上半分の2ボタンは機能がセットされておらず,押しても何も起こりません.ブラウジングをする際に「進む」「戻る」をよく使うので,空いているボタンを活用したいところです.

代替ドライバソフトウェア

上述の(a),(b)の問題を解決するために"LinearMouse"というソフトウェアを使います.LinearMouseはマウスやトラックパッドの調整用のソフトウェアです.マウスカーソルやスクロールの移動速度や加速度の調整のほか,ボタンの再マップにも対応しています. 開発はオープンソースで進められており,無償で利用できます.インストールについては,homebrewが導入されている環境であれば以下のコマンドで実行できます.

brew install --cask linearmouse

インストールが完了したら,以下のアイコンをクリックして起動します.

起動できたら,「スクロール」,「ポインタ」,「ボタン」,「一般」のそれぞれ項目について,設定を行います.筆者の設定を,以下のスクリーンショット群に示します.これにより,カーソルの移動量も調整されます.また,左上の物理ボタンに「戻る」,右上の物理ボタンに「進む」がバインドされます.

スクロール設定

ポインタ設定

ボタン設定

一般設定

LinearMouseでは,2ボタンの同時押しは再現できませんでしたが,前述の(a)や(b)の問題は解決できました.これによって,今のところは快適に使用できています.

設定が有効化されるのは,LinearMouseが動作中のみなので,マシンが再起動すると再度LinearMouseを起動するまでの間は設定が無効になります.このため,マシン起動時に自動的にLinearMouseを立ち上げるように設定すると良いでしょう.macOSの場合は,システム設定アプリを開き,一般->ログイン項目の順に選択します.そして,「ログイン時に開く」の項目にLinearMouse.appを追加します.

結局使わなかった代替ドライバソフトウェア

LinearMouseの他にも代替ドライバソフトウェアが発見できますので,言及します.

SteerMouse

SteerMouseは,macOS上でマウスの高機能なカスタマイズを実現するソフトウェアです.使用方法についてはぺちか氏のnoteの記事がわかりやすいと思います.ただし,これは有償のソフトウェアであり,筆者はプライベート環境では有償のソフトウェアを好んで使わないので,今回は採用を見送りました. https://plentycom.jp/steermouse/

Mac Mouse Fix

こちらも人気のあるソフトウェアのようですが,最新バージョンについでは有償であるため,採用を見送りました.過去バージョンについてはgithubで落とせるようです. https://macmousefix.com/ja/

まとめ

macOS上でExepert Mouse Wireless Trackballを使うために, Kensignton社の公式ドライバソフトウェアを導入しましたが,適切に動作しませんでした.そこで,LinearMouseというソフトウェアを導入することで,デバイスの設定を代替しました.

徳丸本を学生4名で輪講した所感

経緯

脆弱性診断やバグバウンティを始めたいです.そのためには,脆弱性の発生原理を知ることが先決でしょう.Webセキュリティに関する王道的な教科書というと徳丸本ですが,ページ数も演習量も多いため,できれば輪講形式で進めたいです.そこで,この話をt_hに持ち掛けてみたところ割と乗り気だったので,ページ割りを組んで,さっそく開始することにしました.

開催形態

人数

本輪講の参加者はHoriKt_hYoshidaLTI氏,yamamoto-23氏の4名です.いずれもセキュリティとネットワークに関して多少の前提知識を持っています.HoriKやt_hは研究周りでWebセキュリティ技術を学習しています.YoshidaLTI氏は熱心なWebアプリケーションの開発者です.yamamoto-23氏は最近とあるWebアプリケーションの管理者となりました.

形式

発表者は,ホワイトボードを用いて担当範囲の内容を説明します.また,発表者は事前にレジュメを作成しておき,本番ではそれを参加者に配布します.

進行速度

発表者には,1セクションあたり約20ページを割り当てました.輪講の頻度は1週間あたり3回(月,水,金)です.つまり,1週間あたり約60ページの速度で進行します.

ページ割

  • 3章 : Webセキュリティの基礎

    • section 1 : HTTPとセッション管理 (p44-72)
    • section 2 : 受動的攻撃と同一オリジンポリシー,CORS (p73-93)
  • 4章 : Webアプリケーションの機能別にみるセキュリティバグ

    • section 1 : Webアプリケーションの機能と脆弱性の対応 (p97-118)
    • section 2 : 表示処理に伴う問題,XSS基本編 (p120-138)
    • section 3 : XSS発展編,エラーメッセージからの情報漏洩 (p138-150)
    • section 4 : SQLi (p151-174)
    • section 5 : CSRF (p175-196)
    • section 6 : クリックジャッキング,セッションハイジャック,推測可能なセッションID,URL埋め込みのセッションID (p197-217)
    • section 7 : セッションIDの固定化 (p217-230)
    • section 8 : オープンリダイレクト,HTTPヘッダ・インジェクション (p231-253)
    • section 9 : クッキー出力にまつわる脆弱性,メール送信問題の概要 (p253-269)
    • section 10 : メールヘッダ・インジェクション,ディレクトリトラバーサル (p270-288)
    • section 11 : 意図しないファイル公開,OSコマンドインジェクション (p288-307)
    • section 12 : ファイルアップロードにまつわる問題 (p308-327)
    • section 13 : FormCalc,ファイルインクルード,evalインジェクション (p328-350)
    • section 14 : 安全でないDeserialization,XXE (p350-370)
    • section 15 : 共有資源やキャッシュに関する問題,JSONとJSONPの概要 (p371-390)
    • section 16 : JSONエスケープの不備,JSON直接閲覧によるXSS,JSONPコールバック関数名のXSS,WebAPIのCSRF (p391-417)
    • section 17 : JSONハイジャック,JSONPの不適切な利用,CORSの検証不備,セキュリティを強化するレスポンスヘッダ,DOM Based XSSの概要・攻撃手法と影響 (p418-439)
    • section 18 : DOM Based XSSの脆弱性の原因と対策,Webストレージの不適切な使用,postMessage呼び出しの不備,オープンリダイレクト(p441-p455)
  • 5章 : 代表的なセキュリティ機能

    • section 1 : ログイン機能,パスワード認証を狙った攻撃への対策,パスワードの保存方法 (p457-478)
    • section 2 : 自動ログイン,ログインフォーム,エラーメッセージの要件,ログアウト機能,認証機能まとめ,ユーザ登録 (p478-p499)
    • section 3 : パスワード変更,メールアドレス変更、パスワードリセット,アカウント停止,アカウント削除,アカウント管理まとめ,認可,ログ出力 (p500-521)
  • 6章 : 文字コードとセキュリティ

    • section 1 : 文字コードとセキュリティ (p523-546)
  • 7章 : 脆弱性診断入門

    • section 1 : 脆弱性診断の概要,Bad Todo,脆弱ツールのインストール,nmap,OpenVAS, OWASP ZAPによる自動脆弱スキャン (p547-577)
    • section 2 : OWASP ZAPによる手動脆弱診断,RIPSによるソースコード診断,脆弱性診断実施上の注意,まとめ,脆弱性診断所サンプル (p578-606)
  • 8章 : Webサイトの安全性を高めるために

    • section 1 : Webサーバへの攻撃経路,なりすまし対策(p607-625)
    • section 2 : 盗聴・改ざんの経路,中間者攻撃,盗聴・改ざんの対策,マルウェア対策,まとめ (p626-642)
  • 9章 : 安全なWebアプリケーションのための開発マネジメント

    • section 1 : 安全なWebアプリケーションのための開発マネジメント (p643-657)

感想

僕はこの輪講を通して,Webセキュリティの概観を得られたと考えています.まずは,Webセキュリティに関連の深い仕様として,同一オリジンポリシやCORS (Cross-Origin Resource Sharing)を学習しました.また,SQLiやXSS,CSRF,セッションハイジャックなどの多くの攻撃の動作原理を理解できました.これらの事前知識により,今後は脆弱性診断などの学習をよりスムーズに進められると思います.

ここでは,今回の輪講において学習効果を高めたと思われるポイントを紹介します.

(a) 徳丸本の情報
(b) レジュメの使用
(c) ホワイトボードの使用

(a)は,教材の選定が良かったという話です.Webアプリケーションへの攻撃には多くの種類が存在します.このため,類似性に注目し,ある程度は一般化して考えていくべきでしょう.徳丸本では,攻撃の特徴や対策手法のカバー範囲などが動作原理を踏まえて議論されています.これらを順番に学習することで,攻撃や危険な実装のパターンを色々と見出すことができました.

(b)について,この輪講では,発表者に対して6p~10pくらいのレジュメ作成を課しました.発表者は,この作業によって自身の担当範囲への理解度を高めることができました.これによって,輪講の説明もわかりやすくなり,参加者全体が正確な理解を得ることにつながりました.

(c)について,道具としては,あえてスライドではなくてホワイトボードを選択しました.ホワイトボードで誰かに説明を試みるときには,話を進めながら時系列に沿って図表などを書くことが多いです.Webアプリケーションへの攻撃では,時系列的な流れがキーとなることが多いため,ホワイトボードとの相性が良かったと思います.

次に,輪講のスケジュールについての感想を述べます.

本輪講は,2025年12月中旬 ~ 2026年2月下旬の2ヶ月半で実施されました.これは適切なスピードであったと考えます.今回のペースであれば,週末を除くとだいたい1日おきに輪講が行われるため,直前の内容を忘却する前に次の話を聞くことができます.そして,各回の説明で以前の内容を使いますので,再度学習が行われて記憶が定着しました.

ただ,準備時間としてはギリギリだった点は注意すべきです.本ゼミでは各回で20ページ程度の内容を輪講しますが,発表者が担当範囲の内容を読んで理解するのに1.5~3時間を要しました.また,レジュメの作成に4~8時間を要しました.このため,輪講開始前にある程度の準備期間を置くことが望ましいと言えます.

最後に,この輪講の反省点を整理します.反省点は以下の2点です.

  1. 最新の内容が織り込めなかった
  2. 脆弱性診断について十分な情報量が確保できなかった

1について,この輪講で使用した「安全なWebアプリケーションの作り方」の第2版の出版は2018年です.そのため,全体的に話題の選定が古くなってきています.例えば.本書はIEの過去バージョンの仕様の説明が厚いですが,2026年にIEを使っている人はほとんどいませんので,これらは優先度が低いです.反対に,この本ではReactやVueといった現代のWeb環境に必須の技術に関する話題がありません.そうしたわけで,この輪講では最新の内容をフォローするための資料を追加調査するべきでしたが,そこまで手が回りませんでした.

2について,この輪講では,脆弱性診断については本で紹介されている内容をフォローするのに留まってしまいました.本書の演習では,Bad Todoというやられ環境の診断作業の流れを説明していますが,脆弱性診断の演習としては物足りない印象です.輪講内でも,もう少しXSS Gameのようなサイトを紹介して,演習量を稼いだ方が良かったと思います.

以上,2ヶ月半を通してWebセキュリティの概観を得ることができました.週あたり約60ページの進行で,学習スピードもちょうど良かったです.最新情報や,脆弱性診断の演習についても今後フォローしていきたいと思います.

宅内LAN環境の整備 -WifiブリッジをBuffalo製ルータに置換-

概要

 Wifiブリッジとして利用していたTP-LinkのWR841Nが不調になったため,Baffalo製のWSR-2533DHPに買い替えました.このルータにOpenWrtを導入し,設定作業を実施します.

これまでのあらすじ

horik.hatenablog.com

 前回までの記事で,作業用のセグメントに対して,3台のマシンを配置して,それなりに快適に作業ができるように設定してきました.これによって,Mac Miniからリモートアクセスをして,LinuxWindowsの環境も快適に使用できるようになりました.

経緯

Wifiブリッジ接続をしていたルータが不安定

ネットワーク構成図

 作成した環境で快適に作業をすることができていたのですが,ある日を境にネットワーク接続が不安定になり始めました.長時間のライブなどを見ていると,瞬間的にインターネット接続が切れる問題が多発しました.Macのネットワーク接続をWiFi接続にして,メインルータに対して接続すると安定するので,どうやらWiFiブリッジをしているルータ(WR841N)の動作が不安定になっているようです.

 ルータを何度か再起動してみたりしましたが,問題の解決には至りませんでした.このルータ自体が中古で購入したというのもあって,寿命なのかもしれません.そういうわけで,急遽新しいルータを買うことにしました.

ルータを新たに購入

WSR-2533DHP

 新しく購入したルータはBuffaloのWSR-2533DHPです.すでに販売終了したモデルですが,中古市場で2000円くらいで入手できます.この特徴的な色については手元に届いてから気がついたのですが,調べてみるとシャンパンゴールドということでした.

 ハードウェアスペックは次のとおりです.入手した価格を考えると,かなり優秀なルータと言えます.

SoC MT7621
クロック周波数 880MHz
コア数 2
RAM 128MB
ストレージ 8MB ?
有線LANポート 1000Mbps x 5 (LANx4, WANx1)
無線規格 IEEE802.11ac(1733Mbps) IEEE802.11n(800Mbps) IEEE802.11a/g(54Mbps) IEEE802.11b(11Mbps)

 OpenWrtも対応していますので,ファームウェアの移行が可能です.

今回やること

 今回はWSR-2533DHPを,以前使っていたTP-Link製ルータ(WR-841N)と同じように使えるようにします.OpenWrtを導入し,設定ファイルを記述していきます.具体的には以下のような操作で作業を進めます.

  1. 適切なバージョンのOpenWrtを取得
  2. WSR-2533DHPにOpenWrtを導入
  3. OpenWrtのネットワーク設定を記述
  4. 動作確認

作業

1. 適切なバージョンのOpenWrtを取得する

 OpenWrt Firmware Selectorという便利なサイトがあるようです.今回初めて知りました.ページにアクセスして,フォームに機種名を入力すると,ダウンロードすべきイメージファイルが表示されます.

firmware-selector.openwrt.org

OpenWrt Firmware Selectorによる検索結果

 "WSR-2533DHP"の場合は,ramips/mt7621向けのバージョン24.10.4のイメージを取得すれば良いみたいです.表示されているSYSUPGRADEと,KERNELを両方とも取得します.はじめにKERNELの方を動かして,そのあとSYSUPGRADEを導入するという流れです.

2. WSR-2533DHPにOpenWrtを導入

イメージを転送するための構成

 ルータ本体にKERNELの方のイメージを送信します.送信にはTFTPを使います.macの場合には,デフォルトでTFTPサーバがインストールされているので,それを利用します.

 TFTPサーバにルータがアクセスできるように,コンピュータのIPアドレスを192.168.11.1/24と同一ネットワークに配置します.今回はmac miniのLANポートに192.168.11.2を割り当てています.

 TFTPサーバの立ち上げは以下のコマンドで実施できます.

$ sudo launchctl bootstrap system /System/Library/LaunchDaemons/tftp.plist

 起動されると,/privateというディレクトリが作成されます.ルータのイメージを読み込むためにはこの直下の/tftpbootというディレクトリにファイルを配置します.配置に際しては,openwrt-24.10.4-ramips-mt7621-buffalo_wsr-2533dhpl-initramfs-kernel.binのファイル名を"linux.trx-recovery"に変更してから配置します.具体的には以下のような操作をします.

$ sudo mv openwrt-24.10.4-ramips-mt7621-buffalo_wsr-2533dhpl-initramfs-kernel.bin linux.trx-recovery
$ ls
linux.trx-recovery

 ここまでできたら,イメージを配布する準備は完了です.

AOSSボタン

 今度はルータ側を操作します.Baffalo製ルータの場合,起動直後にルータ前面のAOSS(AirStation One-Touch Secure System)ボタンを長押しします.これによって,LAN内のtftpdを探しにいくようです.

15:43:51.676874 IP 192.168.11.1.2835 192.168.11.2.69:
TFTP, length 37, RRQ "linux.trx-recovery" octet timeout 3
15:43:59.545688 IP 192.168.11.1.2835 192.168.11.2.69:
TFTP, length 37, RRQ "linux.trx-recovery" octet timeout 3 
15:44:07.426533 IP 192.168.11.1.2835 192.168.11.2.69:
TFTP, length 37, RRQ "linux.trx-recovery" octet timeout 3 
15:44:15.307389 IP 192.168.11.1.2835 192.168.11.2.69:
TFTP, length 37, RRQ "linux.trx-recovery" octet timeout 3

 しかし,今回は上のようにtftpdがタイムアウトしているようです.この問題の原因はよく分かりませんが,おそらく,baffaloが期待する時間内にtftpdが応答できていません.macのtftpdとbaffalo製ルータの相性問題のような気がします.

 仕方がないので,代わりのtftpdを使います.macのtftpdに関しては使用しませんので,落としておきます.サービスの終了に際しては以下のコマンドを使います.

$ sudo launchctl bootout system/com.apple.tftpd 

 pythonのモジュールのtftpyを使って,tftpdを作ることにします.ソースについてはgithub上に公開されていますので,これを取得します.

github.com

 cloneできたら,setup.pyを使ってインストールします.

$ python setup.py build
$ python setup.py install

 インストールができたら,実際にtfpdを作るスクリプトを書きます.ほとんどマニュアル通りのスクリプトです.非常に小さなコードです.

import tftpy

server = tftpy.TftpServer('/private/tftpboot')
server.listen('192.168.11.2', 69)

 このスクリプトをsudoで実行して,もう一度ルータ側の操作を実行します.

tftpdの通信

 上の画像のようにtftpの通信が発生して,イメージをルータに転送できているようです.無事に転送が終わると,ルータ上でOpenWrtが起動します.

 OpenWrt起動後は,ルータのIPが192.168.1.1/24がなっています.このため,mac側のアドレスもそれに合わせて192.168.1.2/24に変更し,sshでのアクセスができることを確認します.

sshでのアクセスに成功

 これによりバナーが表示され,無事にOpenWrtが起動したことが確認できました.

 つぎは,ここにsysupgradeのファイルを転送します.scpで転送をすれば良いのですが,macのscpは内部でsftpを使うため,openwrt側が対応できません.そこで,-Oをつけて,macのscpをscp準拠の動作にします.具体的には,以下のコマンドで,openwrtの/tmpにsysupgrade.binを転送します.

scp -O openwrt-24.10.4-ramips-mt7621-buffalo_wsr-2533dhpl-squashfs-sysupgrade.bin root@192.168.1.1:/tmp 

 転送後に,OpenWrt上でこのファイルを指定して,sysupgradeコマンドを実行します.ルータが再起動し,OpenWrtで起動することが確認できたら,この作業は完了です.

sysupgrade -n openwrt-24.10.4-ramips-mt7621-buffalo_wsr-2533dhpl-squashfs-sysupgrade.bin

3. OpenWrtのネットワーク設定を記述

 OpenWrtが導入できました.つぎはこのルータがWifiブリッジとして動作するように設定します.しかし,今回の設定内容は,以前の記事と同じですので,省略します.

 詳しくは以下を参照してください.ただし,LANの設定については,list ports 'eth0.1'のような書き方をすると,通信ができなくなったので注意してください.あとは,特に詰まるところはありませんでした.configファイルを直接書いても良いですが,Luciを介して設定するのが安全だと思います.

horik.hatenablog.com

4. 動作確認

 LAN内の端末から8.8.8.8にpingを打ってみます.

8.8.8.8への疎通を確認

 どうやら,適切にforwardingされているようです.Wifiブリッジは適切に動作していると言えます.以上で設定作業を終わります.

まとめ

 Baffalo製のWSR-2533DHPにOpenWrtを導入して,設定することができた.tftpdについては,python製のtftpdを利用することで,期待通りの動作をした.

Behavior Treeを使って敵NPCを動かしてみる Part 2

概要

 本シリーズでは,2Dシューティングの敵NPCを動かすための意思決定アルゴリズムについて考えています.

 Part 2においては,Part 1で紹介したBehavior Treeというアルゴリズムの実装を進めていきます.Behavior Treeには制御ノード,末端ノード,修飾ノードがありますが,このプロジェクトでは,制御ノードとして,SequnceNodeとSelectorNode,末端ノードとしてActionNodeとConditionNode,修飾ノードとして,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeをそれぞれ実装しています.

 開発環境にはProcessingを用いています.

前回までのあらすじ

horik.hatenablog.com

 前回のパートでは,敵NPCを動かすための,キャラクタAIのアルゴリズムとして,Behavior Treeを選択しました.また,Behavior Treeのアルゴリズムの概要についても示しました.

 今回のパートでは,実際にBehavior Treeの実装を進めていきます.

開発環境

processing.org

 開発環境としては今回もProcessingを使います.ProcessingはVisual Artのためのプログラミング環境です.図形描画や画面のアップデート,キー入力の処理などが簡単に利用できて,デモ用コードを書くのに適しています.

 将来的にはUnityへの移植を念頭に置いているため,Processing内での開発言語としてはJavaを選択します.

参考実装

www.behaviortree.dev

 参考実装としては,前回に引き続き,上記のリンクの実装を据えることにします.

 ただし,本記事の内容は,参考実装を100%カバーするものではありませんので,注意してください.

Behavior Treeの実装

全体のベースとなる実装

NodeStatus
enum NodeStatus{
    SUCCESS, FAILURE, RUNNING;
}

 Behavior Treeでは,全てのノードは3種類の結果のいずれかを返します.これを列挙型としてまとめておき,コード中で使っていくことにします.ノードがSUCCESSを返す場合は,ノードが正常状態で終了したことを意味します.FAILUREの場合は失敗して終了した場合を指します.RUNNINGはノードがまだ実行状態であることを意味します.

BehaviorTreeNode
class BehaviorTreeNode{
    String name;
    NodeStatus status;

    BehaviorTreeNode(String nodeName) {
        this.name = nodeName;
    }

    /* must override this method */
    NodeStatus evalNode() {
        return null;
    }

    /* debug function */
    void printName() {
        println("the name of this node is ", name); 
    }
}

 全てノードの基本となるBehaviorNodeは以上のように書きました.BehaviorNodeはノードの名前と,ノードの実行状態の2つのフィールドを持ちます.ノードの評価はevalNode()の中に実装していきます.printName()は無くても機能上問題ないですが,ノードが自身の名前を名乗れるようにしておくと,デバッグの時に便利なので一応書いています.

制御ノードの実装

 Behavior Treeの処理の流れを制御するのが制御ノードですが,今回はSequenceNodeとSelectorNodeの2つを実装します.

 制御コードは配下に複数のノードを持ちますので,子ノードの追加と,それらの評価のメソッドを持つベースクラスを作るところから実装を始めたいと思います.

class ControlNode extends BehaviorTreeNode{
    ArrayList children;

    ControlNode(String nodeName) {
        super(nodeName);
        this.children = new ArrayList();
    }

    void addChild(BehaviorTreeNode new_node) {
        children.add(new_node);  
    }

    void printAllChildren() {
        int len = children.size();

        for (int i=0; i<len; i++) {
            children.get(i).printName();
        }
    }
}

 BehaviorTreeNodeを継承して,ControlNodeを以上のように書きました.ControlNodeは子ノードを管理するための配列を持っており,addChild()で引数として渡されたノードをこの配列に追加します.

 printAllChildren()は無くても機能上は問題ありませんが,デバッグ用途で書いています. 

SequenceNode

 Sequence Nodeの動作を上図に示しました.Sequnce Nodeでは,配下に持っている全てのノードを順番に実行していきます.この際,配下のノードは処理結果を返してきますが,これは気にせずに,次々に実行します.配下のノードを全て実行し終えたら,SUCCESSを上位ノードに返します.処理を実行している間は常にRUNNINGを返します.

class SequenceNode extends ControlNode {
    int numberChildren         = 0;
    int numberExecutedChildren = 0;

    SequenceNode(String nodeName) {
        super(nodeName);
    }

    NodeStatus executeAllChildren() {
        BehaviorTreeNode processNode;
        NodeStatus sequenceStatus = null;
        NodeStatus processResult;

        numberChildren = this.children.size();
        if (numberExecutedChildren == numberChildren) {
            return NodeStatus.SUCCESS;
        }

        // check the child node currently being processed
        processNode = children.get(numberExecutedChildren);

        // decide sequence status depend on the result of the current node.
        processResult = processNode.evalNode();
        switch (processResult) {
            case SUCCESS:
                numberExecutedChildren++;
                sequenceStatus = NodeStatus.RUNNING;
                break;
            case FAILURE:
                numberExecutedChildren++;
                sequenceStatus = NodeStatus.RUNNING;
                break;
            case RUNNING:
                sequenceStatus = NodeStatus.RUNNING;
                break;
        }
        return sequenceStatus;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = this.executeAllChildren();
        return result;
    }
}

 ContolNodeを継承し,以上のようにして,SequenceNodeをつくりました.evalNode()をoverrideして,executeAllChildren()の実行結果をこのノードの処理結果として返すようにしています.executeAllChildrenでは,childrenに追加された子ノードを順番に実行していきます.

 Part1でも紹介したように,Behavior Treeにおいては,Tick()によって何度もツリーを辿ることによって処理が進んでいきます.SequenceNodeはその中で,配下のどのノードを実行しているのかを管理する必要があります.そのために,numberExecutedChildrenという実行されたノードの個数を保持するフィールドを持っています.この値を使って,executeAllChildren()の中で,その回で実行すべき子ノードをchildrenから引っ張り出します.子ノードの個数はnumberChildrenというフィールドに保持されており,numberExecutedChildrenとnumberChildrenが同じ値であれば,配下のノードは全て実行されたということですので,SUCESSを返します.そうでないなら,引っ張り出した子ノードを実際に実行して,その結果に応じた処理をします.

 子ノードの実行結果についてですが,SequenceNodeでは,子ノードの結果は気にしませんので,処理中は常にRUNNINGを返すようにしています.

SelectorNode

 SelectorNodeの動作を上図に示しました.SelectorNodeは,適当なアクションが取れるところまで処理を続ける制御ノードです.配下のノードを順に処理していく部分についてはSequenceNodeと同様ですが,配下のノードの処理結果を加味して動作します.

 配下のノードがSUCCESSを返してきた場合には,何らかの意味のある動作がとれたということですので,上位ノードにSUCCESSを返して処理を終了します.このとき,SUCCESSを返したノード以降のノードは実行されないということに注意してください.SUCCESSを返すようなノードがないときは,次々とノードが実行されていきますが,全てのノードにおいてFAILUREが返された場合には,意味のある行動をとることに失敗したということですので,Selector Node自体も上位ノードにFAILUREを返して終了します.

class SelectorNode extends ControlNode {
    int numberChildren         = 0;
    int numberExecutedChildren = 0;

    SelectorNode (String nodeName) {
        super(nodeName);
    }

    NodeStatus executeChildren() {
        BehaviorTreeNode processNode;
        NodeStatus selectorStatus = null;
        NodeStatus processResult;

        numberChildren = this.children.size();
        if (numberExecutedChildren == numberChildren) {
            return NodeStatus.FAILURE;
        }

        // check the child node currently being processed
        processNode = children.get(numberExecutedChildren);

        // decide selector status depend on the result of the current node.
        processResult = processNode.evalNode();
        switch (processResult) {
            case SUCCESS:
                selectorStatus = NodeStatus.SUCCESS;
                break;
            case FAILURE:
                numberExecutedChildren++;
                selectorStatus = NodeStatus.RUNNING;
                break;
            case RUNNING:
                selectorStatus = NodeStatus.RUNNING;
                break;
        }
        return selectorStatus;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = executeChildren();
        return result;
    }
}

 ControlNodeを継承して,上のようにSelectorNodeを書きました.evalNode()をoverrideして,executeChildre()で配下のノードを実行した結果をSelectorNodeの結果として返すようにしています.

 executeChildren()では,SequenceNodeと同じ要領で,配下の子ノードを順番に実行しています.違っているのはノードを全て実行し終えた時の処理と,子ノードの結果の扱いです.

 SelectorNodeでは,子ノードを全て実行し終えるということは,適切なアクションが取れなかったということを意味します.このため,numberChildrenとnumberExecutedChildrenの値が一致した場合では,SequenceNodeとは反対にFAILUREを返すようにしています.

 また,子ノードの処理結果の扱いに関しては,SequenceNodeでは子ノードの処理結果がSUCCESSであってもFAILUREであってもnumberExecutedChildrenをインクリメントして,次のノードに処理を進めていましたが,SelectorNodeではFAILUREの時だけ処理を次ノードに進めます.これによって,SUCESSを返す子ノードがあった場合にはそのノードに留まるので,以降のノードが実行されなくなります.SUCCESSを示すノードがあったときは,SelectorNode自体もSUCCESSを返すことになります.

末端ノードの実装

 末端ノードは,ゲーム中でのキャラクタの挙動に対応するノードです.大まかには行動に対応するActionNodeと意思決定に対応するConditionNodeがあります.

 ここで,末端ノードはキャラクタのアクションを作るわけですが,ゲーム上でのキャラクタのアクションというのは,ゲームやそのキャラクタに対して固有のものです.つまり,それ専用のコードがBehavior Treeのコードとは別にあるわけです.そのため,Behavior Tree上で実際に使う末端ノードは,Behavior Treeに関する処理と,キャラクタの挙動を作るコードを合わせたものということになります.

 こういうわけで,どこでも使える一般的な末端ノードを作ることは難しいです.そこで,アクションに与えられた残り時間の管理や,上位ノードへの結果の応答といった,Behavior Tree上での末端ノードの挙動だけを実装したActionNodeクラスを実装し,これを継承する形で,ゲームで使う末端ノード作ることにします.

 なお,図上ではActionNodeのみ記載していますが,ConditionNodeについても同じ要領です.

 Part2 (本パート)では,ベースとなるActionNodeとConditionNodeをそれぞれ実装します.これらをPart4で,ゲームで使う末端ノードに仕立てる予定です.

LeafNode
/* This is a base class for leaf nodes. */
class LeafNode extends BehaviorTreeNode{ 
    NodeStatus status;

    LeafNode(String nodeName){
        super(nodeName);
    }
}

 末端ノードのベースとなるクラスを上のように書きました.中身は非常にシンプルで,持っているフィールドは処理結果を示すstatusだけです.

 このクラスを作らずに,直接BehaviorTreeNodeを継承させることもできるのですが,末端ノードは末端ノードで,それ用のベースクラスから作った方が理解しやすい気がしたので使っています.

ActionNode

 ActionNodeは,キャラクタの行動を作る末端ノードのベースとなるクラスです.Behavior Treeにキャラクタの動作のスクリプトを紐づける役割をしています.このクラスが達成すべきことは次の2つです.

 1つ目は,キャラクタの行動に与えられた残り時間を管理することです.キャラクタが行動に使える時間は限られているのが普通ですので,残り時間を常に管理する必要があります.これによって,時間切れとなった時に,失敗とみなして次のタスクに移ったり,想定した時間よりも早く動作が完了した時に動作を繰り返したりすることが可能になります.

 2つ目は,上位ノードに対して行動の結果を通知することです.ActionNodeはキャラクタの行動に紐づくものですから,そのキャラクタがとった行動が成功だったのか,失敗だったのか,あるいはまさに今取り組んでいる途中なのか,を上位ノードに知らせなければなりません.

class ActionNode extends LeafNode{
    boolean isFinished      = false;
    boolean enableRepeat    = false;  // by default, repeating is disabled. 
    int requiredTotalFrames = 0;
    int remainedFrames      = 0;      // it's decremented every Action() calls

    ActionNode(String nodeName, int requiredTotalFrames){
        super(nodeName);
        this.requiredTotalFrames = requiredTotalFrames;
        this.remainedFrames      = requiredTotalFrames;
    }

    NodeStatus Action(){
        if(0 < remainedFrames){
            remainedFrames--;
            return NodeStatus.RUNNING;

        /* execute process to finite the action if remaindFrames reaches 0 */
        }else{

            /* reset */
            if(enableRepeat){
                isFinished = false;
                remainedFrames = requiredTotalFrames;
            }
            return NodeStatus.SUCCESS;
        }
    }

    @Override 
    NodeStatus evalNode(){
        NodeStatus result;
        result = this.Action();
        return result;
    }
}

 Action Nodeを上記のように実装しました.overrideしたevalNode()が呼ばれることで,Action()が実行された結果が返されます.

 Action()は前述した時間管理と,処理結果の決定に対応しているメソッドです.時間管理にはrequiredTotalFramesとremaindFramesの二つのフィールドを使います.requiredTotalFramesはこのクラスが作られる時に設定される値で,このアクションに対して割り当てられた時間(フレーム数)を表します.それに対して,remaindFramesは今残っている時間(フレーム数)を表します.remaindFramesは始めはrequiredTotalFramesで初期化されますが,実行されるたびに1ずつ減っていき,やがて0になります.remaindFramesの値が残っている間は,処理結果としてRUNNINGを返し続けて,0を下回ったときは処理が終了したということですのでSUCCESSを返します.

 また,このAction Nodeは処理の繰り返しができるようになっており,そのためにenableRepeatというフィールドを持っています.このフィールドをtrueに設定しておくと,remaindFramesが0になって処理が終わった時に,remaindFramesがリセットされます.これによって処理が繰り返されます.

 処理が終了したかどうかの判定にisFinishというフィールドを定義しています.これは後々,このクラスを拡張してキャラクタの専用の末端ノードを作る時に使っていくことになります.

ConditionNode

 ConditionNodeはキャラクタの意思決定に対応するノードです.ゲーム上であれば,目標との距離が規定の距離よりも近いか,必殺技が使用可能か,HPが規定以上残っているか,鍵が空いているか,など,キャラクタを取り巻く状況を評価して,その後の行動の決定に使います.

 ターン制のRPGボードゲームは別として,アクションゲームではキャラクタの思考のために特別に時間を割くことはないので,ConditionNodeでは1回のTick()で処理結果を返します.このため,処理結果はSUCCESSかFAILUREのどちらかで,RUNNINGを返すことはありません.

class ConditionNode extends LeafNode{
    boolean isMet = false;
    NodeStatus status;

    ConditionNode(String nodeName){
        super(nodeName);
    }

    void checkCondition(){
        if(isMet){
            status =  NodeStatus.SUCCESS;
        }else{
            status =  NodeStatus.FAILURE;
        }
    }

    @Override
    NodeStatus evalNode(){
        checkCondition();
        return this.status; 
    }
}

 ConditionNodeを上のように実装しました.overrideしたevalNode()でcheckConditionが呼び出されて,これによって処理結果がセットされ,これが応答されます.非常にシンプルです.

 checkCondition()は,条件が満たされているかどうかを判定するisMetというフィールドを元に処理結果をセットします.

 条件が満たされているのかの判定については,ゲーム固有,キャラクタ固有のコードになりますので,ConditionNodeを継承してキャラクタ固有の条件判定用ノードを作る際に記述していきます.

修飾ノードの実装

 修飾ノードは配下のノードの動作に変更を加えるためのノードです.指定回数の繰り返しや,処理結果の反転などが代表的です.

 様々な種類の修飾ノードが考えられますが,このプロジェクトでは,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeを実装します.

 なお,配下のノードの個数についてですが,修飾ノードでは基本的に1つしか子ノードを持たないということに注意してください.複数のノードを修飾の対象としたいときは,制御ノードを間に噛ませると良いです.

DecoratorNode

 制御ノードや末端ノードの場合と同様に,ベースクラスを作るところから始めていきます.

/* This is a base class for decorator nodes. */
class DecoratorNode extends BehaviorTreeNode {
    BehaviorTreeNode child;

    DecoratorNode(String nodeName) {
        super(nodeName);
    }

    void setChild(BehaviorTreeNode new_node) {
        this.child = new_node;
    }
}

 修飾ノードのベースクラスであるDecoratorNodeを上のように実装しました.DecoratorNodeは子ノードchildを持ち,これを修飾の対象とします.childを設定するために,setChild()というメソッドを定義しました.これによって,外部から引数として渡されたノード子ノードに設定できます.

InverterNode

 InverterNodeは,配下のノードの処理結果を反転する修飾ノードです.

/* InverterNode is a node to invert the result of child node and return it */
class InverterNode extends DecoratorNode {

    InverterNode(String nodeName) {
        super(nodeName);
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;
        result = child.evalNode();

        switch(result) {
          case SUCCESS:
            result = NodeStatus.FAILURE; 
            break;
          case FAILURE:
            result = NodeStatus.SUCCESS;
            break;
          case RUNNING:
            result = NodeStatus.RUNNING;
            break;
        }
        return result;
    }
}

 InverterNodeを上のように実装しました.overrideしたevalNode()で,結果の反転をしています.配下ノードのevalNode()を呼び出して得た結果がSUCCESSであった場合にはFAILUREを返し,FAILUREであった場合にはSUCCESSを返します.

 RUNNINGの場合は,まだ配下のノードが実行中ですのでRUNNINGを返します.

RepeaterNode

 RepaterNodeは,配下のノードを指定の回数だけ繰り返し実行する修飾ノードです.

/* RepeaterNode is a node to execute the specific child node repeatedly */
class RepeaterNode extends DecoratorNode {
    int repeatCount = 0;

    RepeaterNode(String nodeName, int repeatCount) {
        super(nodeName);
        this.repeatCount = repeatCount;
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result = child.evalNode();

        // it's termination condition of this node.
        if (repeatCount == 0) {
            return NodeStatus.SUCCESS;
        }

        switch (result) {
            case SUCCESS:
                result = NodeStatus.RUNNING;
                repeatCount--;
                break;
            case FAILURE:
                result = NodeStatus.FAILURE;
                break;
            case RUNNING:
                result = NodeStatus.RUNNING;
                break;
        }
        return result;
    }
}

 RepeaterNodeを上のように実装しました.繰り返し回数を示すrepeatCountというフィールドを持っています.evalNodeでは,配下のノードが成功するたびにこのカウンタをデクリメントしていき,最終的に0になった時にSUCCESSを上位ノードに返します.

 現状の実装では配下ノードがFAILUREとなった時に,ループから抜けられなくなるという問題があります.僕のデモでは特に問題にならなかったので,放置されているのですが,デモ以外の用途でこうしたコードを使うときは,もうすこし防衛的に書くべきです.

RetryUntilSuccessfulNode

 RetryUntilSucessfulNodeは,配下のノードが成功するまで繰り返し実行する修飾ノードです.数回は失敗する前提のアクションを取るときなどに使用します.ただ,試行回数の上限は決めておく必要があって,そうしないと,成功の見込みがない状態で試行を始めた時に,そのアクションから抜け出せなくなってしまいます.

class RetryUntilSuccessfulNode extends DecoratorNode {
    int numberAttempt;

    RetryUntilSuccessfulNode(String nodeName, int numberAttempt) {
        super(nodeName);
        this.numberAttempt =  numberAttempt; 
    }

    @Override 
    NodeStatus evalNode() {
        NodeStatus result;

        result = child.evalNode();
        switch (result) {
            case SUCCESS:
                result = NodeStatus.SUCCESS;
                break;
            case FAILURE:
                result = NodeStatus.RUNNING;
                numberAttempt--;
                break;
            case RUNNING:
                result = NodeStatus.RUNNING;
                break;
        }

        if (numberAttempt == 0) {
            result = NodeStatus.FAILURE;
        }
        return result;
    }
}

 上のようなコードでRetryUntilSuccessfulNodeを実装しました.試行できる残り回数をnumberAttemptというフィールドに保持していて,下位ノードの結果がFAILUREであると,これがデクリメントされて,再度試行されます.numberAttemptが0になってしまったら,もうこれ以上は試行できないので,上位ノードにFAILUREを返します.

 numberAttepmtが0になるまでは,失敗でも試行を続けますので,下位ノードがFAILUREを返した場合でも上位ノードにはRUNNINGを返します.

KeepRunningUntilFailureNode

 KeepRunningUntilFailureNodeは,配下のノードが失敗するまで繰り返す修飾ノードです.RetryUntilSuccessfulNodeを反対にしたようなノードなのですが,回数の上限を特に設けていないという部分が異なります.下位ノードがSUCCESSを返している状態は望ましい状態であると言えるので,名前にKeepと入っていることから分かるように,できる限りその状況を維持しようとします.

 ゲームの文脈では,炭鉱夫がひたすら金脈を掘り続けるみたいな状況で使い道があるかと思います.

class KeepRunningUntilFailureNode extends DecoratorNode{

    KeepRunningUntilFailureNode(String nodeName) {
        super(nodeName);
    }

    @Override
    NodeStatus evalNode() {
        NodeStatus result;

        result = child.evalNode();
        switch (result) {
          case SUCCESS:
            result = NodeStatus.RUNNING;
            break;
          case FAILURE:
            result = NodeStatus.FAILURE;
            break;
          case RUNNING:
            result = NodeStatus.RUNNING;
            break;
        }
        return result;
    }
}

 KeepRunningUntilFailureNodeを上のように実装しました.非常にシンプルで,下位ノードがFAILUREを返した場合以外では,上位ノードにRUNNINGを返し続けます.

クラスの継承関係の確認

 このパートでは,Behevior Treeを実装するために様々なクラスを作成しましたので,ここで一旦,クラス同士の関係を整理しておきたいと思います.

 クラスの継承関係を上図に示しました.全てのベースになっているのはBehaviorTreeNodeです.これを継承して,LeafNode,ControlNode,DecoratorNodeが作られました.それぞれが末端ノード,制御ノード,修飾ノードに対応しています.

 LeafNodeを継承して,キャラクタの行動に対応するActionNodeと,意思決定に対応するConditionNodeが作られました.

 ControlNodeを継承して,処理の順次実行をするSequenceNodeと,選択的な実行をするSelectorNodeが作られました.

 DecoratorNodeを継承して,処理の反転をするInverterNode,指定回数を繰り返すRepeaterNode,成功まで繰り返すRetryUntilSucessfulNode,失敗するまで繰り返すKeepRunningUntilFailureNodeが作られました.

まとめ

 Processingをつかって,Behavior Treeで必要なノード群を実装しました.

 制御ノードとして,SequnceNodeとSelectorNodeをそれぞれ実装しました.

 末端ノードとしてActionNodeとConditionNodeをそれぞれ実装しました.

 修飾ノードとして,InverterNode,RepeaterNode,RetryUntilSuccessfulNode,KeepRunningUntilFailureNodeをそれぞれ実装しました.

次回やること

 このシリーズではゲーム上の敵NPCをBehavior Treeを使って動かすことを目的としています.Part 1,Part 2ではBehavior Treeの実装を進めてきましたが,このアルゴリズムを適用するためのデモ用ゲームがまだ準備されていません.

 そこで,次回のPart 3では,デモ用として,簡単な2Dシューティグゲームを実装します.以前の弾幕シューティングの記事でも似たようなことを実はしているのですが,今回は,HPとダメージ計算も導入して,きちんと勝敗がつくようにします.

別パートへのリンク

Part 1 : 

horik.hatenablog.com

Part 2 :

現在地点.この記事です.

Part 3 :

工事中

Part 4 :

工事中

コード全体

 今回紹介したコードの全体は以下で確認できます.

github.com

可読性に関する本を読んだ -プログラミング作法,他2冊-

概要

 プログラムを書く機会が増えてきたので,プログラミング作法をはじめとして,コードの可読性に関する本をいくつか読みました.この記事では,それらの本を振りかえって,次に学ぶ内容などを検討します.

経緯

 この頃,プログラムを書く機会が非常に増えてきてました.それだけでなく,書くプログラムの規模もだんだんと大きくなってきています.小規模なプログラムであれば,見通しを確保するのは容易ですし,デバッグやテストも簡単なのですが,プログラムの規模が増大すると,こうしたことは途端に困難になります.

 そういうわけで,プログラムの可読性が非常に重要視されるわけですが,これまで,このトピックについてあまり真剣に学んでこなかったという部分があるので,今年度の前期の期間を使って,いくつか書籍を読んで勉強していました.この記事では,それらについて簡単に振り返ろうと思います.

読んだ本紹介

プログラミング作法

www.kadokawa.co.jp

 基本的には可読性の高いプログラムを書くことを目的とする本ですが,コードの良し悪しを検討するための様々な観点を与えてくれる本でした.

 表層的な改善については第1章にコンパクトにまとまっていて,慣習的な命名やシンプルな制御構造を使う重要性を説いています.関数などのインタフェースについては,4章で詳しく説明されます.

 この本は特にデータ構造についての説明が手厚いです.基本となる4つのデータ構造(配列,リスト,ツリー,ハッシュ)の性質が詳しく整理されています.そして,実際にプログラムが扱うデータに対して,どのようにデータ構造を選択するかというのを実例付きで説明しています.この辺りの説明のおかげで,データ構造の選択を,これまでよりもはるかに身近な問題として考えられるようになりました.

 本の中盤以降では,テストやデバッグ,パフォーマンスチューニングなどの,プログラミングの周辺の分野についても詳細に説明されていました.それぞれの項目では,基本スタンスがきちんと示された上で,発展的な手法が紹介されているので,実践もしやすいかと思います.

 最後には移植性や,課題分野において適切なプログラミング環境をどのように構築するのかというような話題にも触れられていました.これらの話題は,コードの可読性とは少し違う部分で,コードの特性を判断する上で役立つと思いました.

リーダブルコード

www.oreilly.co.jp

 あまりに有名な本ですが,コードの可読性に関して非常に記述が強い本です.本は全体として,表層的な改善と,根本的な改善に分かれています.

 表層的な改善の部分では,変数や関数に対する命名のほか,コードの配置,コメントの付け方などが指南されています.コメントについては,いわゆる教科書的な"Whyを書く"のとは違っていて,コードのヒントになるようなコメントは積極的につけることを提案しています.この辺りがプログラマの認知負荷を下げるという目的に,正面から向き合っている感じがします.

 根本的な改善の部分では,データ構造にまで手を入れて改善をかけたりしているのですが,改善が段階的に進むような構成で書かれていて,ライブ感がありました.これによって,ヘルパー関数を定義したり,処理をまとめたりといった,操作がどのタイミングでなされているのかが追えるようになっています.コードを書くとき,それぞれの局面で何を考えるべきか,についての非常に良い教材であると思います.

 文体はかなり易しめで読みやすいです.分量もコンパクトなので,読み返すのも容易かと思います.

良いコードを書く技術

gihyo.jp

 コードの可読性に関しての議論が非常にコンパクトにまとまっている本です.

 方針をきちんと説明するというよりは,コード例をたくさん示すことで,雰囲気を掴んでいくという印象でした.

 文体は優しいのですが,最終的な判断基準のところに関しては,経験を積むことで掴めてくる,というような説明が多用されていて,踏み込みが甘い印象です.上2冊がよめれば,特段これを読む必要がないかと思います.あくまで取っ掛かりとして読む用という感じでした.

次のステップ

 どの本でも共通して,テストの重要性について説いていましたが,これまではセオリー通りのテストというのが実施できていませんでした.これからは反省して,テスト手法のセオリーを学び,堅牢なテストが実施していきたいところです.さしあたっては,"単体テストの考え方/使い方"という本を読み進めています.テストの基本的なアイデアからきちんと説明されていて,今のところいい感じです.

book.mynavi.jp

 それから,コードの可読性の次は,システム全体についての広い視点を持てるようになりたいです.システム全体がうまく構成できれば,それぞれのコンポーネントの役割は明瞭になりますので,それらのコードも簡潔に書ける可能性が高まります.そのために,今後は,ソフトウェアアーキテクチャデザインパターンについても手を出していきたいです.手元に"ソフトウェアアーキテクチャの基礎"という本を積んでいるので,これを崩すところからでしょうか.

www.oreilly.co.jp

まとめ

プログラミング作法,リーダブルコード,良いコードを書く技術の3冊を読みました.それぞれの本から,コードの改善のテクニックが学べました.

今後は,テスト手法やソフトウェアアーキテクチャについても学びを深めていきたいです.

OWASP Juice Shopをビルドする

概要

 この記事ではOWASPが提供しているやられサイトであるJuice shopを導入方法を説明しています.今回はローカル環境へのインストールをソースコードからやる方法をとりました.x64のarch linuxを利用しました.

目的

 WAFの検証をするために,防御対象のWebサイトが必要になりました.目的を考えると,脆弱に作られたやられサイトの方が反応がわかりやすくて,検証に使いやすそうです.そこで,OWASP Juice shopという,OWASPが提供しているやられサイトを使うことにしました.

作業の流れ

pwning.owasp-juice.shop

 Juice shopの導入については,以上のページの内容に従ってやっていきます.今回はローカル環境へのインストールで,ソースコードからビルドしますので,以下のような流れで作業します.

 なお,作業環境はx64のArch Linuxです.debian系のディストリビューションを使用している場合は,パッケージマネージャーの部分の説明をaptで読み替えてください.

  1. nodejsをインストールする
  2. githubからコードを取ってくる
  3. npm install
  4. npm start
  5. 動作確認

実作業

nodejsをインストールする

pacmanでnodejsとnpmをインストールします.

sudo pacman -S nodejs
sudo pacman -S npm    

githubからコードを取ってくる

github.com

 上のリンクからコードを任意のディレクトリにクローンします.

git clone https://github.com/juice-shop/juice-shop.git

npm install

 クローンしたディレクトリに移動して,npm installを実行します.

cd juice-shop/
npm install

npm startして失敗した

 インストールが終わったら,npm startでスタートします.

npm start

 しかし,ここで失敗してしまったようです.pacmanでnodejsをインストールしようとすると最新版の24.6.0-1が入るのですが,Juice shopはv20~22の間のnodejsを要求しているようです.そこで,nodejsをdowngradeすることにします.

 調べてみると,nvmというnodejsのバージョンを管理するシステムがあり,これを使うことで簡単にバージョンを変えられるようです.

v22のnodejsを手にいれる

 まずは,nvmを導入します.

sudo pacman -S nvm

 nvmを有効にするには,さらに,.bashrcに次の一文を追加します.

source /usr/share/nvm/init-nvm.sh

 そして,bashをリロードして,nodejsのv22をインストールします.

source ~/.bashrc
nvm install 22

rebuildしてスタートする

 nodejsのversion 22が手に入ったので,これで先ほど一度導入してしまったjuiceを再度ビルドし直します.ビルドが終わったら,juice shopを起動します.

npm rebuild
npm start

動作確認

 Juice Shopを起動すると,デフォルトではlocalhostの3000番ポートで待ち受けますので,ここにブラウザでアクセスします.

 アクセスできました.これにて導入作業は完了です.

まとめ

 WAF検証のために,OWASP Juice Shopをソースコードから導入しました.

 Juice shopはnodejsのv20~v22を要求しており,nvmを使うことで,適切なnodejsを導入できました.