マージャンソリティア

(2018/01/13 公開)

 同じ牌を選んで消していきます。牌は、左または右と上が空いているものしか取れません。全部とったらクリアです。

 このプログラムも、動作させるための最低限の実装しかしていません。クリアメッセージすらありません(ctrl-Cで止めるしかない)。気が向いたら改造するということで。

 牌の画像データmahjong.png という名前で、SmileBASIC のディレクトリに置いておいてください。このデータはゲームデザイン (http://www.gamedesign.jp/) ご提供の「フリー素材」からいただきました。(現在はリンクがなくなっているようです。)

プログラムリスト

'  マージャンソリティア
'  2018.1.13. Toshi Nagata
'  UTF-8 encoding

'  牌のレイアウト (x1,y1)-(x2,y2),z
'  レイアウトは x, y, z の昇順になっていること
@layout1
data 0, 0, 8, 0, 0
data 1, 1, 10, 1, 0
data 2, 2, 11, 2, 0
data 3, 3, 12, 3, 0
data 4, 4, 13, 4, 0
data 3, 5, 12, 5, 0
data 2, 6, 11, 6, 0
data 1, 7, 10, 7, 0
data 3.5, 1, 8.5, 3, 1
data 4.5, 4, 9.5, 6, 1
data 5, 2, 8, 5, 2
data 6, 3, 7, 4, 3
data 6.5, 3.5, 6.5, 3.5, 4

dim posx[136], posy[136], posz[136]
dim tiles[136], flags[136]
cenx = 0
ceny = 0
maxz = 0
wid = 28
high = 40
wid0 = 27
high0 = 34
curn = -1

'  スプライトの初期化
def init_sprite
  var nx, ny, i, j
  '  GRP2 をスプライト用に使う
  sppage 2
  '  画像読み込み
  load "grp2:mahjong.png"
  '  カーソル位置表示用の四角を GRP2 に描く
  gpage 0, 2
  for i = 0 to 3
    gbox i, high * 5 + i, wid0 + 2 - i, high * 5 + high0 + 2 - i, rgb(160, 50, 50, 255)
  next
  gpage 0, 0
  '  スプライトの定義
  for j = 0 to 33   '  (0, 0) はマスク用(このプログラムでは使わない)
    nx = (j + 1) mod 7
    ny = (j + 1) div 7
    spdef j, nx * wid, ny * high, wid, high
    for i = 0 to 3
      spset j * 4 + i, j
    next
  next
  '  カーソル位置表示用スプライト
  spdef 34, 0, high * 5, wid0 + 2, high0 + 2
  spset 200, 34
end

'  レイアウトを読みこむ
def read_layout
  var x1, y1, x2, y2, z, i, j, n
  restore @layout1
  n = 0
  while 1
    read x1, y1, x2, y2, z
    x2 = x2 - x1
    y2 = y2 - y1
    for i = 0 to y2
      for j = 0 to x2
        posx[n] = x1 + j
        posy[n] = y1 + i
        posz[n] = z
        n = n + 1
        if n >= 136 then @break
      next
    next
  wend
  @break
  cenx = (max(posx) + min(posx)) / 2
  ceny = (max(posy) + min(posy)) / 2
  maxz = max(posz)
end

'  牌をシャッフル
def shuffle
  var i, j, k, w
  for i = 0 to 135
    tiles[i] = i
    flags[i] = 1   ' すべて通常表示
  next
  for i = 0 to 100
    j = rnd(136)
    k = rnd(136)
    w = tiles[j]
    tiles[j] = tiles[k]
    tiles[k] = w
  next
end

'  n 番の牌の上に牌があるかどうかチェック
def is_hidden(n)
  var x1, y1, z1, m
  x1 = posx[n]
  y1 = posy[n]
  z1 = posz[n]
  for m = n + 1 to 135
    if (flags[m] and 1) == 0 then continue
    if posz[m] != z1 + 1 then continue
    if abs(x1 - posx[m]) < 1 && abs(y1 - posy[m]) < 1 then return 1
  next
  return 0
end

'  カーソル移動処理:n から (dx, dy) 方向で一番近い牌を探す
def search_tile(n, dx, dy)
  var m, ret, d, dd, x, y, wx, wy
  ret = n
  if n < 0 then
    x = 0
    y = 0
  else
    x = posx[n]
    y = posy[n]
  endif
  for m = 0 to 135
    if m == n || (flags[m] and 2) == 0 then continue
    wx = posx[m] - x
    wy = posy[m] - y
    if dx != 0 && sgn(wx) != dx then continue
    if dy != 0 && sgn(wy) != dy then continue
    dd = wx * wx + wy * wy
    if ret == n || dd < d then
      ret = m
      d = dd
    endif
  next
  return ret
end

'  取れる牌をマーク
def update_selectable
  var n, m, c
  dim count[34]
  for n = 0 to 33
    count[n] = 0
  next
  for n = 0 to 135
    flags[n] = flags[n] and 1
    ' すでに消されている牌
    if flags[n] == 0 then continue
    ' 左に同じ行・高さの牌がある
    if n > 0 && (flags[n - 1] and 1) != 0 && posy[n - 1] == posy[n] && posz[n - 1] == posz[n] then
      ' 右に同じ行・高さの牌がある
      if n < 135 && (flags[n + 1] and 1) != 0 && posy[n + 1] == posy[n] && posz[n + 1] == posz[n] then
        continue
      endif
    endif
    ' 上に牌がある
    if is_hidden(n) then continue
    ' 取れる牌としてマーク
    flags[n] = flags[n] or 2
    ' 取れる牌の種類をカウント
    m = tiles[n] div 4
    inc count[m]
  next
  c = 0
  for n = 0 to 135
    ' 「取れる」とした牌をもう一度調べる
    if flags[n] != 3 then continue
    ' カウントが1の牌は実際には取れない
    m = tiles[n] div 4
    if count[m] <= 1 then
      flags[n] = 1
    else
      inc c  '  取れる牌の数を数える
    endif
  next
  '  カーソル位置を更新
  if curn < 0 then
    curn = search_tile(-1, 0, 0)
  else
    curn = search_tile(curn, 0, 0)
  endif
end

'  牌を選択する
def select_tile n
  var m
  '  n がすでに選択されていたら選択を外す
  if (flags[n] and 4) != 0 then
    flags[n] = flags[n] and 3
    return
  endif
  '  すでに選択されているものがあるかどうかチェック
  for m = 0 to 135
    if (flags[m] and 4) != 0 then
      '  同じタイルか?
      if (tiles[m] div 4) == (tiles[n] div 4) then
        '  消す
        flags[m] = 0
        flags[n] = 0
        update_selectable
        return
      else
        '  前の選択を解除してこの牌だけを選択
        flags[m] = flags[m] and 3
        flags[n] = flags[n] or 4
        return
      endif
    endif
  next
  '  この牌を選択
  flags[n] = flags[n] or 4
end

'  盤面を表示
def display
  var px, py, n, cn, col, spn, flg
  gfill 0, 36, 639, 359, rgb(0, 64, 0)
  for n = 0 to 135
    spn = tiles[n]
    flg = flags[n]
    if (flg and 1) == 0 then
      sphide spn
      continue
    endif
    px = (posx[n] - cenx - 0.5) * wid0 + 320
    py = (posy[n] - ceny - 0.5) * high0 + 36 + 162
    if (flg and 4) != 0 then
      col = #red
    elseif (flg and 2) != 0 then
      col = #yellow
    else
      cn = 255 - (maxz - posz[n]) / maxz * 60
      col = rgb(cn, cn, cn)
    endif
    spcolor spn, col
    spofs spn, px - posz[n] * 3, py - posz[n] * 7, -n - 1
    spshow spn
    if n == curn then
      spofs 200, px - posz[n] * 3 - 2, py - posz[n] * 7 - 2, -200
      spshow 200
    endif
  next
  if curn < 0 then sphide 200
end

init_sprite
read_layout
shuffle
update_selectable
display
while curn >= 0
  repeat : in$ = inkey$() : until len(in$) > 0
  ch = asc(in$)
  if ch == 13 then
    select_tile curn
    display
    continue
  endif
  x = posx[curn]
  y = posy[curn]
  if ch == 28 then
    dx = 1
    dy = 0
  elseif ch == 29 then
    dx = -1
    dy = 0
  elseif ch == 30 then
    dx = 0
    dy = -1
  elseif ch == 31 then
    dx = 0
    dy = 1
  endif
  curn = search_tile(curn, dx, dy)
  display
wend