DIGITAL REEL GAME
MOJI-SLOT
SCORE0
CHANCE
MOJI-SLOT CONTROL
システム設定
プリセットを選ぶか、各項目を自由にカスタマイズできます。
これを作るきっかけ…
下記ポストのちんすこうマシーンがきっかけで作ってみました。
今日の記事の「ちんすこうの綴りの危うさを可視化するマシーン」の動きだけでもぜひご覧下さい。 https://t.co/F7ecrIXjqX— 沖縄B級ポータルサイト DEEokinawa (@dee_okinawa) September 30, 2026
概要
好きな文字でスロットができる暇つぶし・お遊び用のWEBゲームです。
外部ライブラリ、画像、音源、サーバーAPIは使用しません。HTML/CSSで画面を構成し、JavaScriptで抽選、リール制御、設定保存、役判定、演出を行います。
操作
- BETボタンまたはレバーを押してBET
- もう一度BETボタンまたはレバーを押すと回転
- 各ストップボタンでリールを停止
- 全リール停止後、中央ラインの文字列で役を判定
- 役成立時は固定スコアを加算し、ランプ、効果音、メッセージ、紙吹雪を表示
停止済みのボタンを再度押した場合は、未停止リールを左から停止します。
プリセット
プロセット:ちんすこう
- タイトル: MOJI-SLOT
- 文字: ち / ん / す / こ / う
- リール数: 5
- デザイン: 沖縄ゴールド
- 役: ちんすこう +500 / ちんちん +200 / ちんこ +100 / うんこ +50 / ちん +20
プロセット:LUCKY SEVEN
- タイトル: LUCKY SEVEN
- 記号: チェリー / バナナ / ベル / 星 / 7
- リール数: 3
- デザイン: クラシックレッド
- 役: 7揃い +1000 / チェリー +200 / ベル +100 / バナナ +50 / 星 +25
システム設定
- デザイン3種類
- 01 文字と表示
- リール文字・記号3〜7文字
- リール数3〜7
- ゲーム設定1〜6
- 02 役の設定
- 5種類の役の有効状態、役名、柄
- 03 画面とサウンド
- リール1コマの高さ
- 効果音の音量
- 04 抽選とモーション
- チャンスランプ確率
- 役の引き込み確率
- 最小回転速度
- 最大回転速度
- 減速時間
- 最大スベリコマ
大まかな仕様
- 役判定
成立可能な有効役をスコアの高い順に評価。
停止結果に役の柄が含まれる場合、最もスコアの高い役を1件だけ採用します。スコアと演出レベルはプリセット内の固定値です。 - 対応画面
- スロット本体最大幅: 760px
- 設定画面最大幅: 920px
- 650px以下: 設定画面を1カラム化
- 520px以下: BET、レバー、リール文字を縮小
- 380px以下: 7リール表示向けに操作部を追加縮小
- 動作環境
JavaScript、localStorage、requestAnimationFrame、Web Audio APIが有効なモダンブラウザを対象。Intl.Segmenter非対応環境ではUnicodeコードポイント単位の処理へフォールバックします。]
ソース
ソース:HTML
<!-- MOJI-SLOT -->
<div class="wpslot-app" id="wpslot-app" data-theme="okinawa" aria-busy="false">
<div class="wpslot-cabinet" aria-label="文字スロット">
<header class="wpslot-top-panel">
<div>
<div class="wpslot-eyebrow">DIGITAL REEL GAME</div>
<div class="wpslot-logo" id="wpslot-logo">MOJI-SLOT</div>
</div>
<div class="wpslot-displays">
<div class="wpslot-score-display">
<span>SCORE</span><strong id="wpslot-score">0</strong>
</div>
<div id="wpslot-lamp" class="wpslot-lamp" aria-live="polite">
CHANCE
</div>
</div>
</header>
<div class="wpslot-paytable" id="wpslot-paytable" aria-label="有効な役"></div>
<div class="wpslot-stage">
<div class="wpslot-side-control">
<button type="button" id="wpslot-bet-btn" class="wpslot-round-bet" aria-label="1ベット">
<span class="wpslot-bet-num">1</span><span class="wpslot-bet-txt">BET</span>
</button>
</div>
<div class="wpslot-center-area">
<div class="wpslot-window-frame">
<div class="wpslot-payline" aria-hidden="true"></div>
<div class="wpslot-reels" id="wpslot-reels"></div>
<div class="wpslot-glass" aria-hidden="true"></div>
</div>
<div class="wpslot-stop-buttons" id="wpslot-stop-buttons"></div>
</div>
<div class="wpslot-side-control">
<button type="button" class="wpslot-lever-box" id="wpslot-lever" aria-label="スタートレバー">
<span class="wpslot-lever-shaft"></span><span class="wpslot-lever-ball"></span>
</button>
</div>
</div>
<div class="wpslot-status-bar" id="wpslot-msg" role="status" aria-live="polite">
BETボタン または レバー を押してください
</div>
<div class="wpslot-footer-row">
<span id="wpslot-setting-label">設定 6</span>
<button type="button" id="wpslot-score-reset" class="wpslot-text-button">
スコアを0に戻す
</button>
</div>
<div class="wpslot-fx" id="wpslot-fx" aria-hidden="true"></div>
</div>
</div>
<section class="wpsys-panel" id="wpslot-system-settings" aria-labelledby="wpsys-title">
<header class="wpsys-header">
<div>
<p class="wpsys-kicker">MOJI-SLOT CONTROL</p>
<div class="wpsys-title" id="wpsys-title" role="heading" aria-level="2">
システム設定
</div>
<p>プリセットを選ぶか、各項目を自由にカスタマイズできます。</p>
</div>
<span class="wpsys-save-state" id="wpsys-save-state" role="status">未保存</span>
</header>
<div class="wpsys-presets" aria-label="プリセット">
<button type="button" class="wpsys-preset" data-preset="chinsuko">
<span class="wpsys-preset-icon">ち</span><span><b>ちんすこう</b><small>現行ベース・5リール</small></span>
</button>
<button type="button" class="wpsys-preset" data-preset="classic">
<span class="wpsys-preset-icon" data-symbol="cherry" aria-hidden="true"></span><span><b>LUCKY SEVEN</b><small>絵文字・3リール</small></span>
</button>
<button type="button" class="wpsys-preset" data-preset="custom">
<span class="wpsys-preset-icon" data-symbol="gear" aria-hidden="true"></span><span><b>カスタム</b><small>現在の値を編集</small></span>
</button>
</div>
<div class="wpsys-grid">
<fieldset class="wpsys-card wpsys-card-wide">
<legend><span>01</span> 文字と表示</legend>
<div class="wpsys-form-grid">
<label class="wpsys-field"><span>タイトル</span><input id="wpsys-machine-title" type="text" maxlength="24" value="MOJI-SLOT"></label>
<label class="wpsys-field"><span>デザイン</span><select id="wpsys-theme">
<option value="okinawa">沖縄ゴールド</option>
<option value="classic">クラシックレッド</option>
<option value="neon">ネオンパープル</option>
</select></label>
</div>
<div class="wpsys-symbol-head">
<span>リールの文字・記号</span><output id="wpsys-symbol-count">5 / 7</output>
</div>
<p class="wpsys-help">
左から1文字ずつ入力。絵文字も1文字として扱います。文字列の貼り付けで一括入力もできます。
</p>
<div class="wpsys-char-grid" id="wpsys-char-grid" aria-label="リール記号 最大7文字"></div>
<div class="wpsys-form-grid wpsys-top-gap">
<label class="wpsys-field"><span>リール数 <small>3〜7</small></span><input id="wpsys-reel-count" type="number" min="3" max="7" step="1"></label>
<label class="wpsys-field"><span>ゲーム設定(当たりやすさ) <small>1〜6</small></span><select id="wpsys-slot-setting">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
</select></label>
</div>
</fieldset>
<fieldset class="wpsys-card wpsys-card-wide">
<legend><span>02</span> 役の設定</legend>
<p class="wpsys-help">
停止結果に「柄」が含まれると成立。スコアは役ごとの固定値です。
</p>
<div class="wpsys-role-table" id="wpsys-role-table"></div>
</fieldset>
<fieldset class="wpsys-card">
<legend><span>03</span> 画面とサウンド</legend>
<div class="wpsys-range-list">
<label><span>リール1コマの高さ <output id="wpsys-row-out"></output></span><input id="wpsys-row" type="range" min="52" max="78" step="1"></label>
<label><span>効果音の音量 <output id="wpsys-volume-out"></output></span><input id="wpsys-volume" type="range" min="0" max="30" step="1"></label>
</div>
<div class="wpsys-note">
<b>すべてブラウザ内で動作</b><br>Web Audio
APIで効果音を生成するため、音源ファイルは不要です。
</div>
</fieldset>
<fieldset class="wpsys-card">
<legend><span>04</span> 抽選とモーション</legend>
<div class="wpsys-range-list">
<label><span>チャンスランプ確率 <output id="wpsys-chance-out"></output></span><input id="wpsys-chance" type="range" min="0" max="100" step="1"></label>
<label><span>役の引き込み確率 <output id="wpsys-force-out"></output></span><input id="wpsys-force" type="range" min="0" max="100" step="1"></label>
<label><span>最小回転速度 <output id="wpsys-speed-min-out"></output></span><input id="wpsys-speed-min" type="range" min="12" max="45" step="1"></label>
<label><span>最大回転速度 <output id="wpsys-speed-max-out"></output></span><input id="wpsys-speed-max" type="range" min="18" max="60" step="1"></label>
<label><span>減速時間 <output id="wpsys-stop-out"></output></span><input id="wpsys-stop" type="range" min="260" max="1600" step="20"></label>
<label><span>最大スベリコマ <output id="wpsys-slip-out"></output></span><input id="wpsys-slip" type="range" min="0" max="8" step="1"></label>
</div>
</fieldset>
</div>
<footer class="wpsys-actions">
<button type="button" class="wpsys-secondary" id="wpsys-reload">
保存済みの値に戻す
</button>
<button type="button" class="wpsys-primary" id="wpsys-apply">
設定を反映
</button>
</footer>
</section>ソース:CSS
/* MOJI-SLOT */
.wpslot-app {
--accent: #f2c94c;
--accent-2: #ff7a45;
--cabinet: #171a21;
--panel: #0d1118;
--reel: #fffaf0;
--ink: #17202a;
--glow: rgba(242, 201, 76, 0.42);
--row-h: 64px;
box-sizing: border-box !important;
width: 100% !important;
max-width: 760px !important;
margin: 20px auto !important;
padding: 8px !important;
color: #fff !important;
font-family:
Inter,
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans JP",
sans-serif !important;
-webkit-tap-highlight-color: transparent !important;
touch-action: manipulation !important;
}
.wpslot-app[data-theme="classic"] {
--accent: #ffd54a;
--accent-2: #ff315d;
--cabinet: #17141f;
--panel: #09070e;
--reel: #fff;
--ink: #141414;
--glow: rgba(255, 49, 93, 0.38);
}
.wpslot-app[data-theme="neon"] {
--accent: #68f9d2;
--accent-2: #a878ff;
--cabinet: #111326;
--panel: #08091b;
--reel: #f7f5ff;
--ink: #1d1734;
--glow: rgba(104, 249, 210, 0.4);
}
.wpslot-app,
.wpslot-app *,
.wpslot-app *::before,
.wpslot-app *::after {
box-sizing: border-box !important;
}
.wpslot-cabinet {
position: relative !important;
overflow: hidden !important;
padding: clamp(12px, 3vw, 24px) !important;
border: 1px solid color-mix(in srgb, var(--accent) 62%, #fff 8%) !important;
border-radius: 24px !important;
background:
radial-gradient(
circle at 50% -20%,
color-mix(in srgb, var(--accent) 16%, transparent),
transparent 48%
),
linear-gradient(145deg, #292e39, var(--cabinet) 58%, #0b0d12) !important;
box-shadow:
0 24px 70px rgba(0, 0, 0, 0.38),
inset 0 1px rgba(255, 255, 255, 0.1),
inset 0 0 35px rgba(0, 0, 0, 0.35) !important;
}
.wpslot-top-panel {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 12px !important;
padding: 12px 14px !important;
margin-bottom: 12px !important;
border: 1px solid rgba(255, 255, 255, 0.09) !important;
border-radius: 14px !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), transparent),
var(--panel) !important;
}
.wpslot-eyebrow {
color: rgba(255, 255, 255, 0.45) !important;
font-size: 9px !important;
font-weight: 800 !important;
letter-spacing: 0.18em !important;
}
.wpslot-logo {
color: var(--accent) !important;
font-size: clamp(17px, 4vw, 25px) !important;
font-weight: 950 !important;
letter-spacing: 0.09em !important;
text-shadow: 0 0 18px var(--glow) !important;
}
.wpslot-displays {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.wpslot-score-display {
min-width: 82px !important;
padding: 5px 9px !important;
text-align: right !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 9px !important;
background: #07090d !important;
box-shadow: inset 0 0 10px #000 !important;
}
.wpslot-score-display span {
display: block !important;
color: rgba(255, 255, 255, 0.42) !important;
font-size: 8px !important;
font-weight: 800 !important;
letter-spacing: 0.15em !important;
}
.wpslot-score-display strong {
color: #8cff9b !important;
font:
800 18px/1.1 ui-monospace,
SFMono-Regular,
Consolas,
monospace !important;
text-shadow: 0 0 8px rgba(140, 255, 155, 0.55) !important;
}
.wpslot-lamp {
padding: 7px 11px !important;
border: 1px solid #2b2f38 !important;
border-radius: 999px !important;
color: #3d2028 !important;
background: #09090b !important;
font-size: 10px !important;
font-weight: 950 !important;
letter-spacing: 0.08em !important;
}
.wpslot-lamp.gogo {
color: #ff315d !important;
border-color: #ff315d !important;
background: #fff0f4 !important;
box-shadow:
0 0 22px rgba(255, 49, 93, 0.8),
inset 0 0 9px rgba(255, 49, 93, 0.28) !important;
animation: wpslot-pulse 0.55s ease-in-out infinite alternate !important;
}
.wpslot-paytable {
display: flex !important;
gap: 6px !important;
min-height: 27px !important;
margin: 0 2px 12px !important;
overflow-x: auto !important;
scrollbar-width: thin !important;
}
.wpslot-role-chip {
flex: 0 0 auto !important;
padding: 5px 9px !important;
border: 1px solid rgba(255, 255, 255, 0.09) !important;
border-radius: 999px !important;
background: rgba(0, 0, 0, 0.2) !important;
color: rgba(255, 255, 255, 0.67) !important;
font-size: 10px !important;
white-space: nowrap !important;
}
.wpslot-role-chip b {
color: var(--accent) !important;
}
.wpslot-stage {
display: grid !important;
grid-template-columns: 52px minmax(0, 1fr) 52px !important;
align-items: center !important;
gap: clamp(6px, 2vw, 14px) !important;
}
.wpslot-side-control {
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.wpslot-round-bet {
width: 50px !important;
height: 50px !important;
border: 1px solid #8f6d00 !important;
border-radius: 50% !important;
color: #362700 !important;
background: radial-gradient(
circle at 35% 28%,
#fff4a7,
var(--accent) 42%,
#9e7500 100%
) !important;
box-shadow:
0 5px 0 #584000,
0 9px 15px rgba(0, 0, 0, 0.35),
inset 0 2px 3px rgba(255, 255, 255, 0.55) !important;
cursor: pointer !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
transition:
transform 0.12s,
box-shadow 0.12s !important;
}
.wpslot-round-bet:active,
.wpslot-round-bet.inserted {
transform: translateY(4px) !important;
box-shadow:
0 1px 0 #584000,
0 0 20px var(--glow) !important;
}
.wpslot-bet-num {
font-size: 15px !important;
font-weight: 950 !important;
line-height: 1 !important;
}
.wpslot-bet-txt {
font-size: 9px !important;
font-weight: 950 !important;
line-height: 1.1 !important;
}
.wpslot-center-area {
min-width: 0 !important;
display: flex !important;
flex-direction: column !important;
gap: 12px !important;
}
.wpslot-window-frame {
position: relative !important;
overflow: hidden !important;
padding: 6px !important;
border: 1px solid rgba(255, 255, 255, 0.18) !important;
border-radius: 13px !important;
background: #050608 !important;
box-shadow:
inset 0 0 20px #000,
0 8px 24px rgba(0, 0, 0, 0.25) !important;
}
.wpslot-reels {
display: flex !important;
gap: 5px !important;
height: calc(var(--row-h) * 3) !important;
}
.wpslot-reel-col {
position: relative !important;
flex: 1 1 0 !important;
min-width: 0 !important;
overflow: hidden !important;
border-radius: 7px !important;
background: var(--reel) !important;
box-shadow:
inset 0 22px 18px rgba(0, 0, 0, 0.25),
inset 0 -22px 18px rgba(0, 0, 0, 0.25) !important;
}
.wpslot-reel-strip {
position: absolute !important;
inset: 0 0 auto !important;
width: 100% !important;
will-change: transform !important;
}
.wpslot-symbol {
height: var(--row-h) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
color: var(--ink) !important;
font-size: clamp(20px, 5.8vw, 39px) !important;
font-weight: 950 !important;
line-height: 1 !important;
text-shadow: 0 1px rgba(255, 255, 255, 0.7) !important;
backface-visibility: hidden !important;
}
.wpslot-payline {
position: absolute !important;
z-index: 5 !important;
top: 50% !important;
left: 4px !important;
right: 4px !important;
height: 2px !important;
pointer-events: none !important;
background: linear-gradient(
90deg,
transparent,
var(--accent-2) 8%,
var(--accent-2) 92%,
transparent
) !important;
box-shadow: 0 0 8px var(--accent-2) !important;
}
.wpslot-payline::before,
.wpslot-payline::after {
content: "" !important;
position: absolute !important;
top: -5px !important;
width: 0 !important;
height: 0 !important;
border-top: 6px solid transparent !important;
border-bottom: 6px solid transparent !important;
}
.wpslot-payline::before {
left: 0 !important;
border-left: 8px solid var(--accent-2) !important;
}
.wpslot-payline::after {
right: 0 !important;
border-right: 8px solid var(--accent-2) !important;
}
.wpslot-glass {
position: absolute !important;
z-index: 4 !important;
inset: 0 !important;
pointer-events: none !important;
background: linear-gradient(
115deg,
rgba(255, 255, 255, 0.12),
transparent 24%,
transparent 70%,
rgba(255, 255, 255, 0.04)
) !important;
}
.wpslot-stop-buttons {
display: flex !important;
gap: 5px !important;
padding: 0 6px !important;
}
.wpslot-btn-col {
flex: 1 1 0 !important;
min-width: 0 !important;
display: flex !important;
justify-content: center !important;
}
.wpslot-round-stop {
width: clamp(32px, 7vw, 46px) !important;
height: clamp(32px, 7vw, 46px) !important;
border: 2px solid rgba(255, 255, 255, 0.9) !important;
border-radius: 50% !important;
background: radial-gradient(
circle at 35% 27%,
#ff9b9b,
#f02c42 43%,
#7c0718
) !important;
box-shadow:
0 4px 0 #47020d,
0 7px 12px rgba(0, 0, 0, 0.3) !important;
cursor: pointer !important;
transition:
transform 0.1s,
filter 0.2s,
opacity 0.2s !important;
}
.wpslot-round-stop:active:not(.all-locked) {
transform: translateY(3px) !important;
}
.wpslot-round-stop.is-stopped {
filter: saturate(0.25) brightness(0.6) !important;
transform: translateY(3px) !important;
}
.wpslot-round-stop.all-locked {
opacity: 0.38 !important;
cursor: not-allowed !important;
}
.wpslot-lever-box {
position: relative !important;
width: 44px !important;
height: 126px !important;
padding: 0 !important;
border: 1px solid #3d424c !important;
border-radius: 24px !important;
background: #090a0d !important;
box-shadow:
inset 0 0 10px #000,
0 3px 10px rgba(0, 0, 0, 0.3) !important;
cursor: pointer !important;
}
.wpslot-lever-shaft {
position: absolute !important;
top: 17px !important;
left: 50% !important;
width: 8px !important;
height: 47px !important;
transform: translateX(-50%) !important;
border-radius: 5px !important;
background: linear-gradient(
90deg,
#353941,
#e8ebef 40%,
#fff 55%,
#555a64
) !important;
transition: top 0.2s cubic-bezier(0.2, 0.8, 0.3, 1) !important;
}
.wpslot-lever-ball {
position: absolute !important;
top: 5px !important;
left: 50% !important;
width: 36px !important;
height: 36px !important;
transform: translateX(-50%) !important;
border: 1px solid #4c0010 !important;
border-radius: 50% !important;
background: radial-gradient(
circle at 34% 28%,
#ff9caa,
#ed294b 45%,
#7b0018
) !important;
box-shadow:
0 4px 8px #000,
inset 0 2px 4px rgba(255, 255, 255, 0.4) !important;
transition: top 0.2s cubic-bezier(0.2, 0.8, 0.3, 1) !important;
}
.wpslot-lever-box.pulled .wpslot-lever-shaft,
.wpslot-lever-box:active .wpslot-lever-shaft {
top: 63px !important;
}
.wpslot-lever-box.pulled .wpslot-lever-ball,
.wpslot-lever-box:active .wpslot-lever-ball {
top: 86px !important;
}
.wpslot-status-bar {
min-height: 42px !important;
margin-top: 15px !important;
padding: 11px 14px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border: 1px solid rgba(255, 255, 255, 0.07) !important;
border-radius: 11px !important;
background: rgba(0, 0, 0, 0.22) !important;
color: var(--accent) !important;
text-align: center !important;
font-size: 13px !important;
font-weight: 750 !important;
}
.wpslot-status-bar.win {
color: #fff !important;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--accent-2) 34%, #111),
color-mix(in srgb, var(--accent) 24%, #111)
) !important;
animation: wpslot-winbar 0.45s ease-in-out 3 alternate !important;
}
.wpslot-footer-row {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
margin-top: 8px !important;
padding: 0 4px !important;
color: rgba(255, 255, 255, 0.38) !important;
font-size: 10px !important;
}
.wpslot-text-button {
padding: 4px !important;
border: 0 !important;
color: rgba(255, 255, 255, 0.46) !important;
background: transparent !important;
font: inherit !important;
text-decoration: underline !important;
cursor: pointer !important;
}
.wpslot-fx {
position: absolute !important;
z-index: 20 !important;
inset: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
.wpslot-confetti {
position: absolute !important;
top: -20px !important;
width: 8px !important;
height: 14px !important;
border-radius: 2px !important;
animation: wpslot-fall var(--fall, 1.4s) cubic-bezier(0.18, 0.75, 0.35, 1)
forwards !important;
}
@keyframes wpslot-pulse {
to {
transform: scale(1.035);
filter: brightness(1.12);
}
}
@keyframes wpslot-winbar {
to {
filter: brightness(1.45);
}
}
@keyframes wpslot-fall {
to {
transform: translate3d(var(--drift, 0), 620px, 0) rotate(720deg);
opacity: 0.15;
}
}
@media (max-width: 520px) {
.wpslot-app {
padding: 2px !important;
}
.wpslot-cabinet {
border-radius: 17px !important;
padding: 11px !important;
}
.wpslot-stage {
grid-template-columns: 40px minmax(0, 1fr) 40px !important;
gap: 5px !important;
}
.wpslot-round-bet {
width: 39px !important;
height: 39px !important;
}
.wpslot-lever-box {
width: 36px !important;
height: 108px !important;
}
.wpslot-lever-ball {
width: 31px !important;
height: 31px !important;
}
.wpslot-lever-box.pulled .wpslot-lever-ball,
.wpslot-lever-box:active .wpslot-lever-ball {
top: 73px !important;
}
.wpslot-lever-box.pulled .wpslot-lever-shaft,
.wpslot-lever-box:active .wpslot-lever-shaft {
top: 54px !important;
}
.wpslot-score-display {
min-width: 65px !important;
}
.wpslot-lamp {
display: none !important;
}
.wpslot-reels {
gap: 3px !important;
}
.wpslot-stop-buttons {
gap: 3px !important;
}
.wpslot-symbol {
font-size: clamp(17px, 7.2vw, 31px) !important;
}
}
@media (max-width: 380px) {
.wpslot-cabinet {
padding: 9px !important;
}
.wpslot-stage {
grid-template-columns: 34px minmax(0, 1fr) 34px !important;
gap: 4px !important;
}
.wpslot-round-bet {
width: 34px !important;
height: 34px !important;
}
.wpslot-lever-box {
width: 32px !important;
}
.wpslot-round-stop {
width: clamp(24px, 7vw, 32px) !important;
height: clamp(24px, 7vw, 32px) !important;
}
.wpslot-top-panel {
padding: 9px !important;
}
.wpslot-score-display {
min-width: 60px !important;
}
.wpslot-role-chip {
padding: 4px 7px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.wpslot-app * {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}
.wpsys-panel {
--sys-accent: #f2c94c;
--sys-accent-2: #ff7a45;
--sys-card: #171e29;
--sys-field: #0d131c;
--sys-border: #2b3544;
--sys-text: #f5f7fa;
--sys-muted: #9ba7b8;
box-sizing: border-box !important;
isolation: isolate !important;
width: 100% !important;
max-width: 920px !important;
margin: 24px auto !important;
padding: clamp(16px, 3vw, 28px) !important;
border: 1px solid #303a49 !important;
border-radius: 24px !important;
background:
radial-gradient(
circle at 15% -10%,
rgba(242, 201, 76, 0.13),
transparent 30%
),
linear-gradient(145deg, #171d28, #10141c 65%) !important;
color: var(--sys-text) !important;
color-scheme: dark !important;
box-shadow: 0 22px 64px rgba(4, 8, 14, 0.35) !important;
font-family:
Inter,
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans JP",
sans-serif !important;
}
.wpsys-panel,
.wpsys-panel *,
.wpsys-panel *::before,
.wpsys-panel *::after {
box-sizing: border-box !important;
}
.wpsys-panel button,
.wpsys-panel input,
.wpsys-panel select {
font: inherit !important;
}
.wpsys-panel fieldset {
min-width: 0 !important;
}
.wpsys-header {
display: flex !important;
align-items: flex-start !important;
justify-content: space-between !important;
gap: 18px !important;
margin: 0 0 20px !important;
padding: 2px 2px 18px !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
}
.wpsys-kicker {
margin: 0 0 5px !important;
color: var(--sys-accent) !important;
font-size: 10px !important;
font-weight: 850 !important;
letter-spacing: 0.18em !important;
}
.wpsys-title {
display: block !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: none !important;
color: #fff !important;
font-size: clamp(23px, 4vw, 32px) !important;
font-weight: 900 !important;
line-height: 1.18 !important;
box-shadow: none !important;
}
.wpsys-header p:not(.wpsys-kicker) {
margin: 8px 0 0 !important;
color: var(--sys-muted) !important;
font-size: 13px !important;
line-height: 1.65 !important;
}
.wpsys-save-state {
flex: 0 0 auto !important;
max-width: min(100%, 360px) !important;
padding: 7px 11px !important;
border: 1px solid #4a3b19 !important;
border-radius: 999px !important;
background: #292316 !important;
color: #f8d86a !important;
font-size: 11px !important;
font-weight: 800 !important;
}
.wpsys-save-state.saved {
border-color: #245c43 !important;
background: #142d23 !important;
color: #7ee2aa !important;
}
.wpsys-save-state.error {
border-color: #71323a !important;
background: #351a20 !important;
color: #ff9aa8 !important;
}
.wpsys-presets {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 10px !important;
margin: 0 0 16px !important;
}
.wpsys-preset {
display: flex !important;
align-items: center !important;
gap: 10px !important;
min-width: 0 !important;
padding: 12px !important;
border: 1px solid var(--sys-border) !important;
border-radius: 14px !important;
background: #151c27 !important;
color: var(--sys-text) !important;
text-align: left !important;
box-shadow: none !important;
cursor: pointer !important;
transition:
transform 0.16s,
border-color 0.16s,
background 0.16s,
box-shadow 0.16s !important;
}
.wpsys-preset:hover {
transform: translateY(-1px) !important;
border-color: #9c8540 !important;
background: #1a2330 !important;
}
.wpsys-preset.active,
.wpsys-preset[aria-pressed="true"] {
border-color: var(--sys-accent) !important;
background: linear-gradient(145deg, #25251e, #191f29) !important;
box-shadow: 0 0 0 3px rgba(242, 201, 76, 0.12) !important;
}
.wpsys-preset-icon {
flex: 0 0 40px !important;
width: 40px !important;
height: 40px !important;
display: grid !important;
place-items: center !important;
border: 1px solid rgba(242, 201, 76, 0.18) !important;
border-radius: 11px !important;
background: #24271f !important;
color: var(--sys-accent) !important;
font-size: 21px !important;
font-weight: 900 !important;
}
.wpsys-preset b,
.wpsys-preset small {
display: block !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.wpsys-preset b {
color: #fff !important;
font-size: 13px !important;
}
.wpsys-preset small {
margin-top: 3px !important;
color: var(--sys-muted) !important;
font-size: 10px !important;
}
.wpsys-grid {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 14px !important;
}
.wpsys-card {
min-width: 0 !important;
margin: 0 !important;
padding: 18px !important;
border: 1px solid var(--sys-border) !important;
border-radius: 17px !important;
background: linear-gradient(155deg, #192230, var(--sys-card)) !important;
color: var(--sys-text) !important;
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035) !important;
}
.wpsys-card-wide {
grid-column: 1 / -1 !important;
}
.wpsys-card legend {
float: none !important;
width: auto !important;
margin: 0 !important;
padding: 0 8px !important;
border: 0 !important;
background: transparent !important;
color: #fff !important;
font-size: 13px !important;
font-weight: 850 !important;
line-height: 1.3 !important;
}
.wpsys-card legend span {
margin-right: 4px !important;
color: var(--sys-accent) !important;
font-size: 10px !important;
}
.wpsys-form-grid {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 12px !important;
}
.wpsys-field > span,
.wpsys-symbol-head,
.wpsys-range-list label > span {
color: #c1cad6 !important;
font-size: 11px !important;
font-weight: 750 !important;
}
.wpsys-field > span {
display: block !important;
margin: 0 0 6px !important;
}
.wpsys-field small {
color: #7f8b9d !important;
font-weight: 500 !important;
}
.wpsys-field input,
.wpsys-field select,
.wpsys-role-row input[type="text"] {
width: 100% !important;
margin: 0 !important;
border: 1px solid #344154 !important;
border-radius: 10px !important;
background: var(--sys-field) !important;
color: var(--sys-text) !important;
box-shadow: none !important;
outline: none !important;
}
.wpsys-field input,
.wpsys-field select {
height: 42px !important;
padding: 0 11px !important;
}
.wpsys-field input:focus,
.wpsys-field select:focus,
.wpsys-role-row input:focus {
border-color: var(--sys-accent) !important;
box-shadow: 0 0 0 3px rgba(242, 201, 76, 0.13) !important;
}
.wpsys-top-gap {
margin-top: 15px !important;
}
.wpsys-symbol-head {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
margin: 15px 0 0 !important;
}
.wpsys-symbol-head output {
padding: 3px 8px !important;
border-radius: 7px !important;
background: #2b281b !important;
color: var(--sys-accent) !important;
font-weight: 850 !important;
}
.wpsys-help {
margin: 6px 0 10px !important;
color: var(--sys-muted) !important;
font-size: 10px !important;
line-height: 1.6 !important;
}
.wpsys-char-grid {
display: grid !important;
grid-template-columns: repeat(7, minmax(38px, 1fr)) !important;
gap: 7px !important;
direction: ltr !important;
}
.wpsys-char-cell {
position: relative !important;
min-width: 0 !important;
}
.wpsys-char-cell::after {
content: attr(data-index) !important;
position: absolute !important;
right: 5px !important;
bottom: 3px !important;
color: #667286 !important;
font-size: 8px !important;
pointer-events: none !important;
}
.wpsys-char-cell input {
width: 100% !important;
height: 54px !important;
margin: 0 !important;
padding: 2px !important;
border: 1px solid #39475b !important;
border-radius: 10px !important;
background: #0c121b !important;
color: #fff !important;
text-align: center !important;
font-size: clamp(20px, 4vw, 29px) !important;
font-weight: 850 !important;
box-shadow: inset 0 1px 8px rgba(0, 0, 0, 0.25) !important;
outline: none !important;
}
.wpsys-char-cell input:focus {
border-color: var(--sys-accent) !important;
box-shadow: 0 0 0 3px rgba(242, 201, 76, 0.14) !important;
}
.wpsys-char-cell input:placeholder-shown {
background: #101722 !important;
}
.wpsys-range-list {
display: grid !important;
gap: 14px !important;
}
.wpsys-range-list label > span {
display: flex !important;
justify-content: space-between !important;
margin: 0 0 6px !important;
}
.wpsys-range-list output {
color: var(--sys-accent) !important;
font-weight: 850 !important;
}
.wpsys-range-list input[type="range"] {
width: 100% !important;
height: 5px !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
accent-color: var(--sys-accent) !important;
cursor: pointer !important;
}
.wpsys-note {
margin: 18px 0 0 !important;
padding: 12px !important;
border: 1px solid #343c30 !important;
border-radius: 11px !important;
background: #1b211b !important;
color: var(--sys-muted) !important;
font-size: 10px !important;
line-height: 1.6 !important;
}
.wpsys-note b {
color: #e9d77a !important;
}
.wpsys-role-table {
display: grid !important;
gap: 7px !important;
}
.wpsys-role-row {
display: grid !important;
grid-template-columns: 34px minmax(100px, 1fr) minmax(110px, 1.3fr) 74px !important;
align-items: center !important;
gap: 7px !important;
}
.wpsys-role-row.header {
color: #7f8b9d !important;
font-size: 9px !important;
font-weight: 800 !important;
text-transform: uppercase !important;
}
.wpsys-role-row input[type="text"] {
min-width: 0 !important;
height: 39px !important;
padding: 0 9px !important;
}
.wpsys-role-enabled {
display: grid !important;
place-items: center !important;
}
.wpsys-role-enabled input {
width: 17px !important;
height: 17px !important;
margin: 0 !important;
accent-color: var(--sys-accent) !important;
}
.wpsys-role-score {
padding: 8px 6px !important;
border: 1px solid #4c4220 !important;
border-radius: 8px !important;
background: #282318 !important;
color: var(--sys-accent) !important;
text-align: center !important;
font:
800 11px/1 ui-monospace,
SFMono-Regular,
Consolas,
monospace !important;
}
.wpsys-actions {
position: sticky !important;
z-index: 4 !important;
bottom: 10px !important;
display: flex !important;
justify-content: flex-end !important;
gap: 9px !important;
margin: 16px 0 0 !important;
padding: 10px !important;
border: 1px solid rgba(74, 85, 102, 0.82) !important;
border-radius: 14px !important;
background: rgba(15, 20, 29, 0.9) !important;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.26) !important;
backdrop-filter: blur(12px) !important;
}
.wpsys-actions button {
min-height: 42px !important;
margin: 0 !important;
padding: 0 16px !important;
border-radius: 10px !important;
font-size: 12px !important;
font-weight: 850 !important;
cursor: pointer !important;
}
.wpsys-secondary {
border: 1px solid #3b4657 !important;
background: #171e29 !important;
color: #c8d0db !important;
}
.wpsys-primary {
border: 1px solid #e9ba31 !important;
background: linear-gradient(135deg, #f2c94c, #e89a2d) !important;
color: #211801 !important;
box-shadow: 0 8px 20px rgba(232, 154, 45, 0.2) !important;
}
@media (max-width: 650px) {
.wpsys-panel {
padding: 14px !important;
border-radius: 18px !important;
}
.wpsys-presets,
.wpsys-grid,
.wpsys-form-grid {
grid-template-columns: 1fr !important;
}
.wpsys-card-wide {
grid-column: auto !important;
}
.wpsys-char-grid {
grid-template-columns: repeat(7, minmax(0, 1fr)) !important;
gap: 4px !important;
}
.wpsys-char-cell input {
height: 46px !important;
font-size: clamp(16px, 6vw, 25px) !important;
}
.wpsys-role-row {
grid-template-columns: 24px minmax(58px, 0.8fr) minmax(70px, 1.1fr) 48px !important;
gap: 4px !important;
}
.wpsys-role-row input[type="text"] {
padding: 0 5px !important;
font-size: 10px !important;
}
.wpsys-role-score {
padding: 7px 3px !important;
font-size: 10px !important;
}
.wpsys-actions {
justify-content: stretch !important;
}
.wpsys-actions button {
flex: 1 !important;
padding: 0 8px !important;
}
.wpsys-header {
display: block !important;
}
.wpsys-save-state {
display: inline-block !important;
margin-top: 10px !important;
}
}ソース:JavaScript
/* MOJI-SLOT */
(function () {
"use strict";
function bootMojiSlot() {
var RULES = {
symbolMin: 3,
symbolMax: 7,
reelMin: 3,
reelMax: 7,
roleCount: 5,
roleScores: [1000, 200, 100, 50, 25],
roleEffects: ["jackpot", "big", "big", "small", "small"],
};
(function () {
"use strict";
var CONFIG_KEY = "wpslotModernConfigV1";
var SCORE_KEY = "wpslotModernScoreV1";
var root = document.getElementById("wpslot-app");
if (!root || root.dataset.ready === "1") return;
root.dataset.ready = "1";
var fallback = {
title: "MOJI-SLOT",
preset: "chinsuko",
theme: "okinawa",
symbols: ["ち", "ん", "す", "こ", "う"],
reelCount: 5,
slotSetting: 6,
rowHeight: 64,
chanceProbability: 0.47,
forceRoleProbability: 0.72,
spinMin: 27,
spinMax: 33,
stopDuration: 720,
maxSlip: 4,
soundVolume: 0.12,
roles: [
{
id: "perfect",
name: "ちんすこう",
pattern: "ちんすこう",
score: 500,
enabled: true,
effect: "jackpot",
},
{
id: "chinchin",
name: "ちんちん",
pattern: "ちんちん",
score: 200,
enabled: true,
effect: "big",
},
{
id: "chinko",
name: "ちんこ",
pattern: "ちんこ",
score: 100,
enabled: true,
effect: "big",
},
{
id: "unko",
name: "うんこ",
pattern: "うんこ",
score: 50,
enabled: true,
effect: "small",
},
{
id: "chin",
name: "ちん",
pattern: "ちん",
score: 20,
enabled: true,
effect: "small",
},
],
};
var el = {
reels: document.getElementById("wpslot-reels"),
buttons: document.getElementById("wpslot-stop-buttons"),
bet: document.getElementById("wpslot-bet-btn"),
lever: document.getElementById("wpslot-lever"),
lamp: document.getElementById("wpslot-lamp"),
msg: document.getElementById("wpslot-msg"),
logo: document.getElementById("wpslot-logo"),
score: document.getElementById("wpslot-score"),
scoreReset: document.getElementById("wpslot-score-reset"),
paytable: document.getElementById("wpslot-paytable"),
setting: document.getElementById("wpslot-setting-label"),
fx: document.getElementById("wpslot-fx"),
};
if (!el.reels || !el.buttons || !el.bet || !el.lever) return;
var segmenter =
typeof Intl !== "undefined" && Intl.Segmenter
? new Intl.Segmenter("ja", { granularity: "grapheme" })
: null;
function decodeEntities(value) {
var text = String(value == null ? "" : value);
if (text.indexOf("&") < 0) return text;
var area = document.createElement("textarea");
area.innerHTML = text;
return area.value;
}
function chars(value) {
value = decodeEntities(value);
return segmenter
? Array.from(segmenter.segment(value), function (x) {
return x.segment;
})
: Array.from(value);
}
function number(v, min, max, d) {
v = Number(v);
return Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : d;
}
function clone(v) {
return JSON.parse(JSON.stringify(v));
}
function loadConfig() {
try {
var saved = JSON.parse(localStorage.getItem(CONFIG_KEY) || "null");
if (!saved) return clone(fallback);
var merged = Object.assign({}, fallback, saved);
merged.symbols = Array.isArray(saved.symbols)
? saved.symbols.slice(0, RULES.symbolMax)
: clone(fallback.symbols);
merged.roles = Array.isArray(saved.roles)
? saved.roles
: clone(fallback.roles);
return merged;
} catch (e) {
return clone(fallback);
}
}
var cfg = loadConfig(),
phase = "IDLE",
reels = [],
anim = 0,
score = number(localStorage.getItem(SCORE_KEY), 0, 999999999, 0);
var selectedRole = null,
forcedTarget = [],
audioCtx = null,
spinToken = 0,
pendingConfig = null;
function setMessage(text, isWin) {
el.msg.textContent = text;
el.msg.classList.toggle("win", !!isWin);
}
function saveScore() {
try {
localStorage.setItem(SCORE_KEY, String(score));
} catch (e) {}
el.score.textContent = score.toLocaleString("ja-JP");
}
function tone(freq, type, duration, delay) {
if (cfg.soundVolume <= 0) return;
window.setTimeout(function () {
try {
if (!audioCtx)
audioCtx = new (
window.AudioContext || window.webkitAudioContext
)();
if (audioCtx.state === "suspended") audioCtx.resume();
var osc = audioCtx.createOscillator(),
gain = audioCtx.createGain(),
now = audioCtx.currentTime;
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(cfg.soundVolume, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + duration);
} catch (e) {}
}, delay || 0);
}
function fanfare(effect) {
var notes =
effect === "jackpot"
? [523, 659, 784, 1047, 1319, 1568]
: effect === "big"
? [523, 659, 784, 1047]
: [659, 784, 988];
notes.forEach(function (f, i) {
tone(f, "triangle", 0.28, i * 115);
});
}
function confetti(amount) {
el.fx.textContent = "";
var colors = ["#ffd54a", "#ff315d", "#68f9d2", "#a878ff", "#fff"];
for (var i = 0; i < amount; i++) {
var p = document.createElement("i");
p.className = "wpslot-confetti";
p.style.left = Math.random() * 100 + "%";
p.style.background = colors[i % colors.length];
p.style.setProperty("--drift", (Math.random() - 0.5) * 180 + "px");
p.style.setProperty("--fall", 1 + Math.random() * 1.1 + "s");
p.style.animationDelay = Math.random() * 0.28 + "s";
el.fx.appendChild(p);
}
window.setTimeout(function () {
el.fx.textContent = "";
}, 2500);
}
function validRoles() {
return (cfg.roles || [])
.filter(function (role) {
var p = chars(role.pattern || "");
return (
role.enabled !== false &&
p.length > 0 &&
p.length <= cfg.reelCount &&
p.every(function (c) {
return cfg.symbols.indexOf(c) > -1;
})
);
})
.sort(function (a, b) {
return Number(b.score) - Number(a.score);
});
}
function renderPaytable() {
el.paytable.textContent = "";
validRoles().forEach(function (role) {
var chip = document.createElement("span");
chip.className = "wpslot-role-chip";
chip.append(document.createTextNode(role.pattern + " "));
var b = document.createElement("b");
b.textContent = "+" + role.score;
chip.appendChild(b);
el.paytable.appendChild(chip);
});
}
function applyConfig(next, announce) {
if (phase === "SPINNING") {
pendingConfig = next;
if (announce) setMessage("回転終了後に設定を反映します");
return;
}
cfg = Object.assign({}, fallback, next || {});
cfg.symbols = (
Array.isArray(cfg.symbols)
? cfg.symbols.reduce(function (all, item) {
return all.concat(chars(item));
}, [])
: chars(cfg.symbols)
)
.filter(Boolean)
.slice(0, RULES.symbolMax);
if (cfg.symbols.length < RULES.symbolMin)
cfg.symbols = clone(fallback.symbols);
cfg.roles = (Array.isArray(cfg.roles) ? cfg.roles : fallback.roles).map(
function (role) {
var clean = Object.assign({}, role);
clean.pattern = chars(clean.pattern || "")
.slice(0, RULES.symbolMax)
.join("");
return clean;
},
);
cfg.reelCount = Math.round(
number(
cfg.reelCount,
RULES.reelMin,
RULES.reelMax,
cfg.symbols.length,
),
);
cfg.rowHeight = Math.round(number(cfg.rowHeight, 52, 78, 64));
cfg.spinMin = number(cfg.spinMin, 12, 55, 27);
cfg.spinMax = number(cfg.spinMax, cfg.spinMin, 65, 33);
cfg.stopDuration = number(cfg.stopDuration, 260, 1600, 720);
cfg.maxSlip = Math.round(number(cfg.maxSlip, 0, 8, 4));
cfg.soundVolume = number(cfg.soundVolume, 0, 0.3, 0.12);
cfg.chanceProbability = number(cfg.chanceProbability, 0, 1, 0.47);
cfg.forceRoleProbability = number(cfg.forceRoleProbability, 0, 1, 0.72);
root.dataset.theme =
["okinawa", "classic", "neon"].indexOf(cfg.theme) > -1
? cfg.theme
: "okinawa";
root.style.setProperty("--row-h", cfg.rowHeight + "px");
el.logo.textContent = cfg.title || "MOJI-SLOT";
el.setting.textContent =
"設定 " + Math.round(number(cfg.slotSetting, 1, 6, 6));
renderPaytable();
setupReels();
if (announce) setMessage("システム設定を反映しました");
}
function symbolCycle(reelIndex, count) {
var out = [];
for (var i = 0; i < count; i++)
out.push(cfg.symbols[(i * 3 + reelIndex * 2) % cfg.symbols.length]);
for (var j = out.length - 1; j > 0; j--) {
var k = Math.floor(Math.random() * (j + 1)),
t = out[j];
out[j] = out[k];
out[k] = t;
}
cfg.symbols.forEach(function (s, idx) {
out[(idx * 3 + reelIndex) % out.length] = s;
});
return out;
}
function setupReels() {
spinToken++;
cancelAnimationFrame(anim);
el.reels.textContent = "";
el.buttons.textContent = "";
reels = [];
phase = "IDLE";
var baseCount = Math.max(28, cfg.symbols.length * 7);
for (var i = 0; i < cfg.reelCount; i++)
(function (index) {
var col = document.createElement("div"),
strip = document.createElement("div"),
cycle = symbolCycle(index, baseCount);
col.className = "wpslot-reel-col";
strip.className = "wpslot-reel-strip";
cycle.concat(cycle, cycle.slice(0, 7)).forEach(function (s) {
var item = document.createElement("div");
item.className = "wpslot-symbol";
item.textContent = s;
strip.appendChild(item);
});
col.appendChild(strip);
el.reels.appendChild(col);
var wrap = document.createElement("div"),
btn = document.createElement("button");
wrap.className = "wpslot-btn-col";
btn.type = "button";
btn.disabled = true;
btn.className = "wpslot-round-stop all-locked";
btn.setAttribute("aria-label", index + 1 + "番リールを停止");
btn.addEventListener("click", function () {
smartStop(index);
});
wrap.appendChild(btn);
el.buttons.appendChild(wrap);
var start = Math.floor(Math.random() * baseCount),
startPos = -(baseCount + start) * cfg.rowHeight;
strip.style.transform = "translate3d(0," + startPos + "px,0)";
reels.push({
index: index,
strip: strip,
btn: btn,
cycle: cycle,
count: baseCount,
pos: startPos,
speed: 0,
state: "stopped",
stopAt: 0,
startSpeed: 0,
target: null,
snapTimer: 0,
});
})(i);
el.bet.classList.remove("inserted");
el.lamp.classList.remove("gogo");
setMessage("BETボタン または レバー を押してください");
}
function input() {
if (phase === "IDLE") {
phase = "BET";
el.bet.classList.add("inserted");
tone(700, "square", 0.07);
setMessage("【BET完了】もう一度押すか、レバーでスタート!");
return;
}
if (phase === "BET") startSpin();
}
function chooseTarget() {
selectedRole = null;
forcedTarget = [];
var roles = validRoles();
if (!roles.length || Math.random() >= cfg.forceRoleProbability) return;
var totalWeight = roles.reduce(function (total, role) {
return total + 1 / Math.max(1, Number(role.score) || 1);
}, 0);
var draw = Math.random() * totalWeight;
selectedRole = roles[roles.length - 1];
roles.some(function (role) {
draw -= 1 / Math.max(1, Number(role.score) || 1);
if (draw > 0) return false;
selectedRole = role;
return true;
});
var p = chars(selectedRole.pattern),
offset = Math.floor(Math.random() * (cfg.reelCount - p.length + 1));
for (var i = 0; i < cfg.reelCount; i++)
forcedTarget[i] =
cfg.symbols[Math.floor(Math.random() * cfg.symbols.length)];
p.forEach(function (c, j) {
forcedTarget[offset + j] = c;
});
}
function startSpin() {
phase = "SPINNING";
root.setAttribute("aria-busy", "true");
spinToken++;
var token = spinToken;
el.bet.classList.remove("inserted");
el.msg.classList.remove("win");
el.lamp.classList.remove("gogo");
el.lever.classList.add("pulled");
window.setTimeout(function () {
el.lever.classList.remove("pulled");
}, 220);
chooseTarget();
var chance = Math.random() < cfg.chanceProbability || !!selectedRole;
if (chance)
window.setTimeout(function () {
if (phase === "SPINNING" && token === spinToken) {
el.lamp.classList.add("gogo");
tone(1280, "sawtooth", 0.24);
}
}, 140);
reels.forEach(function (r) {
clearTimeout(r.snapTimer);
r.state = "spinning";
r.speed = cfg.spinMin + Math.random() * (cfg.spinMax - cfg.spinMin);
r.btn.disabled = false;
r.btn.classList.remove("all-locked", "is-stopped");
});
tone(310, "sine", 0.14);
setMessage("リール回転中!好きな順番で止めてください");
cancelAnimationFrame(anim);
anim = requestAnimationFrame(engine);
}
function engine(now) {
var moving = false;
reels.forEach(function (r) {
if (r.state === "spinning") {
moving = true;
r.pos += r.speed;
} else if (r.state === "slowing") {
moving = true;
var p = Math.min(1, (now - r.stopAt) / cfg.stopDuration),
ease = (1 - p) * (1 - p);
r.pos += Math.max(0.35, r.startSpeed * ease);
if (p >= 1) beginSnap(r);
}
var loop = r.count * cfg.rowHeight;
while (r.pos >= -loop) r.pos -= loop;
while (r.pos < -2 * loop) r.pos += loop;
r.strip.style.transform = "translate3d(0," + r.pos + "px,0)";
});
if (moving) anim = requestAnimationFrame(engine);
}
function smartStop(index) {
if (phase !== "SPINNING") return;
var r = reels[index];
if (!r || r.state !== "spinning")
r = reels.find(function (x) {
return x.state === "spinning";
});
if (!r) return;
r.state = "slowing";
r.stopAt = performance.now();
r.startSpeed = r.speed;
r.target = forcedTarget[r.index] || null;
r.btn.classList.add("is-stopped");
tone(470, "square", 0.07);
}
function beginSnap(r) {
if (r.state !== "slowing") return;
r.state = "snapping";
var top = Math.floor(-r.pos / cfg.rowHeight),
slip = 0;
if (r.target) {
for (var s = 0; s <= cfg.maxSlip; s++) {
if (
r.cycle[(((top - s + 1) % r.count) + r.count) % r.count] ===
r.target
) {
slip = s;
break;
}
}
}
r.pos = -(top - slip) * cfg.rowHeight;
r.strip.style.transition =
"transform 180ms cubic-bezier(.2,.85,.3,1.05)";
r.strip.style.transform = "translate3d(0," + r.pos + "px,0)";
r.snapTimer = window.setTimeout(function () {
var loop = r.count * cfg.rowHeight;
while (r.pos >= -loop) r.pos -= loop;
while (r.pos < -2 * loop) r.pos += loop;
r.strip.style.transition = "none";
r.strip.style.transform = "translate3d(0," + r.pos + "px,0)";
r.state = "stopped";
checkStopped();
}, 195);
}
function resultString() {
return reels
.map(function (r) {
var top =
((Math.round(-r.pos / cfg.rowHeight) % r.count) + r.count) %
r.count;
return r.cycle[(top + 1) % r.count];
})
.join("");
}
function checkStopped() {
if (
!reels.every(function (r) {
return r.state === "stopped";
})
)
return;
phase = "IDLE";
root.setAttribute("aria-busy", "false");
reels.forEach(function (r) {
r.btn.disabled = true;
r.btn.classList.add("all-locked");
});
var result = resultString();
var role = validRoles().find(function (x) {
return result.indexOf(x.pattern) > -1;
});
if (role) {
var gain = Math.max(0, Math.round(Number(role.score) || 0));
score += gain;
saveScore();
el.lamp.classList.add("gogo");
setMessage(
"【" + role.name + "】 " + result + " +" + gain + " POINT!",
true,
);
fanfare(role.effect);
confetti(role.effect === "jackpot" ? 54 : 32);
} else {
setMessage("停止結果:「" + result + "」 次のBETへ");
tone(210, "sine", 0.12);
}
if (pendingConfig) {
var queued = pendingConfig;
pendingConfig = null;
window.setTimeout(function () {
applyConfig(queued, true);
}, 900);
}
}
el.bet.addEventListener("click", input);
el.lever.addEventListener("click", input);
el.scoreReset.addEventListener("click", function () {
if (phase === "SPINNING") return;
score = 0;
saveScore();
setMessage("スコアを0に戻しました");
});
window.addEventListener("wpslot:config-changed", function (e) {
applyConfig(e.detail || loadConfig(), true);
});
window.addEventListener("storage", function (e) {
if (e.key === CONFIG_KEY) applyConfig(loadConfig(), true);
});
saveScore();
applyConfig(cfg, false);
})();
(function () {
"use strict";
var KEY = "wpslotModernConfigV1",
root = document.getElementById("wpslot-system-settings");
if (!root || root.dataset.ready === "1") return;
root.dataset.ready = "1";
var seg =
typeof Intl !== "undefined" && Intl.Segmenter
? new Intl.Segmenter("ja", { granularity: "grapheme" })
: null;
function decodeEntities(v) {
var text = String(v == null ? "" : v);
if (text.indexOf("&") < 0) return text;
var area = document.createElement("textarea");
area.innerHTML = text;
return area.value;
}
function chars(v) {
v = decodeEntities(v);
return seg
? Array.from(seg.segment(v), function (x) {
return x.segment;
})
: Array.from(v);
}
function clone(v) {
return JSON.parse(JSON.stringify(v));
}
var cherry = String.fromCodePoint(0x1f352),
banana = String.fromCodePoint(0x1f34c),
bell = String.fromCodePoint(0x1f514),
star = String.fromCodePoint(0x2b50),
seven = String.fromCodePoint(0x37, 0xfe0f, 0x20e3),
gear = String.fromCodePoint(0x2699);
var classicIcon = root.querySelector(
'[data-preset="classic"] .wpsys-preset-icon',
);
if (classicIcon) classicIcon.textContent = cherry;
var customIcon = root.querySelector(
'[data-preset="custom"] .wpsys-preset-icon',
);
if (customIcon) customIcon.textContent = gear;
var presets = {
chinsuko: {
title: "MOJI-SLOT",
preset: "chinsuko",
theme: "okinawa",
symbols: ["ち", "ん", "す", "こ", "う"],
reelCount: 5,
slotSetting: 6,
rowHeight: 64,
chanceProbability: 0.47,
forceRoleProbability: 0.72,
spinMin: 27,
spinMax: 33,
stopDuration: 720,
maxSlip: 4,
soundVolume: 0.12,
roles: [
{
id: "perfect",
name: "ちんすこう",
pattern: "ちんすこう",
score: 500,
enabled: true,
effect: "jackpot",
},
{
id: "chinchin",
name: "ちんちん",
pattern: "ちんちん",
score: 200,
enabled: true,
effect: "big",
},
{
id: "chinko",
name: "ちんこ",
pattern: "ちんこ",
score: 100,
enabled: true,
effect: "big",
},
{
id: "unko",
name: "うんこ",
pattern: "うんこ",
score: 50,
enabled: true,
effect: "small",
},
{
id: "chin",
name: "ちん",
pattern: "ちん",
score: 20,
enabled: true,
effect: "small",
},
],
},
classic: {
title: "LUCKY SEVEN",
preset: "classic",
theme: "classic",
symbols: [cherry, banana, bell, star, seven],
reelCount: 3,
slotSetting: 3,
rowHeight: 68,
chanceProbability: 0.28,
forceRoleProbability: 0.45,
spinMin: 30,
spinMax: 38,
stopDuration: 860,
maxSlip: 4,
soundVolume: 0.14,
roles: [
{
id: "seven",
name: "JACKPOT",
pattern: seven.repeat(3),
score: 1000,
enabled: true,
effect: "jackpot",
},
{
id: "cherry",
name: "CHERRY",
pattern: cherry.repeat(3),
score: 200,
enabled: true,
effect: "big",
},
{
id: "bell",
name: "BELL",
pattern: bell.repeat(3),
score: 100,
enabled: true,
effect: "big",
},
{
id: "banana",
name: "BANANA",
pattern: banana.repeat(3),
score: 50,
enabled: true,
effect: "small",
},
{
id: "star",
name: "STAR",
pattern: star.repeat(3),
score: 25,
enabled: true,
effect: "small",
},
],
},
};
var e = {
title: root.querySelector("#wpsys-machine-title"),
theme: root.querySelector("#wpsys-theme"),
chars: root.querySelector("#wpsys-char-grid"),
count: root.querySelector("#wpsys-symbol-count"),
reels: root.querySelector("#wpsys-reel-count"),
setting: root.querySelector("#wpsys-slot-setting"),
roles: root.querySelector("#wpsys-role-table"),
state: root.querySelector("#wpsys-save-state"),
apply: root.querySelector("#wpsys-apply"),
reload: root.querySelector("#wpsys-reload"),
};
var rangeDefs = {
chance: ["#wpsys-chance", "#wpsys-chance-out", "%"],
force: ["#wpsys-force", "#wpsys-force-out", "%"],
speedMin: ["#wpsys-speed-min", "#wpsys-speed-min-out", " px/f"],
speedMax: ["#wpsys-speed-max", "#wpsys-speed-max-out", " px/f"],
stop: ["#wpsys-stop", "#wpsys-stop-out", " ms"],
slip: ["#wpsys-slip", "#wpsys-slip-out", " コマ"],
row: ["#wpsys-row", "#wpsys-row-out", " px"],
volume: ["#wpsys-volume", "#wpsys-volume-out", "%"],
};
Object.keys(rangeDefs).forEach(function (k) {
var d = rangeDefs[k];
e[k] = root.querySelector(d[0]);
e[k + "Out"] = root.querySelector(d[1]);
e[k].addEventListener("input", function () {
e[k + "Out"].textContent = e[k].value + d[2];
dirty();
});
});
var charInputs = [],
currentPreset = "custom";
for (var i = 0; i < RULES.symbolMax; i++)
(function (index) {
var wrap = document.createElement("label"),
inp = document.createElement("input");
wrap.className = "wpsys-char-cell";
wrap.dataset.index = String(index + 1);
inp.type = "text";
inp.inputMode = "text";
inp.placeholder = "·";
inp.setAttribute("aria-label", index + 1 + "文字目");
inp.addEventListener("input", function () {
var a = chars(inp.value);
inp.value = a[0] || "";
if (a.length > 1) distribute(a, index);
updateCount();
dirty();
if (inp.value && index < 6) charInputs[index + 1].focus();
});
inp.addEventListener("paste", function (ev) {
var text = (ev.clipboardData || window.clipboardData).getData(
"text",
);
if (text) {
ev.preventDefault();
distribute(chars(text), index);
updateCount();
dirty();
}
});
inp.addEventListener("keydown", function (ev) {
if (ev.key === "Backspace" && !inp.value && index > 0) {
charInputs[index - 1].focus();
}
});
wrap.appendChild(inp);
e.chars.appendChild(wrap);
charInputs.push(inp);
})(i);
function distribute(list, start) {
list.slice(0, RULES.symbolMax - start).forEach(function (c, j) {
charInputs[start + j].value = c;
});
}
function symbols() {
return charInputs
.map(function (x) {
return x.value;
})
.filter(Boolean);
}
function updateCount() {
e.count.textContent = symbols().length + " / " + RULES.symbolMax;
}
function setState(text, type) {
e.state.textContent = text;
e.state.classList.toggle("saved", type === "saved");
e.state.classList.toggle("error", type === "error");
}
function dirty() {
currentPreset = "custom";
setState("未保存", "dirty");
root.querySelectorAll(".wpsys-preset").forEach(function (x) {
var active = x.dataset.preset === "custom";
x.classList.toggle("active", active);
x.setAttribute("aria-pressed", String(active));
});
}
function roleTemplate(roles) {
e.roles.textContent = "";
var h = document.createElement("div");
h.className = "wpsys-role-row header";
["ON", "役名", "柄", "SCORE"].forEach(function (t) {
var s = document.createElement("span");
s.textContent = t;
h.appendChild(s);
});
e.roles.appendChild(h);
roles.forEach(function (role, index) {
var row = document.createElement("div");
row.className = "wpsys-role-row";
row.dataset.id = role.id || "role" + index;
row.dataset.effect = role.effect || RULES.roleEffects[index];
row.dataset.score = String(role.score || RULES.roleScores[index]);
var checkWrap = document.createElement("label");
checkWrap.className = "wpsys-role-enabled";
var check = document.createElement("input");
check.type = "checkbox";
check.checked = role.enabled !== false;
check.setAttribute("aria-label", index + 1 + "番目の役を有効化");
checkWrap.appendChild(check);
var name = document.createElement("input");
name.type = "text";
name.maxLength = 18;
name.value = role.name || "";
name.setAttribute("aria-label", index + 1 + "番目の役名");
var pattern = document.createElement("input");
pattern.type = "text";
pattern.value = chars(role.pattern || "").join("");
pattern.setAttribute("aria-label", index + 1 + "番目の役柄");
pattern.addEventListener("input", function () {
pattern.value = chars(pattern.value)
.slice(0, RULES.symbolMax)
.join("");
dirty();
});
var score = document.createElement("span");
score.className = "wpsys-role-score";
score.textContent = "+" + row.dataset.score;
[check, name].forEach(function (x) {
x.addEventListener("input", dirty);
});
row.append(checkWrap, name, pattern, score);
e.roles.appendChild(row);
});
}
function normalizeRoles(list, presetName) {
var defaults = presets[presetName] ? presets[presetName].roles : [];
var base =
list && list.length
? list
: defaults.length
? defaults
: presets.chinsuko.roles;
return Array.from({ length: RULES.roleCount }, function (_, i) {
return (
base[i] ||
defaults[i] || {
id: "role" + i,
name: "役" + (i + 1),
pattern: "",
score: RULES.roleScores[i],
enabled: false,
effect: RULES.roleEffects[i],
}
);
});
}
function setForm(c, presetName) {
c = Object.assign({}, presets.chinsuko, c || {});
e.title.value = c.title || "";
e.theme.value = c.theme || "okinawa";
charInputs.forEach(function (x, i) {
x.value = chars((c.symbols || [])[i] || "")[0] || "";
});
e.reels.value = c.reelCount || 5;
e.setting.value = c.slotSetting || 6;
e.chance.value = Math.round((c.chanceProbability || 0) * 100);
e.force.value = Math.round((c.forceRoleProbability || 0) * 100);
e.speedMin.value = c.spinMin || 27;
e.speedMax.value = c.spinMax || 33;
e.stop.value = c.stopDuration || 720;
e.slip.value = c.maxSlip == null ? 4 : c.maxSlip;
e.row.value = c.rowHeight || 64;
e.volume.value = Math.round((c.soundVolume || 0) * 100);
Object.keys(rangeDefs).forEach(function (k) {
e[k].dispatchEvent(new Event("input"));
});
roleTemplate(normalizeRoles(c.roles, presetName || c.preset));
updateCount();
var activePreset = presetName || c.preset || "custom";
currentPreset = activePreset;
root.querySelectorAll(".wpsys-preset").forEach(function (x) {
var active = x.dataset.preset === activePreset;
x.classList.toggle("active", active);
x.setAttribute("aria-pressed", String(active));
});
setState("未保存", "dirty");
}
function readForm() {
var syms = symbols();
if (syms.length < RULES.symbolMin)
throw new Error("リールの文字・記号は3文字以上設定してください。");
var rc = Math.max(
RULES.reelMin,
Math.min(RULES.reelMax, Number(e.reels.value) || RULES.reelMin),
);
var roleRows = Array.from(
e.roles.querySelectorAll(".wpsys-role-row:not(.header)"),
);
var roles = roleRows.map(function (row) {
var inputs = row.querySelectorAll("input");
return {
id: row.dataset.id,
name: inputs[1].value.trim() || "役",
pattern: chars(inputs[2].value).slice(0, RULES.symbolMax).join(""),
score: Number(row.dataset.score),
enabled: inputs[0].checked,
effect: row.dataset.effect,
};
});
if (
roles.some(function (r) {
return r.enabled && !r.pattern;
})
)
throw new Error("有効な役には「柄」を入力してください。");
if (
roles.some(function (r) {
return r.enabled && chars(r.pattern).length > rc;
})
)
throw new Error("役の柄はリール数以下にしてください。");
if (
roles.some(function (r) {
return (
r.enabled &&
chars(r.pattern).some(function (c) {
return syms.indexOf(c) < 0;
})
);
})
)
throw new Error(
"役の柄には設定済みの文字・記号だけを使用してください。",
);
return {
title: e.title.value.trim() || "MOJI-SLOT",
preset: currentPreset,
theme: e.theme.value,
symbols: syms,
reelCount: rc,
slotSetting: Number(e.setting.value),
rowHeight: Number(e.row.value),
chanceProbability: Number(e.chance.value) / 100,
forceRoleProbability: Number(e.force.value) / 100,
spinMin: Number(e.speedMin.value),
spinMax: Math.max(Number(e.speedMin.value), Number(e.speedMax.value)),
stopDuration: Number(e.stop.value),
maxSlip: Number(e.slip.value),
soundVolume: Number(e.volume.value) / 100,
roles: roles,
};
}
function load() {
try {
return (
JSON.parse(localStorage.getItem(KEY) || "null") ||
clone(presets.chinsuko)
);
} catch (err) {
return clone(presets.chinsuko);
}
}
root.querySelectorAll(".wpsys-preset").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.dataset.preset === "custom") {
dirty();
return;
}
setForm(clone(presets[btn.dataset.preset]), btn.dataset.preset);
});
});
[e.title, e.theme, e.reels].forEach(function (x) {
x.addEventListener("input", dirty);
});
e.setting.addEventListener("change", function () {
var s = Number(e.setting.value);
e.chance.value = Math.round((0.08 + s * 0.065) * 100);
e.force.value = Math.min(98, Math.round((0.2 + s * 0.13) * 100));
e.chance.dispatchEvent(new Event("input"));
e.force.dispatchEvent(new Event("input"));
dirty();
});
e.apply.addEventListener("click", function () {
try {
var c = readForm();
localStorage.setItem(KEY, JSON.stringify(c));
window.dispatchEvent(
new CustomEvent("wpslot:config-changed", { detail: c }),
);
setState("保存済み", "saved");
} catch (err) {
setState(err.message, "error");
}
});
e.reload.addEventListener("click", function () {
setForm(load(), "custom");
setState("保存済み", "saved");
});
var initial = load();
if (
!Array.isArray(initial.roles) ||
initial.roles.length < RULES.roleCount
) {
var migrationPreset = initial.preset;
var savedSymbols = Array.isArray(initial.symbols)
? initial.symbols.join("")
: "";
if (migrationPreset === "custom" && savedSymbols === "ちんすこう")
migrationPreset = "chinsuko";
if (
migrationPreset === "custom" &&
savedSymbols === presets.classic.symbols.join("")
)
migrationPreset = "classic";
initial.roles = normalizeRoles(initial.roles, migrationPreset);
try {
localStorage.setItem(KEY, JSON.stringify(initial));
window.dispatchEvent(
new CustomEvent("wpslot:config-changed", { detail: initial }),
);
} catch (err) {}
}
setForm(initial, initial.preset || "custom");
setState("保存済み", "saved");
})();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootMojiSlot, { once: true });
} else {
bootMojiSlot();
}
})();

コメント