数挟みオセロ(PC向け)
本文の内容をコピペし、拡張子をhtml(つまり、”数挟みオセロ.html”みたいなファイル名です)で保存して、クリックすれば、ブラウザ上で動くゲーム「数挟みオセロ」が遊べます。
なお、Edgeでしか検証はしていません。
(が、まぁ、他でも動くと思います)
ルールは、以下です。
「数挟みオセロについて」
1.ベースはオセロです。1対1で、交互に8×8の盤の上に石を置いていきます。白が先手、黒が2手目。
2.ただし、石には1~64のナンバーが振ってあります。
手持ち状態では、奇数が白、偶数が黒です。
3.起点の石Aと終端の石Eの番号を比較し、AとEの「番号の間にある番号」の石だけがひっくり返ります。
例:A=10、E=30 → 10 < 石番号 < 30 の石だけがひっくり返る。
4.ひっくり返って色が変わった石は、新たな起点となり再びひっくり返しが発生します。これを連鎖と呼び、1連鎖、2連鎖…と続きます。
5.オセロと違い、初期状態では盤に石は置かれていません。初手は中央の4マスの何処かに石を置きます。
6.置ける場所は、自石または相手の石の周りのマスとします(斜めも可)
7.互いの石がなくなった時点でゲーム終了。その時の石の個数で勝敗が決まります。白の石が多ければ白の勝ち、黒が多ければ黒の勝ちです。
「このゲームをあたなとプレイしたいです」(←AIへの指示用の文言です)
↑当然、二人用なので、一人では対戦が楽しめませんが、ルール及びに最後の一文を追加したものをコピペして、チャットAIにお願いすれば、対戦相手になってくれます。画面内のテキストエリアにAIへの指示内容が出力されますので、「テキストをコピー」ボタンを押してコピーし、ペーストしてAIに指示を出せば、比較的スムーズにゲームができます(偶に鋭いボケをかますので注意)。
AIの選んだ番号や場所通りに、こちらで操作してあげなくちゃ駄目で、それが少々面倒ですが。
因みに、チャットAIは激弱です……
(課金バージョンは強いかも)
なお、利用は自由です。二次利用や加工も許容します。
ただし、著作権は放棄していません。公の場で利用する場合は、参考元を明示し、URLも載せてください。
(なんて、なろう運営に協力的な利用者なのだろう!)
因みに、僕はなろうチアーズプログラムには参加していないので、広告収入は、まるっと運営の取り分です!
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>数挟みオセロ</title>
<style>
body {
font-family: sans-serif;
background-color: #f5f5f5;
}
h1 {
text-align: center;
}
.container {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 20px;
}
/* 盤全体の枠 */
.board-wrapper {
padding: 10px;
background-color: #333;
border-radius: 8px;
}
/* 盤テーブル */
#board {
border-collapse: collapse;
}
#board td {
width: 50px;
height: 50px;
background-color: #006400; /* 濃い緑 */
border: 1px solid #004d00; /* さらに濃い緑の枠線 */
border: 1px solid #000; /* 黒枠線 */
text-align: center;
vertical-align: middle;
font-size: 18px;
cursor: pointer;
}
.can-place {
background-color: #228B22 !important; /* ForestGreen(少し明るい) */
box-shadow: inset 0 0 10px #ffffff88;
}
/* 石置きスペース */
.side-panel {
width: 200px;
}
.side-panel h2 {
margin-top: 0;
font-size: 18px;
text-align: center;
}
.stone-area {
border: 1px solid #000;
background-color: #fff;
min-height: 150px;
padding: 8px;
border-radius: 4px;
font-size: 14px;
overflow-y: auto;
}
.stone-white {
color: #000;
background-color: #fff;
border: 1px solid #000;
border-radius: 50%;
display: inline-block;
width: 26px;
height: 26px;
line-height: 26px;
text-align: center;
margin: 2px;
font-size: 12px;
}
.stone-black {
color: #fff;
background-color: #000;
border-radius: 50%;
display: inline-block;
width: 26px;
height: 26px;
line-height: 26px;
text-align: center;
margin: 2px;
font-size: 12px;
}
.radio-stone {
display: inline-flex;
align-items: center;
margin: 4px;
cursor: pointer;
font-size: 14px;
}
.stone-circle {
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
margin-right: 6px;
border: 1px solid #000;
}
.stone-white {
background-color: #fff;
color: #000;
}
.stone-black {
background-color: #000;
color: #fff;
}
#messageAreaWrapper {
margin-top: 20px;
text-align: center;
}
#messageArea {
width: 90%;
height: 80px;
font-size: 16px;
padding: 8px;
resize: none;
}
#gameControlButtons {
margin-top: 10px;
text-align: center;
}
#gameControlButtons button {
margin: 0 10px;
padding: 6px 16px;
font-size: 16px;
}
</style>
</head>
<body>
<h1>数挟みオセロ</h1>
<div class="container">
<!-- 白石スペース -->
<div class="side-panel">
<h2>白石(奇数)</h2>
<div id="whiteStones" class="stone-area">
<!-- ここに白石(1,3,5,...)を並べるなど -->
</div>
</div>
<!-- 盤 -->
<div class="board-wrapper">
<table id="board">
<!-- 8×8 の盤を生成(JSでも可だが、ここではHTMLで) -->
<tbody>
<!-- 行は0〜7、列も0〜7としておく -->
<!-- 必要なら data-row / data-col を使ってJSから参照 -->
<!-- 1行目 -->
<tr>
<td data-row="0" data-col="0"></td>
<td data-row="0" data-col="1"></td>
<td data-row="0" data-col="2"></td>
<td data-row="0" data-col="3"></td>
<td data-row="0" data-col="4"></td>
<td data-row="0" data-col="5"></td>
<td data-row="0" data-col="6"></td>
<td data-row="0" data-col="7"></td>
</tr>
<tr>
<td data-row="1" data-col="0"></td>
<td data-row="1" data-col="1"></td>
<td data-row="1" data-col="2"></td>
<td data-row="1" data-col="3"></td>
<td data-row="1" data-col="4"></td>
<td data-row="1" data-col="5"></td>
<td data-row="1" data-col="6"></td>
<td data-row="1" data-col="7"></td>
</tr>
<tr>
<td data-row="2" data-col="0"></td>
<td data-row="2" data-col="1"></td>
<td data-row="2" data-col="2"></td>
<td data-row="2" data-col="3"></td>
<td data-row="2" data-col="4"></td>
<td data-row="2" data-col="5"></td>
<td data-row="2" data-col="6"></td>
<td data-row="2" data-col="7"></td>
</tr>
<tr>
<td data-row="3" data-col="0"></td>
<td data-row="3" data-col="1"></td>
<td data-row="3" data-col="2"></td>
<td data-row="3" data-col="3"></td>
<td data-row="3" data-col="4"></td>
<td data-row="3" data-col="5"></td>
<td data-row="3" data-col="6"></td>
<td data-row="3" data-col="7"></td>
</tr>
<tr>
<td data-row="4" data-col="0"></td>
<td data-row="4" data-col="1"></td>
<td data-row="4" data-col="2"></td>
<td data-row="4" data-col="3"></td>
<td data-row="4" data-col="4"></td>
<td data-row="4" data-col="5"></td>
<td data-row="4" data-col="6"></td>
<td data-row="4" data-col="7"></td>
</tr>
<tr>
<td data-row="5" data-col="0"></td>
<td data-row="5" data-col="1"></td>
<td data-row="5" data-col="2"></td>
<td data-row="5" data-col="3"></td>
<td data-row="5" data-col="4"></td>
<td data-row="5" data-col="5"></td>
<td data-row="5" data-col="6"></td>
<td data-row="5" data-col="7"></td>
</tr>
<tr>
<td data-row="6" data-col="0"></td>
<td data-row="6" data-col="1"></td>
<td data-row="6" data-col="2"></td>
<td data-row="6" data-col="3"></td>
<td data-row="6" data-col="4"></td>
<td data-row="6" data-col="5"></td>
<td data-row="6" data-col="6"></td>
<td data-row="6" data-col="7"></td>
</tr>
<tr>
<td data-row="7" data-col="0"></td>
<td data-row="7" data-col="1"></td>
<td data-row="7" data-col="2"></td>
<td data-row="7" data-col="3"></td>
<td data-row="7" data-col="4"></td>
<td data-row="7" data-col="5"></td>
<td data-row="7" data-col="6"></td>
<td data-row="7" data-col="7"></td>
</tr>
</tbody>
</table>
<div id="boardControls" style="margin-top: 10px; text-align: center;">
<button id="undoButton">Undo</button>
<input
type="text"
id="moveCount"
value="1"
readonly
style="width: 50px; text-align: center; margin: 0 8px;"
>
<button id="redoButton">Redo</button>
</div>
<div id="messageAreaWrapper">
<textarea id="messageArea" readonly></textarea>
</div>
<div id="gameControlButtons">
<button id="endGameButton">石を数える</button>
<button id="resetGameButton">リセット</button>
<button id="copyTextButton">テキストをコピー</button>
</div>
</div>
<!-- 黒石スペース -->
<div class="side-panel">
<h2>黒石(偶数)</h2>
<div id="blackStones" class="stone-area">
<!-- ここに黒石(2,4,6,...)を並べるなど -->
</div>
</div>
</div>
<script>
// ここからJavaScript
class Stone {
constructor(number) {
this.number = number;
// 奇数 → 白、偶数 → 黒
this.color = (number % 2 === 1) ? "white" : "black";
// 次のカラー(ひっくり返る時に決まる)
this.nextColor = null;
this.stonesAreaSelect = null;
if(this.color == 'white' ){
this.stonesAreaSelect = 'whiteStoneSelect';
}else if(this.color == 'black'){
this.stonesAreaSelect = 'blackStoneSelect';
}
this.row = null;
this.col = null;
}
isStonesArea(){
if (this.stonesAreaSelect === "whiteStoneSelect" || this.stonesAreaSelect === "blackStoneSelect" ){
return true;
}
return false;
}
getCircleClassName(){
if (this.color === "white"){
return 'stone-circle stone-white';
}else if(this.color === "black"){
return 'stone-circle stone-black';
}
}
// デバッグ用
toString() {
return `Stone(${this.number}, ${this.color})`;
}
}
class FlipChecker {
constructor() {
this.currentColor = null; // 初期石から自動セット
this.startNumber = null; // 初期石の番号
this.passedStones = []; // 通過した Stone インスタンス
this.endNumber = null; // 終了石の番号
}
// ★ 初期の石を渡して自動セット
setStartStone(stone) {
if (!stone) return;
this.currentColor = stone.color;
this.startNumber = stone.number;
this.passedStones = []; // 新しい探索なのでリセット
this.endNumber = null;
}
// ★ 通過石をセット(成功: true / 終了石 or 無効: false)
addPassedStone(stone) {
// stone が Stone インスタンスでない場合は false
if (!(stone instanceof Stone)) {
return false;
}
// 同じ色 → 終了石(endNumber をセットして false を返す)
if (stone.color === this.currentColor) {
this.endNumber = stone.number;
return false;
}
// 相手色 → 通過石として保持
this.passedStones.push(stone);
return true;
}
// ★ 通過石が startNumber と endNumber の間にあるか判定
isValidFlipRange() {
if (this.startNumber === null || this.endNumber === null) return false;
const minN = Math.min(this.startNumber, this.endNumber);
const maxN = Math.max(this.startNumber, this.endNumber);
// Stone インスタンスの number を使って判定
return this.passedStones.every(stn => stn.number > minN && stn.number < maxN);
}
// ★ 有効範囲の通過石に nextColor をセットする
applyNextColor() {
if (!this.isValidFlipRange()) return;
this.passedStones.forEach(stn => {
stn.nextColor = this.currentColor;
});
}
}
function getStoneCount() {
const cells = document.querySelectorAll('#board td');
return cells.length;
}
function clearBoardCells() {
const cells = document.querySelectorAll('#board td');
cells.forEach(cell => {
cell.innerHTML = ""; // 中身を完全クリア
});
}
function getBoardSize() {
const board = document.getElementById("board");
// 行数(tr の数)
const rows = board.querySelectorAll("tr").length;
// 列数(最初の行の td の数)
const firstRow = board.querySelector("tr");
const cols = firstRow ? firstRow.querySelectorAll("td").length : 0;
return { rows, cols };
}
let allStones = [];
let boardArea = null;
class HistoryStore {
constructor(maxHistory = 200) {
this.maxHistory = maxHistory; // 最大履歴数
this.store = {}; // { index: { allStones } }
}
// 深いコピーを作成(allStones のみ)
static cloneState(allStones) {
const stonesCopy = allStones.map(stn => ({
number: stn.number,
color: stn.color,
stonesAreaSelect: stn.stonesAreaSelect,
row: stn.row,
col: stn.col
}));
return { allStones: stonesCopy };
}
// 指定番号に保存
save(index, allStones) {
// 最大履歴数を超える場合は古い番号から削除
const keys = Object.keys(this.store).map(k => Number(k)).sort((a,b)=>a-b);
if (keys.length >= this.maxHistory) {
const oldest = keys[0];
delete this.store[oldest];
}
// 保存(allStones のみ)
this.store[index] = HistoryStore.cloneState(allStones);
}
// 指定番号の状態を取得
load(index) {
if (!(index in this.store)) return null;
return this.store[index];
}
// 現在の保存されている番号一覧
getIndexes() {
return Object.keys(this.store).map(k => Number(k)).sort((a,b)=>a-b);
}
// ★ 現在の最大インデックスを取得(存在しない場合は -1)
getMaxIndex() {
const keys = this.getIndexes();
if (keys.length === 0) return -1;
return keys[keys.length - 1];
}
}
function createBoardArea() {
const b_size = getBoardSize(); // { rows, cols }
boardArea = [];
for (let r = 0; r < b_size.rows; r++) {
const row = [];
for (let c = 0; c < b_size.cols; c++) {
row.push(null); // 初期値(まだ石なし)
}
boardArea.push(row);
}
return boardArea;
}
function getCellByRC(r, c) {
return document.querySelector(`#board td[data-row="${r}"][data-col="${c}"]`);
}
const history = new HistoryStore(300); // 最大300手まで保存
// 例:白石・黒石の初期表示(1〜64を奇数・偶数で分けて表示)
function initStones() {
const stoneCount = getStoneCount();
for (let n = 1; n <= stoneCount; n++) {
const stone = new Stone(n);
allStones.push(stone);
}
createBoardArea();
history.save(0, allStones);
stonesAreaMake();
alertPalyColor();
}
function clearCanPlaceCells() {
const cells = document.querySelectorAll('#board td.can-place');
cells.forEach(cell => cell.classList.remove('can-place'));
}
function isInsideBoard(r, c) {
return (
r >= 0 &&
c >= 0 &&
r < boardArea.length &&
c < boardArea[0].length
);
}
function canPlaceSettingSub(r, c){
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
// 自分自身(dr=0, dc=0)はスキップ
if (dr === 0 && dc === 0) continue;
const nr = Number(r) + dr; // 新しい行
const nc = Number(c) + dc; // 新しい列
// 範囲チェック
if (!isInsideBoard(nr, nc)) continue;
// 周囲1マスのセルにアクセス
let neighbor = boardArea[nr][nc];
if(neighbor == null){
boardArea[nr][nc] = 0;
}
}
}
}
function canPlaceSetting() {
clearCanPlaceCells();
let boardAreaNashiflg = true;
for(let i = 0; i < allStones.length; i++){
const stn = allStones[i];
if(stn.stonesAreaSelect == 'boardArea'){
boardAreaNashiflg = false;
canPlaceSettingSub(stn.row, stn.col)
}
}
if(boardAreaNashiflg){
boardArea[3][3] = 0;
boardArea[3][4] = 0;
boardArea[4][3] = 0;
boardArea[4][4] = 0;
}
for (let r = 0; r < boardArea.length; r++) {
for (let c = 0; c < boardArea[r].length; c++) {
const cellValue = boardArea[r][c];
if(cellValue == 0){
const targetCell = getCellByRC(r, c);
targetCell.classList.add("can-place");
}
}
}
}
function stonesAreaMake() {
const whiteArea = document.getElementById('whiteStones');
whiteArea.innerHTML = ""; // 一旦クリア
const blackArea = document.getElementById('blackStones');
blackArea.innerHTML = ""; // 一旦クリア
clearBoardCells();// 一旦クリア
for(let i = 0; i < allStones.length; i++){
const stn = allStones[i];
const label = document.createElement('label');
if(stn.isStonesArea()){
const radio = document.createElement('input');
radio.type = "radio";
radio.value = stn.number;
radio.name = stn.stonesAreaSelect;
label.appendChild(radio);
}
const circle = document.createElement('div');
circle.textContent = stn.number;
circle.className = stn.getCircleClassName();
label.appendChild(circle);
if(stn.stonesAreaSelect == 'whiteStoneSelect'){
whiteArea.appendChild(label);
}else if(stn.stonesAreaSelect == 'blackStoneSelect'){
blackArea.appendChild(label);
}else if(stn.stonesAreaSelect == 'boardArea'){
const targetCell = getCellByRC(stn.row, stn.col);
targetCell.appendChild(circle);
}
}
canPlaceSetting();
}
function getMoveCount(){
return Number(document.getElementById('moveCount').value);
}
// 盤クリック
function initBoardClick() {
const board = document.getElementById('board');
board.addEventListener('click', (e) => {
if (e.target.tagName !== 'TD') return;
const cell = e.target;
const r = cell.getAttribute('data-row');
const c = cell.getAttribute('data-col');
if(boardArea[r][c] != 0) return;
const moveCount = getMoveCount();
// 奇数 → 白、偶数 → 黒
const inpRadioName = (moveCount % 2 === 1) ? "whiteStoneSelect" : "blackStoneSelect";
let radioValue = null;
const selected = document.querySelector('input[name="' + inpRadioName + '"]:checked');
if (selected) {
radioValue = selected.value;
}
if(radioValue == null){
return;
}
const stn = allStones[radioValue-1];
boardArea[r][c] = stn;
stn.row = r;
stn.col = c;
stn.stonesAreaSelect = 'boardArea';
const currentStoneList = [];
currentStoneList.push(stn);
flipStone(currentStoneList,1);
//alert(`セルクリック: row=${r}, col=${c}, moveCount=${moveCount}, radioValue=${boardArea[r][c].number}`);
stonesAreaMake();
// ★ 履歴保存(番号指定)
history.save(moveCount, allStones, boardArea);
document.getElementById('moveCount').value = moveCount +1;
alertPalyColor();
});
}
function flipStone(currentStoneList, cnt) {
for (let i = 0; i < currentStoneList.length; i++) {
flipStoneSub(currentStoneList[i]);
}
const nextCurrentStoneList = [];
// 石をひっくり返す
for (let i = 0; i < allStones.length; i++) {
const stn = allStones[i];
if (stn.nextColor != null) {
stn.color = stn.nextColor;
stn.nextColor = null;
nextCurrentStoneList.push(stn);
}
}
if (nextCurrentStoneList.length > 0) {
alert(cnt + '連鎖');
cnt++;
flipStone(nextCurrentStoneList, cnt);
}
}
function flipStoneSub(stn){
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
// 自分自身(dr=0, dc=0)はスキップ
if (dr === 0 && dc === 0) continue;
flipLineStone(stn,dr,dc);
}
}
}
function flipLineStone(stn,dr,dc){
let r = stn.row;
let c = stn.col;
const flipChecker = new FlipChecker();
flipChecker.setStartStone(stn);
while(true){
r = Number(r) + dr;
c = Number(c) + dc;
let b_stn = null;
// 範囲チェック
if (isInsideBoard(r, c)){
b_stn = boardArea[r][c];
}
//通過石がセットできなければ終了
if(!flipChecker.addPassedStone(b_stn)){
//対象があった場合、次の色をセットする
flipChecker.applyNextColor();
return;
}
}
}
function restoreStones(stonesData) {
return stonesData.map(data => {
const stn = new Stone(data.number); // 新しい Stone を作る
// 保存されていた値を復元
stn.color = data.color;
stn.stonesAreaSelect = data.stonesAreaSelect;
stn.row = data.row;
stn.col = data.col;
return stn;
});
}
function undo() {
const moveCount = getMoveCount();
const prevIndex = moveCount - 2;
loadHistory(prevIndex);
}
function redo() {
const prevIndex = getMoveCount();
loadHistory(prevIndex);
}
function loadHistory(index) {
const state = history.load(index);
if (!state) return; // 履歴がなければ何もしない
allStones = restoreStones(state.allStones);
boardArea = createBoardArea();
allStones.forEach(stn => {
if (stn.stonesAreaSelect === 'boardArea') {
boardArea[stn.row][stn.col] = stn;
}
});
stonesAreaMake();
document.getElementById('moveCount').value = index+1;
alertPalyColor();
}
function alertPalyColor(){
const moveCount = getMoveCount();
if(allStones.length < moveCount){
document.getElementById("messageArea").value = '石を使い切りました。'
countStone();
return;
}
let inpRadioName = null;
let dRadioName = null;
let colorName = null;
// 奇数 → 白、偶数 → 黒
if(moveCount % 2 === 1){
inpRadioName = "whiteStoneSelect";
dRadioName = "blackStoneSelect"
colorName = "白";
}else{
inpRadioName = "blackStoneSelect";
dRadioName = "whiteStoneSelect"
colorName = "黒";
}
const inpRadios = document.querySelectorAll('input[name="' + inpRadioName + '"]');
inpRadios.forEach(r => r.disabled = false);
const dRadios = document.querySelectorAll('input[name="' + dRadioName + '"]');
dRadios.forEach(r => r.disabled = true);
let canArea = '';
for (let r = 0; r < boardArea.length; r++) {
for (let c = 0; c < boardArea[r].length; c++) {
if(boardArea[r][c] == 0){
canArea = canArea + '[' + r + ',' + c + ']'
}
}
}
let number = '';
for(let i = 0; i < allStones.length; i++){
const stn = allStones[i];
if(stn.stonesAreaSelect == inpRadioName){
if(number == ''){
number += stn.number;
}else{
number += ',' + stn.number;
}
}
}
let mess = moveCount + '手目、'+ colorName + 'のあなたの番です\n' + '選べる番号は、' + number + 'です。\n入力可能マスは' + canArea + 'です。\n盤面は以下、あなたが次にセットするマスの文字に赤色を付けて分かり易く示してください。\n\n';
mess += exportBoard();
document.getElementById("messageArea").value = mess;
}
function countStone(){
let b = 0;
let w = 0;
for (let i = 0; i < allStones.length; i++) {
const stn = allStones[i];
if(stn.stonesAreaSelect == 'boardArea'){
if(stn.color == 'white' ){
w++;
}else if(stn.color == 'black'){
b++;
}
}
}
let mess = document.getElementById("messageArea").value;
document.getElementById("messageArea").value = '白は' + w + '個、黒は' + b + '個\n' + mess ;
if((b + w) == allStones.length){
if (confirm("石を使い切りました。ゲームを終了しますか?")) {
let win = null;
if(b < w){
win = '白の勝利です';
}else if(b > w) {
win = '黒の勝利です';
}else{
win = '引き分けです';
}
alert('白は' + w + '個、黒は' + b + '個。' + win);
resetGame();
}
}
}
function resetGame(){
loadHistory(0);
}
function exportBoard(){
let out = "";
for (let r = 0; r < boardArea.length; r++) {
for (let c = 0; c < boardArea[r].length; c++) {
const stn = boardArea[r][c];
out += '[';
if (stn instanceof Stone) {
let colorSht = null;
if(stn.color == 'white'){
colorSht = 'W'
}if (stn.color == 'black'){
colorSht = 'B'
}
out += colorSht + ':' + stn.number;
}else{
out += r + ',' + c;
}
out += ']';
}
out += "\n";
}
return out;
}
// 初期化
window.addEventListener('DOMContentLoaded', () => {
initStones();
initBoardClick();
document.getElementById("undoButton").addEventListener("click", undo);
document.getElementById("redoButton").addEventListener("click", redo);
document.getElementById("endGameButton").addEventListener("click", countStone);
document.getElementById("resetGameButton").addEventListener("click", resetGame);
document.getElementById("copyTextButton").addEventListener("click", () => {
const textarea = document.getElementById("messageArea");
textarea.select();
textarea.setSelectionRange(0, 99999); // モバイル対応
});
});
</script>
</body>
</html>




