カテゴリー: Developments

  • ワードクラウドの定期更新 →

    1つできるとN個には簡単にスケールできる件。二番煎じ感あるけど再び。

    備忘:メモ書き___φ(. . )

    Janome
    https://github.com/mocobeta/janome

    word_cloud
    https://github.com/amueller/word_cloud

    Janome

    Janomeの形態素解析ではMeCabの辞書を使用していてパッケージのインストール時に辞書をビルドしている(setup.py、ipadic/build.sh、ipadic/build.py)。辞書に載っているか総当たりでチェックしている以下の処理がメイン処理かと思われる。

    Tokenizerクラス

    def __tokenize_partial(self, text, wakati, baseform_unk, dotfile):
        if self.wakati and not wakati:
            raise WakatiModeOnlyException
    
        chunk_size = min(len(text), Tokenizer.MAX_CHUNK_SIZE)
        lattice = Lattice(chunk_size, self.sys_dic)
        pos = 0
        while not self.__should_split(text, pos):
            encoded_partial_text = text[pos:pos + min(50, chunk_size - pos)].encode('utf-8')
            # user dictionary
            if self.user_dic:
                entries = self.user_dic.lookup(encoded_partial_text)
                for e in entries:
                    lattice.add(SurfaceNode(e, NodeType.USER_DICT))
                matched = len(entries) > 0
    
            # system dictionary
            entries = self.sys_dic.lookup(encoded_partial_text, self.matcher)
            for e in entries:
                lattice.add(SurfaceNode(e, NodeType.SYS_DICT))
            matched = len(entries) > 0
    
            # unknown
            cates = self.sys_dic.get_char_categories(text[pos])
            if cates:
                for cate in cates:
                    if matched and not self.sys_dic.unknown_invoked_always(cate):
                        continue
                    # unknown word length
                    length = self.sys_dic.unknown_length(cate) \
                        if not self.sys_dic.unknown_grouping(cate) else self.max_unknown_length
                    assert length >= 0
                    # buffer for unknown word
                    buf = text[pos]
                    for p in range(pos + 1, min(chunk_size, pos + length + 1)):
                        _cates = self.sys_dic.get_char_categories(text[p])
                        if cate in _cates or any(cate in _compat_cates for _compat_cates in _cates.values()):
                            buf += text[p]
                        else:
                            break
                    unknown_entries = self.sys_dic.unknowns.get(cate)
                    assert unknown_entries
                    for entry in unknown_entries:
                        left_id, right_id, cost, part_of_speech = entry
                        base_form = buf if baseform_unk else '*'
                        dummy_dict_entry = (buf, left_id, right_id, cost, part_of_speech, '*', '*', base_form, '*', '*')
                        lattice.add(Node(dummy_dict_entry, NodeType.UNKNOWN))
    
            pos += lattice.forward()
        lattice.end()
        min_cost_path = lattice.backward()
        assert isinstance(min_cost_path[0], BOS)
        assert isinstance(min_cost_path[-1], EOS)
        if wakati:
            tokens = [node.surface for node in min_cost_path[1:-1]]
        else:
            tokens = []
            for node in min_cost_path[1:-1]:
                if type(node) == SurfaceNode and node.node_type == NodeType.SYS_DICT:
                    tokens.append(Token(node, self.sys_dic.lookup_extra(node.num)))
                elif type(node) == SurfaceNode and node.node_type == NodeType.USER_DICT:
                    tokens.append(Token(node, self.user_dic.lookup_extra(node.num)))
                else:
                    tokens.append(Token(node))
        if dotfile:
            lattice.generate_dotfile(filename=dotfile)
        return (tokens, pos)

    ユーザー辞書にありますか?システム辞書にありますか?unknownですか?

    形態素解析ってこういうことですね。

    word_cloud

    ワードクラウドは、パラメタお化けだけど柔軟なメソッドでコマンドのラッパーも用意されている。

    classwordcloud.WordCloud(font_path=None, width=400, height=200, margin=2, ranks_only=None, prefer_horizontal=0.9, mask=None, scale=1, color_func=None, max_words=200, min_font_size=4, stopwords=None, random_state=None, background_color='black', max_font_size=None, font_step=1, mode='RGB', relative_scaling='auto', regexp=None, collocations=True, colormap=None, normalize_plurals=True, contour_width=0, contour_color='black', repeat=False, include_numbers=False, min_word_length=0, collocation_threshold=30)

    中身は画像処理で、ワードクラウドの文字列をメモリ上に描画して、矩形の領域を取得して、ベースとなる画像に上書きしている。上書きするときに他の配置済みの文字列と重ならない場所を全画素、総当たりで判定して配置位置を決定している。

    generate_from_frequencies関数

    # start drawing grey image
    for word, freq in frequencies:
        if freq == 0:
            continue
        # select the font size
        rs = self.relative_scaling
        if rs != 0:
            font_size = int(round((rs * (freq / float(last_freq))
                                    + (1 - rs)) * font_size))
        if random_state.random() < self.prefer_horizontal:
            orientation = None
        else:
            orientation = Image.ROTATE_90
        tried_other_orientation = False
        while True:
            # try to find a position
            font = ImageFont.truetype(self.font_path, font_size)
            # transpose font optionally
            transposed_font = ImageFont.TransposedFont(
                font, orientation=orientation)
            # get size of resulting text
            box_size = draw.textbbox((0, 0), word, font=transposed_font, anchor="lt")
            # find possible places using integral image:
            result = occupancy.sample_position(box_size[3] + self.margin,
                                                box_size[2] + self.margin,
                                                random_state)
    ・・・省略・・・

    occupancy.sample_position()から呼び出されるquery_integral_image()が、「query_integral_image.pyx」に書かれていて「.pyx」の拡張子となっている。調べてみるとCythonのコードでCのソースを生成しているとのこと。

    query_integral_image.pyx:35行
    query_integral_image.c:21,510行

    巨大な.cのコードは、#ifdefだらけで人間が読むコードではなくなっている。

    「query_integral_image.c」は、setup.pyに拡張モジュールとして書かれていてパッケージのインストール時にコンパイルされてlib配下にsoファイルが生成されていた。(query_integral_image.cpython-311-darwin.so)

    ext_modules=[Extension("wordcloud.query_integral_image",
                           ["wordcloud/query_integral_image.c"])],

    以下が、「query_integral_image.pyx:35行 」のソース

    # cython: language_level=3
    # cython: boundscheck=False
    # cython: wraparound=False
    import array
    import numpy as np
    
    
    def query_integral_image(unsigned int[:,:] integral_image, int size_x, int
                             size_y, random_state):
        cdef int x = integral_image.shape[0]
        cdef int y = integral_image.shape[1]
        cdef int area, i, j
        cdef int hits = 0
    
        # count how many possible locations
        for i in xrange(x - size_x):
            for j in xrange(y - size_y):
                area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
                area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
                if not area:
                    hits += 1
        if not hits:
            # no room left
            return None
        # pick a location at random
        cdef int goal = random_state.randint(0, hits)
        hits = 0
        for i in xrange(x - size_x):
            for j in xrange(y - size_y):
                area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
                area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
                if not area:
                    hits += 1
                    if hits == goal:
                        return i, j
    

    画像全体をピクセル単位で走査して、そこに文字列を置けるか判定している。

    area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
    area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]

    ただ、性能的な理由でsoファイルに切り出していると思われるけれど、なぜか全体を2回走査している。何かそれ相応の理由があるのか?

    ライブラリの中身がざっくり分かったところでAPI Referenceを眺めていたら以下のメソッドを発見。

    to_svg([embed_font, optimize_embedded_font, …])

    to_svg()のソースはテキストの書き出し処理でダラダラ長いだけの面白くないソースなので割愛。

    PNGファイルをSVGファイルに差し替えて一旦終わり。SVGファイル公開。

    フォント周り

    ワードクラウドのフォントを変更した場合、PNGファイルには反映されるけれどSVGファイルには反映されない現象が発生した。

    SVGファイルのフォントはローカル環境にインストールされているフォントが使われて指定したフォントがないと代替のフォントで表示されるとのこと。

    すべての環境で同じフォントを使用させたい場合、Webフォントを使用する必要があるが、それだとネットに繋がっていない場合や、フォントサーバが落ちている場合に現象が再現すると思われる。

    さらに調べるとWebフォントをbase64にエンコードしてSVGファイルに添付している人がいた。フォントファイルを丸ごとエンコードするとファイルサイズが巨大になるので使用している文字だけ切り出してサブセットのフォントを生成する必要があるとのこと。

    そしてサブセットのフォントを生成してSVGファイルに添付する場合、フォントの改変や再頒布が可能なライセンスが必要でありフォントのライセンスを調査した。

    • SIL Open Font License
    • Apache License 2.0
    • M+ FONT LICENSE

    ライセンスや使用条件を確認しながらフリーフォントをダウンロードした。フリーフォントを公開している人に感謝です。ありがとうございます。

    あとは修正作業。SVGファイルで使用している文字をpyftsubsetコマンドでフォントから切り出してきて、base64コマンドでエンコードして、SVGファイルを修正して、raspiにデプロイして、macOSとLinuxでbase64コマンドのオプションが異なるみたいなことにハマりつつ、SVGファイルにフリーフォントを反映できるようになった。最後にフォントをランダムに切り替える処理を追加して、バッチとして動くことを確認して、ミッションコンプリート!

    1つできるまでが長い。0から1が長い。

    正常ルートだけのやりたいことしか書いてないダメダメソースだけど40行から120行に膨らんでいる。

    # word_cloudパッケージを6/1にインストールしていたので10日間ぐらいの作業メモ。読み返すと修正したくなる。書き足したくなる。推敲したくなる。けど、書き殴りのメモ書きが正解やんなあ?

  • Raspberry Pi Pico W

    ポチった。マイコンにWi-Fiが付いたら何ができるー?わくわく

    https://www.amazon.co.jp/gp/product/B0BB69CH2T/

  • ワードクラウド

    「派」とか「次」って何だ?ってなるやん。

    1文字の場合は無条件にフィルタした方がいいかも。

    「今日は何の日」配下の文字列からワードクラウドを生成しています。毎回異なる画像が生成されるため、1時間に1回のバッチを実行しています。しばらくして飽きたら停止する予定です。「今日は何の日」の文字列は、画面描画後に動的に生成されているため、単純なGETリクエストでは取得できませんでした。ヘッドレスでSeleniumを起動して「今日は何の日」の文字列を取得し、Janomeで文字列を分かち書きし、WordCloudにデータを流し込み、最後にシェルスクリプトで画像を差し替えています。たった40行程度のライブラリを利用するだけの処理ですが、Pythonのライブラリは本当にすごいですね。

    ChatGPT添削&修正。

  • Raspberry Piケースきた

    そこのあなたー、あなたはこれにアクセスしています。

  • I’m Feeling Lucky →

    Wikipediaの映画のページにランダムに遷移します。

    遷移先は、映画の公開日などが今日の「mm月dd日」と一致する作品です。

    備忘:

    Wikipediaダンプファイル
    https://dumps.wikimedia.org/jawiki/latest/

    Wikipediaのダウンロードできるデータファイル一覧
    http://www.mwsoft.jp/programming/munou/wikipedia_data_list.html

    ファイル
    jawiki-latest-pages-articles.xml.bz2(3.9G)
    jawiki-latest-pages-articles.xml(16.3G)

    ツール:WikiExtractor
    https://github.com/attardi/wikiextractor

    • XMLファイルが大きすぎて、viでも開けません。ターミナルがフリーズします。
    • Python 3.11では、ツールが実行できません。既知のバグの可能性があります。
    • Python 3.10では、XMLからテキストファイルへの変換はできますが、変換するとページ右側に表示される公開日の情報などが完全に削除されることが判明しました。
    • コマンド/シェルスクリプトでXMLの解析を試みましたが、行指向の処理では判定後に上の行を取得できないという問題が発生し、断念しました。AWKを頑張って使うよりも、Pythonでパースする方が良いと判断し、Pythonスクリプトを作成しました。
    • ページ内のテンプレート「Template:Infobox Film」の情報をXMLファイルから抽出しました。
    • 1ページに複数回テンプレートが登場する場合があります。
    • テンプレートの開始「{{Infobox Film」、終了「}}」の書き方が統一されていません。終了「}}」がテンプレートの終了以外でも使用される場合があります。「|}}」「次作 =}} xxx」「}} 本文開始」など、終了の表現方法が複数存在し、単純な正規表現ではマッチングできませんでした。
    • 急がば回れですが、「Infobox Film」が含まれるページのみを抽出すると、作業が容易になります。(16.3GB → 41MB)
    • 「<page>」のタグ数は2,808,250です。
    • 「Infobox Film」のテンプレートを含むページ数は25,173です。
    • 「{{Infobox Film」と「}}」の間に[1][2]などの注釈リンクが含まれており、今日の日付がページに表示されていない可能性があります(抽出したデータの妥当性を検証できていません)。
    • テンプレートには改行が含まれており、1ページを1行のCSVで管理するのは難しいため、SQLiteを使用しました。
    • MySQLにデータを格納するためのツールが公開されています。最初からMySQLにデータを格納するべきだったかもしれませんが、MySQLはローカル環境で使用しているため、仕方ありません。

    ChatGPTに添削してもらったら丁寧な文章になった。

  • 世界時価総額ランキング10(1989 – 2023)

    時を戻そう。日本がんばれ〜

    ツール:Flourish https://flourish.studio/
    データ:Bing AI
    データ加工:Excel ピボットテーブル + XLOOKUP関数

    新しいBingでデータを集めた。一括でデータ収集させてCSV形式でダウンロードしたいのだけど現状では無理っぽい。1年ごとに検索してExcelにコピペする手作業が発生してる。しかも、チャットのレスが遅い… 持ってないデータを検索して解析してゴニョゴニョしてて時間がかかるとは思うもののまだまだ使いづらい。

    1990年と1991年はデータが見つからない。データも怪しいしランキング順位が大きく変わる年はグラフが正しく表示されない。逆再生したかっただけだからいいけど…

    時間は戻らないらしいな。時間とは何か?それが問題だ?

    正しそうなやつも貼っとく

  • ヘッダー画像の定期更新 ⤴️

    古き良き壁紙チェンジャー的なやつ。ハジマルー

    過去にtwitterおよびblogに投稿した画像をヘッダー画像に貼り付け。1日1回0:00に差し代わる予定。しばらくこれで。

    備忘:

    • ランダムな位置に重ならないように配置していくと30枚から40枚程度しか乗らない。
    • OpenCVパッケージをグローバルにインストールしてcronから実行した場合、OpenCVのパッケージが見つからないエラーが発生する。シェルスクリプトを被せてvenvの仮想環境を定期実行している。
    • macOSの濁点問題。まったく同じ表示の文字でも異なる文字コードが使用されてて不一致みたい事象あり。エンコードした。
    • ブラウザのキャッシュ問題。更新した画像が表示さない事象あり。Wordpress本体のPHPを1行修正した。アップデートでファイルが置き換えられた場合、再度修正が必要となるぞっと。
    (WORDPRESS_ROOT)/wp-includes/theme.php
    
    function get_header_image() {
    ・・・
        if ( is_random_header_image() ) {
            //$url = get_random_header_image();
            //キャッシュバスティング対応
            $url = get_random_header_image() . '?' . filemtime('./wp-content/themes/twentyseventeen/assets/images/header.jpg');
        }

    気付けない落とし穴的なデグレを仕込むことになるので本体には手を入れたくないのだが仕方ないのか?他に対処方法はないんかー?

  • オセロ by ChatGPT

    ChatGPTで生成したバグってるソースを手直した。変数の宣言がなかったり関数ごと欠損しているソースをデバッグするところからスタートして何となく動くようになったので投稿。初めテトリスを作ろうとしたけど、まともに動かない状態から抜け出せずに挫折… オセロは、テトリスの半分以下で300行程度。

    黒の手番です

    備忘:CSSに一番苦戦した。borderとborder-radiusが同時に指定できない。border-radiusでコマを描くとborderで描いた枠線が欠落してしまう。最終的にはコマの白黒画像を用意してbackground-imageで読み込むように修正した。


    ご参考:
    プロンプト:オセロゲームのサンプルをHTML/CSS/JavaScriptで作成してください。
    ChatGPT:はい、以下にHTML、CSS、JavaScriptで書かれたオセロゲームのサンプルを用意しました。
    ・・・

    <!DOCTYPE html>
    <html>
    
    <head>
    	<meta charset="UTF-8">
    	<title>オセロゲーム</title>
    	<style>
    		.container {
    			display: flex;
    			flex-wrap: wrap;
    			width: 320px;
    			height: 320px;
    			margin: 0 auto;
    		}
    
    		.cell {
    			width: 40px;
    			height: 40px;
    			background-color: green;
    			border: 1px solid black;
    			box-sizing: border-box;
    			cursor: pointer;
    		}
    
    		.cell.black {
    			background-color: black;
    		}
    
    		.cell.white {
    			background-color: white;
    		}
    
    		.turn {
    			margin-top: 10px;
    			text-align: center;
    			font-size: 20px;
    		}
    
    		.win {
    			margin-top: 10px;
    			text-align: center;
    			font-size: 30px;
    			font-weight: bold;
    		}
    	</style>
    </head>
    
    <body>
    	<div class="container"></div>
    	<div class="turn">黒の手番です</div>
    	<div class="win"></div>
    	<script>
    
    		const WHITE = 1;
    		const BLACK = -1;
    		const EMPTY = 0;
    
    		// ゲームボードのサイズ
    		const BOARD_SIZE = 8;
    
    		// ゲームボードの初期化
    		let board = [];
    		for (let i = 0; i < BOARD_SIZE; i++) {
    			board[i] = [];
    			for (let j = 0; j < BOARD_SIZE; j++) {
    				board[i][j] = 0;
    			}
    		}
    		board[3][3] = board[4][4] = 1;
    		board[3][4] = board[4][3] = -1;
    
    		// 現在のプレイヤー(1:黒, -1:白)
    		let currentPlayer = 1;
    
    		// 盤面を描画する関数
    		function drawBoard() {
    			const container = document.querySelector('.container');
    			container.innerHTML = '';
    			for (let i = 0; i < BOARD_SIZE; i++) {
    				for (let j = 0; j < BOARD_SIZE; j++) {
    					const cell = document.createElement('div');
    					cell.className = 'cell';
    					if (board[i][j] === 1) {
    						cell.classList.add('black');
    					} else if (board[i][j] === -1) {
    						cell.classList.add('white');
    					}
    					cell.dataset.row = i;
    					cell.dataset.col = j;
    					cell.addEventListener('click', handleClick);
    					container.appendChild(cell);
    				}
    			}
    		}
    
    		drawBoard();
    
    		// クリックイベントのハンドラー関数
    		function handleClick(event) {
    			const row = parseInt(event.target.dataset.row);
    			const col = parseInt(event.target.dataset.col);
    			if (board[row][col] !== 0) {
    				return;
    			}
    			const directions = [
    				{ x: 0, y: -1 }, // 上
    				{
    					x: 0, y
    
    1 }, // 下
    				{ x: -1, y: 0 }, // 左
    				{ x: 1, y: 0 }, // 右
    				{ x: -1, y: -1 }, // 左上
    				{ x: 1, y: -1 }, // 右上
    				{ x: -1, y: 1 }, // 左下
    				{ x: 1, y: 1 } // 右下
    			];
    			let canPlace = false;
    			for (let d = 0; d < directions.length; d++) {
    				const dx = directions[d].x;
    				const dy = directions[d].y;
    				let x = row + dx;
    				let y = col + dy;
    				let flipCount = 0;
    				while (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE) {
    					if (board[x][y] === -currentPlayer) {
    						flipCount++;
    						x += dx;
    						y += dy;
    					} else if (board[x][y] === currentPlayer) {
    						if (flipCount > 0) {
    							canPlace = true;
    						}
    						break;
    					} else {
    						break;
    					}
    				}
    				if (canPlace) {
    					break;
    				}
    			}
    			if (!canPlace) {
    				return;
    			}
    			board[row][col] = currentPlayer;
    			event.target.classList.add(currentPlayer === 1 ? 'black' : 'white');
    			event.target.removeEventListener('click', handleClick);
    			flipStones(row, col);
    			currentPlayer = -currentPlayer;
    			const turnEl = document.querySelector('.turn');
    			turnEl.textContent = ${ currentPlayer === 1 ? '黒' : '白' } の手番です;
    			if (!hasPlaceableCell()) {
    				currentPlayer = -currentPlayer;
    				if (!hasPlaceableCell()) {
    					const winEl = document.querySelector('.win');
    					const blackCount = countStones(1);
    					const whiteCount = countStones(-1);
    					if (blackCount > whiteCount) {
    						winEl.textContent = '黒の勝ち!';
    					} else if (blackCount < whiteCount) {
    						winEl.textContent = '白の勝ち!';
    					} else {
    						winEl.textContent = '引き分け!';
    					}
    				} else {
    					turnEl.textContent = ${ currentPlayer === 1 ? '黒' : '白' } はパスです;
    				}
    			}
    		}
    
    		// 石を返す関数
    		function flipStones(row, col) {
    			const directions = [
    				{ x: 0, y: -1 }, // 上
    				{ x: 0, y: 1 }, // 下
    				{ x: -1, y: 0 }, // 左
    				{ x: 1, y: 0 }, // 右
    				{ x: -1, y: -1 }, // 左上
    				{ x: 1, y: -1 }, // 右上
    				{ x: -1, y: 1 }, // 左下
    				{ x: 1, y: 1 } // 右下
    			];
    			for (let d = 0; d < directions.length; d++) {
    				const dx = directions[d].x;
    
    				const dy = directions[d].y;
    				let x = row + dx;
    				let y = col + dy;
    				let flipCount = 0;
    				while (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE) {
    					if (board[x][y] === -currentPlayer) {
    						flipCount++;
    						x += dx;
    						y += dy;
    					} else if (board[x][y] === currentPlayer) {
    						if (flipCount > 0) {
    							for (let i = 1; i <= flipCount; i++) {
    								const flipX = row + i * dx;
    								const flipY = col + i * dy;
    								board[flipX][flipY] = currentPlayer;
    								const flipEl = document.querySelector(`[data-row="${flipX}"][data-col="${flipY}"]`);
    								flipEl.classList.remove(currentPlayer === 1 ? 'white' : 'black');
    								flipEl.classList.add(currentPlayer === 1 ? 'black' : 'white');
    							}
    						}
    						break;
    					} else {
    						break;
    					}
    				}
    			}
    		}
    
    		// 置けるセルがあるかどうかを判定する関数
    		function hasPlaceableCell() {
    			for (let i = 0; i < BOARD_SIZE; i++) {
    				for (let j = 0; j < BOARD_SIZE; j++) {
    					if (board[i][j] === 0 && canPlaceStone(i, j)) {
    						return true;
    					}
    				}
    			}
    			return false;
    		}
    
    		// 石の数を数える関数
    		function countStones(player) {
    			let count = 0;
    			for (let i = 0; i < BOARD_SIZE; i++) {
    				for (let j = 0; j < BOARD_SIZE; j++) {
    					if (board[i][j] === player) {
    						count++;
    					}
    				}
    			}
    			return count;
    		}
    
    		// ゲームをリセットする関数
    		function resetGame() {
    			board = initializeBoard();
    			currentPlayer = 1;
    			const cells = document.querySelectorAll('.cell');
    			for (let i = 0; i < cells.length; i++) {
    				cells[i].classList.remove('black', 'white');
    				cells[i].addEventListener('click', handleClick);
    			}
    			const turnEl = document.querySelector('.turn');
    			turnEl.textContent = '黒の手番です';
    			const winEl = document.querySelector('.win');
    			winEl.textContent = '';
    		}
    
    		function canPlaceStone(row, col, color) {
    			if (board[row][col] !== EMPTY) {
    				return false;
    			}
    			for (let drow = -1; drow <= 1; drow++) {
    				for (let dcol = -1; dcol <= 1; dcol++) {
    					if (drow === 0 && dcol === 0) {
    						continue;
    					}
    					let r = row + drow;
    					let c = col + dcol;
    					let flipped = false;
    					while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
    						if (board[r][c] === EMPTY) {
    							break;
    						}
    						if (board[r][c] === color) {
    							if (flipped) {
    								return true;
    							} else {
    								break;
    							}
    						}
    						flipped = true;
    						r += drow;
    						c += dcol;
    					}
    				}
    			}
    			return false;
    		}
    
    		function initializeBoard() {
    			for (let row = 0; row < BOARD_SIZE; row++) {
    				for (let col = 0; col < BOARD_SIZE; col++) {
    					const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
    					cell.classList.remove('black', 'white');
    					if ((row === 3 && col === 3) || (row === 4 && col === 4)) {
    						cell.classList.add('white');
    						board[row][col] = WHITE;
    					} else if ((row === 3 && col === 4) || (row === 4 && col === 3)) {
    						cell.classList.add('black');
    						board[row][col] = BLACK;
    					} else {
    						board[row][col] = EMPTY;
    					}
    				}
    			}
    		}
    
    		// 初期化
    		let board = initializeBoard();
    		let currentPlayer = 1;
    		const cells = document.querySelectorAll('.cell');
    		for (let i = 0; i < cells.length; i++) {
    			cells[i].addEventListener('click', handleClick);
    		}
    		const resetBtn = document.querySelector('.reset');
    		resetBtn.addEventListener('click', resetGame);
    		const turnEl = document.querySelector('.turn');
    		turnEl.textContent = '黒の手番です';
    }) ();
    
    	</script>
    </body>
    
    </html>

    ※インデントなど整形済み