お絵描きパズル

今は携帯全盛ですが、一昔前までは通勤電車で「お絵描きパズル」に夢中になっている方を良く見かけたものです。
それほど面白く多くの人に親しまれているドットで絵を描く「Logic Puzzle」を作成します。
Javascript から引き継いで「お絵描きゲーム」を完成させます。

プログラムを知らなくても お絵描きパズルの実行 から「お絵描きパズル」を楽しむことが出来ます。
事前に お絵描きゲーム の後部から「お絵描きパズル」のデータファイルをダウンロードして下さい。

JavaScript から引き継ぎの概要

  1. お絵描きゲーム でパズルゲームの作成を始めました。
    一応完成してプレイ出来るようになったのですが、パズルの問題(データ)を直接プログラム中に記述しています。
    本来ならお絵描きパズルのデータはファイルから入力するのが本筋でしょう。
    そこで、データを入力するページとゲームを操作するページを分けて作成することにしました。
    そのとき問題となるのがパラメータで渡すデータサイズの問題です。
    (JavaScript でもファイルから入力できるようになったのですが、サイズが大きくなると動きません。)
    そこで JavaScript で入力したデータを POST で PHP に渡して、お絵描きパズルを完成させます。
  2. Javascript から渡されるデータは、改行コード(\r\n)を「;」に置き換えた次のような形式です。
    「・・・」は同様の記述が続くことを意味します。
    //いちご;15 15;//行方向の定義;2;4 2;7 2;2 6;・・・7;//列方向の定義;6;8 1;4 3 3;・・・4 2 1 1 ;・・・
    
  3. PHP では Javascript から渡されたデータ(string)を解析して、プレイ画面を表示します。
    そして Javascript のページ(Canvas を含む)をブラウザに渡してパズルをプレイします。
    プレイヤーが操作するマウスやキーボードは、クライアントサイドで動作する Javascript でなければ検出できません。
    PHP が吐き出すページのソースコードは、Javascript のホームページに準じます。

  4. ファイルからデータを入力して dot_game.php 呼び出す dot_game.html のソースコードです。
    <html>
    <head>
    <meta charset=utf-8>
    <title>お絵描き</title>
    </head>
    
    <body bgcolor=#e0d8d0>
    <h1>お絵描きパズル</h1>
    データファイルを選択して下さい。<br><br>
    <form name="test">
    <input type="file" id="selfile" /><br/>
    </form>
    
    <script>
    function postForm(value)
    {   var form = document.createElement('form');
        var request = document.createElement('input');
     
        form.method = 'POST';
        form.action = 'http://localhost:8000/dot_game.php';
        //form.action = 'http://maedakobe.rw.xsi.jp/php/dot_game.php';
     
        request.name = 'data';
        request.value = value;
     
        form.appendChild(request);
        document.body.appendChild(form);
     
        form.submit();
    }
    
        var obj1 = document.getElementById("selfile");
        //ダイアログでファイルが選択された時
        obj1.addEventListener("change",function(evt)
        {   var file = evt.target.files;
            var reader = new FileReader();
            reader.readAsText(file[0]);
      
            //読込終了後の処理
            reader.onload = function(ev)
            {   var str= reader.result;
                str= str.replace(/\n/g, ';');
                postForm(str);
            }
        },false);
    </script>
    
    </body>
    </html>
    
  5. ファイル選択で、パズルファイルからデータを入力して、改行コード(\n)を「;」に置き換えて渡します。
    デバッグ段階では dot_game.php は、パソコンからビルトインサーバーを起動してテストしています。('http://localhost:8000/dot_game.php')。
    テストが終わった段階で、PHP をサポートしているサーバーにアップロードします。
    Javascript のファイル入力の説明は Read Text File を参照して下さい。
    POST の転送は POST で送信 を参照して下さい。

  6. Javascript からデータを受け取ってゲームページを吐き出す dot_game.php のソースコードです。
    <html>
    <head>
    <meta charset="UTF-8">
    <title>お絵描きパズル</title>
    <script>
    // Counter Class で数字を表示
    function Counter(img, sw, sh)
    {   this.Img= img;  //Image File(0~9の画像)
        this.Sw = sw;   //Sprite の幅
        this.Sh = sh;   //Sprite の高さ
    
        //「上, 右, 下, 左」の順
        this.View_Num = function(num, x, y)
        {   if (num>9)
            {   var n= Math.floor(num/10);
                var pos = n*this.Sw;
                var s1= 'style="clip:rect(0px,' + (pos+this.Sw) + 'px,' + this.Sh + 'px,' + pos + 'px);';
                var s2 = 'position:absolute;left:' + (x-pos-2) + 'px;top:' + y + 'px;';
                var s = '<img src="' + this.Img + '"' + s1 + s2 + '">';
                document.write(s);
                var n= num%10;
                var pos = n*this.Sw;
                var s1= 'style="clip:rect(0px,' + (pos+this.Sw) + 'px,' + this.Sh + 'px,' + pos + 'px);';
                var s2 = 'position:absolute;left:' + (x-pos+2) + 'px;top:' + y + 'px;';
                var s = '<img src="' + this.Img + '"' + s1 + s2 + '">';
                document.write(s);
                return;
            }
            var pos = num*this.Sw;
            var s1= 'style="clip:rect(0px,' + (pos+this.Sw) + 'px,' + this.Sh + 'px,' + pos + 'px);';
            var s2 = 'position:absolute;left:' + (x-pos) + 'px;top:' + y + 'px;';
            var s = '<img src="' + this.Img + '"' + s1 + s2 + '">';
            document.write(s);
        }
    }
    // num行(num+100列)を抽出→s_line[], s_no, s_data[]
    function Select(num)
    {   if (num<0 || (num>90 && num-100<0)) return;
        s_line = []; 
        //選択マークの設定(赤⇔null)
        if (s_no!=-1)
        {   if (s_no<90)    //行(枠の下をクリア)
            {   Mark3("null", -1, s_no);
                for(var i=0; i<xnum; i++) Mark3('null', i, ynum);
            }
            else            //列(枠の右をクリア)
            {   Mark3("null", s_no-100, -1);
                for(var i=0; i<ynum; i++) Mark3('null', xnum, i);
            }
        }
        if (num<90)     Mark3("red", -1, num);
        else            Mark3("red", num-100, -1);
    
        // num 行(列)を抽出
        //window.alert("Select:" + num + "  ynum:" + ynum + "  xnum:" + xnum);
        s_no= num;
        if (num<90)     // 行を抽出
        {   for(var i=0; i<xnum; i++) s_line[i]= t[num][i];
            s_data= xt[num];
        }
        else            // 列を抽出
        {   var nw= num-100;
            for(var i=0; i<ynum; i++) s_line[i]= t[i][nw];
            s_data= yt[nw];
        }
        Cross();
        Set_Hint(num, s_line);
    }
    // 行・列のヒントを枠の右・下に表示
    function Set_Hint(num, s_line)
    {   //window.alert(s_line);
        if (num<100)    //抽出行を下に描画
        {   for(var i=0; i<xnum; i++)
            {   if (s_line[i]==1)       Mark3("black", i, ynum);
                else if(s_line[i]==2)   Mark3("white", i, ynum);
                else Mark3("null", i, ynum);
            }
        }
        else            //抽出列を右に描画
        {   for(var i=0; i<ynum; i++)
            {   if (s_line[i]==1)       Mark3("black", xnum, i);
                else if(s_line[i]==2)   Mark3("white", xnum, i);
                else Mark3("null", xnum, i);
            }
        }
        SetMark("black");
    }
    // s_line→配列 t[][] に戻す
    function Set(num)
    {   //window.alert("NUM:" + num + "  SEL:" + s_line + "  ynum:" + ynum + "  xnum:" + xnum);
        if (num<100)    //行を戻す(0~xnum, num)
        {   for(var i=0; i<xnum; i++)
            {   t[num][i]= s_line[i];
                if (s_line[i]==1)   Mark("black", i, num);
                if (s_line[i]==2)   Mark("white", i, num);
                if (s_line[i]==0)   Mark("null", i, num);
            }
        }
        else            //列を戻す(num-100, 0~ynum)
        {   var nw= num-100;
            for(var i=0; i<ynum; i++)
            {   t[i][nw]= s_line[i];
                if (s_line[i]==1)   Mark("black", nw, i);
                if (s_line[i]==2)   Mark("white", nw, i);
                if (s_line[i]==0)   Mark("null", nw, i);
            }
        }
        SetMark("black");
    }
    // s_line[] の解析(s_line[], s_no, s_data[]) は正しいとする
    function Cross()
    {   num= s_line.length;
        var sum = 0;        //s_data[] の合計
        for(var i=0; i<s_data.length; i++)
        {   sum += parseInt(s_data[i]);  }
        var c= 0;           //Mark Count
        var n= 0;           //null Count
        for(var i=0; i<num; i++)
        {   if (s_line[i]==1)   c++;
            if (s_line[i]==0)   n++;
        }
        //window.alert("sum:" + sum + "  mark:" + c + "  null:" + n);
        if (c==sum)         //マーク確定(残りは空白)
        {   for(var i=0; i<num; i++)
                if (s_line[i]!=1)   s_line[i]= 2;
            return;
        }
        if (c+n==sum)       //残りの null にマークする
        {   for(var i=0; i<num; i++)
                if (s_line[i]==0)   s_line[i]= 1;
            return;
        }
        //重なるマークを調べる
        var ary= new Array();
        for(i=0; i<s_data.length; i++)
        {   for(j=0; j<s_data[i]; j++)  ary.push(i);
            if (ary.length<num) ary.push(-1);
        }
        var bk= new Array();
        for(i=s_data.length-1; i>=0; i--)
        {   for(j=0; j<s_data[i]; j++)  bk.push(i);
            if (bk.length<num)  bk.push(-1);
        }
        for(i=ary.length; i<num; i++)
        {   ary.push(-1);
            bk.push(-1);
        }
        for(i=0; i<num; i++)
        {   if (ary[i]!=-1 && ary[i]==bk[num-i-1])  s_line[i]= 1;  }
    }
    function Mark(cor, xp, yp)
    {   if (xp>=xnum || yp>=ynum || xp<0 || yp<0)   return;
        Mark2(cor, xp, yp);
    }
    function Mark2(cor, xp, yp)
    {   var xw = xp*16+88;
        var yw = yp*16+88;
        c.fillStyle = cor;
        if (cor=='black' || cor=='white')
        {   c.fillRect(xw, yw, 13, 13);
            return;
        }
        if (cor=='null')
        {   c.clearRect(xw, yw, 13, 13);
            return;
        }
        c.beginPath();
        if (cor=='red') c.arc(xw+4, yw+4, 3, 0, 2 * Math.PI, false);
        else if (cor=='green')  c.arc(xw+10, yw+4, 3, 0, 2 * Math.PI, false);
        else if (cor=='blue')   c.arc(xw+4, yw+10, 3, 0, 2 * Math.PI, false);
        else c.arc(xw+10, yw+10, 3, 0, 2 * Math.PI, false);
        c.fill();
    }
    function Mark3(cor, xp, yp)
    {   var xw = xp*16+90;
        var yw = yp*16+90;
        c.fillStyle = cor;
        if (cor=='black' || cor=='white')
        {   c.fillRect(xw, yw, 10, 10);
            return;
        }
        if (cor=='null')
        {   c.clearRect(xw, yw, 10, 10);
            return;
        }
        c.beginPath();
        if (cor=='red') c.arc(xw+3, yw+3, 3, 0, 2 * Math.PI, false);
        //else if (cor=='green')  c.arc(xw+5, yw+3, 3, 0, 2 * Math.PI, false);
        //else if (cor=='blue')   c.arc(xw+3, yw+5, 3, 0, 2 * Math.PI, false);
        //else c.arc(xw+5, yw+5, 3, 0, 2 * Math.PI, false);
        c.fill();
    }
    // 左上隅にマークを描画
    function SetMark(mk)
    {   cor= mk;
        c.clearRect(20, 20, 14, 14);
        if (cor=='null')    return;
        c.fillStyle = cor;
        if (cor=='black' || cor=='white')
        {   c.fillRect(20, 20, 10, 10);
            return;
        }
        c.beginPath();
        if (cor=='red') c.arc(23, 23, 3, 0, 2 * Math.PI, false);
        else if (cor=='green')  c.arc(21, 21, 3, 0, 2 * Math.PI, false);
        else if (cor=='blue')   c.arc(21, 21, 3, 0, 2 * Math.PI, false);
        else c.arc(23, 23, 3, 0, 2 * Math.PI, false);
        c.fill();
    }
    // 完成チェック
    function Fin()
    {   SetMark("black");
        for(var i=0; i<ynum; i++)
            for(var j=0; j<xnum; j++)
                if (t[i][j]==0)
                {   window.alert("未完成です");
                    return;
                }
        for(var i=0; i<xt.length; i++)
        {   var wk= xt[i];
            var p= 0;
            for(var j=0; j<wk.length; j++)
            {   for(; p<xnum && t[i][p]!=1; p++);
                for(var c=0; p<xnum && t[i][p]==1; c++, p++);
                if (wk[j]!=c)
                {   window.alert("Error 行:" + i);
                    return;
                }
            }
        }
        for(var i=0; i<yt.length; i++)
        {   var wk= yt[i];
            var p= 0;
            for(var j=0; j<wk.length; j++)
            {   for(; p<ynum && t[p][i]!=1; p++);
                for(var c=0; p<ynum && t[p][i]==1; c++, p++);
                if (wk[j]!=c)
                {   window.alert("Error 列:" + i);
                    return;
                }
            }
        }
        window.alert("*完成です");
    }
    function scr()
    {   xscr = document.body.scrollLeft;
        yscr = document.body.scrollTop;
        //xscr = document.documentElement.scrollLeft;
        //yscr = document.documentElement.scrollTop;
        //alert("横" + xscr + "px,縦" + yscr + "px");
    }
    // 罫線を描画する
    function View_Line()
    {   // 5セルの枠
        c.strokeStyle = 'blue';
        c.lineWidth = 1;
        for(var i=0; i<xnum; i=i+5)
            for(var j=0; j<ynum; j=j+5)
            {   c.strokeRect(i*16+base+71, j*16+base+71, 80, 80);  }
        // 線の色と太さ
        c.strokeStyle = 'gray';
        c.lineWidth = 2;
        c.beginPath();
        // 横線
        for(var i=0; i<=ynum; i++)
        {   c.moveTo(base+70, i*16+base+70);
            c.lineTo(xnum*16+base+70, i*16+base+70);
        }
        // 縦線
        for(var i=0; i<=xnum; i++)
        {   c.moveTo(i*16+base+70, base+70);
            c.lineTo(i*16+base+70, ynum*16+base+70);
        }
        // 外枠
        c.moveTo(base+1, base+1);
        c.lineTo(xnum*16+base+70, base+1);
        c.moveTo(base+1, base+1);
        c.lineTo(base+1, ynum*16+base+70);
        for(var i=0; i<ynum+1; i=i+5)
        {   c.moveTo(base, i*16+base+70);
            c.lineTo(base+70, i*16+base+70);
        }
        for(var i=0; i<xnum+1; i=i+5)
        {   c.moveTo(i*16+base+70, base);
            c.lineTo(i*16+base+70, base+70);
        }
        c.stroke();
    
        // 数字の表示
        var cls = new Counter("numm.gif", 16, 16);
        for(var i=0; i<xt.length; i++)
           for(var j=0; j<xt[i].length; j++)
               cls.View_Num(xt[i][j], j*10+base+10, i*16+base+80);
        for(var i=0; i<yt.length; i++)
           for(var j=0; j<yt[i].length; j++)
               cls.View_Num(yt[i][j], i*16+base+80, j*12+base+10);
        //メニューの表示
        document.write('<div onclick= Set(s_no) style="position:absolute;left:' + bs + 'px;top:140px;">設定</div>');
        document.write('<div onclick= Fin() style="position:absolute;left:' + (bs+60) + 'px;top:140px;">完成?</div>');
        document.write('<div onclick= SetMark("black") style="position:absolute;left:' + bs + 'px;top:180px;">黑</div>');
        document.write('<div onclick= SetMark("white") style="position:absolute;left:' + bs + 'px;top:200px;">白</div>');
        document.write('<div onclick= SetMark("null") style="position:absolute;left:' + bs + 'px;top:220px;">消去</div>');
        document.write('<div onclick= SetMark("red") style="position:absolute;left:' + (bs+60) + 'px;top:180px;">赤</div>');
        document.write('<div onclick= SetMark("green") style="position:absolute;left:' + (bs+60) + 'px;top:200px;">緑</div>');
        document.write('<div onclick= SetMark("blue") style="position:absolute;left:' + (bs+60) + 'px;top:220px;">青</div>');
        document.write('<div onclick= SetMark("yellow") style="position:absolute;left:' + (bs+60) + 'px;top:240px;">黄</div>');
    }
    </script>
    <?php
    function init($str)
    {   global $xnum, $ynum, $xt, $yt, $name;
    
        //error_log($str, 3, 'app.log');
        $w1 = explode(';', $str);
        $name= substr($w1[0],2);        //パズル名
        $xy= explode(' ', $w1[1]);      //行数, 列数
        //$xy= explode("/,| /", $w1[1]);    //Error
        $xnum= intval($xy[0]);
        $ynum= intval($xy[1]);
        //error_log("\nX num:$xnum  Y num:$ynum\n", 3, 'app.log');
    
        $c= 0;
        for($p=3; $c<$xnum; $p++)
        {   $wk= $w1[$p];
            if (substr($wk,0,1)=="/")   continue;
            //error_log("\n$wk", 3, 'app.log');
            $ww= explode(' ', $wk);
            array_push($xt, $ww);
            $c++;
        }
        for($i=0; $i<count($xt); $i++)
        {   $wk= $xt[$i];
            if (count($wk)>1 && $wk[count($wk)-1]==0)   array_pop($xt[$i]);
        }
        $c= 0;
        for($p++; $c<$ynum; $p++)
        {   $wk= $w1[$p];
            if (substr($wk,0,1)=="/")   continue;
            //error_log("\n$wk", 3, 'app.log');
            $ww= explode(' ', $wk);
            array_push($yt, $ww);
            $c++;
        }
        for($i=0; $i<count($yt); $i++)
        {   $wk= $yt[$i];
            if (count($wk)>1 && $wk[count($wk)-1]==0)   array_pop($yt[$i]);
        }
    }
    ?>
    </head>
    
    <body bgcolor=#e0d8d0>
    <canvas id="mycanvas" width="800" height="600"></canvas>
    <style>
    canvas
    {   border: 1px solid silver;  }
    </style>
    
    <?php
    $xnum= 0;
    $ynum= 0;
    $xt= array();
    $yt= array();
    $name= "いちご";
    $data = $_POST["data"];
    init($data);
    $jsonXT = json_encode($xt);
    $jsonYT = json_encode($yt);
    ?>
    
    <script>
      document.onmousedown =
        function(e)
        {   if (!e)  e= window.event;
            scr();      // Scrill 値を調べる
            var xp = Math.floor((e.clientX-base-75)/16);
            var yp = Math.floor((e.clientY-base-75)/16);
            var click= e.clientX + "," + e.clientY;
            document.getElementById("click").textContent= click;
            var pos= xp + "," + yp;
            document.getElementById("pos").textContent= pos;
    
            // マスのクリック
            if (xp>=0 && xp<xnum && yp>=0 && yp<ynum)
            {   Mark(cor, xp, yp);
                if (cor=='null')    t[yp][xp]= 0;
                if (cor=='black')   t[yp][xp]= 1;
                if (cor=='white')   t[yp][xp]= 2;
                return;
            }
            // 行を抽出する
            if (xp<-4 && yp<ynum)
            {   Select(yp);
                return;
            }
            // 列を抽出する
            if (yp<-4 && xp<xnum)
            {   Select(xp+100);
                return;
            }
            return;
        }
    
        xt = JSON.parse('<?php echo $jsonXT;?>');
        yt = JSON.parse('<?php echo $jsonYT;?>');
        //window.alert("XT: " + xt);
        ynum= xt.length;  // 縦方向のマスの数
        xnum= yt.length;  // 横方向のマスの数
        //window.alert("縦のマス:" + ynum + "  横のマス:" + xnum);
        xscr= 0;                    // ScrollX 値
        yscr= 0;                    // ScrollY 値
        cor= 'black';   
        base= 16;                   // マスの基点
        bs= (xnum+2)*16+86;         // メニューの表示座標
        s_line= new Array(50);      // 解析エリア(MAX 50)
        s_no= -1;                   // 行(0~15),列(100~115)
        s_data= [0];
        t= new Array(ynum);         // 0:未定, 1:マーク, 2:空白
        for(var i=0; i<ynum; i++)
        {   t[i]= new Array(xnum);  }
        for(var i=0; i<ynum; i++)
            for(var j=0; j<xnum; j++)
                t[i][j]= 0;
    
        var canvas = document.getElementById('mycanvas');
        c = canvas.getContext('2d');
        View_Line();
        SetMark("black");
    </script>
    <p>
      <div id="click" style="position:absolute;left:26px;top:40px;">click</div><br>
      <div id="pos" style="position:absolute;left:26px;top:55px;">pos</div><br>
      <div style="position:absolute;left:26px;top:75px;"><?php echo $name;?></div>
    </p>
    
    </body>
    </html>
    
  7. PHP から Javascript に渡すデータに関係する領域の定義です。
    $ynum が列(縦)方向に並べたマスの数で、$xnum は行(横)方向に並べたマスの数です。
    $xt, $yt は、行と列に表示する数字のテーブルです。
    $name はファイルから入力したパズルの名前です。
    $_POST["data"]; でファイルのテキストデータを受け取って init($data); で解析します。
    PHP で作成したゲームデータのテーブル($xt, $yt)を Javascript に json_encode で渡します。
    <?php
    $xnum= 0;
    $ynum= 0;
    $xt= array();
    $yt= array();
    $name= "いちご";
    $data = $_POST["data"];
    init($data);
    $jsonXT = json_encode($xt);
    $jsonYT = json_encode($yt);
    ?>
    
  8. Javascript では $xt, $yt を JSON.parse で受け取ります。
        xt = JSON.parse('<?php echo $jsonXT;?>');
        yt = JSON.parse('<?php echo $jsonYT;?>');
    
    $name をページに表示します。
      <div style="position:absolute;left:630px;top:50px;"><?php echo $name;?></div>
    
  9. dot_game.php が吐き出すページのソースコードは、Javascript のホームページに準じます。
    説明は お絵描きゲーム を参照して下さい。

アップロード

  1. ビルトインサーバーのテストが終わった段階で dot_game.php をサーバーにアップロードします。
    dot_game.html から dot_game.php を呼び出しているソースコードを修正して下さい。
    'http://maedakobe.rw.xsi.jp' は、私がお世話になっている PHP のサーバーです。
        form.action = 'http://maedakobe.rw.xsi.jp/php/dot_game.php';
    
  2. パズルが実行される流れは次のようになります。
    1. 私のホームページから「お絵描きパズル」をクリックして dot_game.html を呼び出します。
      (パソコン上の dot_game.html をダブルクリックで起動してもOKです)
    2. ブラウザが起動して、パソコン上で dot_game.html のファイル選択画面が表示されます。
    3. お絵描きパズルのファイルを選択して、データを入力します。
    4. POST 転送でサーバーの dot_game.php を呼び出します。
    5. dot_game.php でデータを解析して、ゲームページを吐き出します。
    6. 吐き出されたソースコードをブラウザが受け取ってゲーム画面を表示します。
    7. ゲーム画面でプレイします。
  3. プログラムを知らなくても お絵描きパズルの実行 から「お絵描きパズル」を楽しむことが出来ます。
    事前に お絵描きゲーム の後部から「お絵描きパズル」のデータファイルをダウンロードして下さい。
    パズルファイルは、一行目がパズル名で、2行目がサイズで、行の定義と列の定義が続きます。
    蛇.dot(縦のマス:15, 横のマス:10)の例です。
    //蛇
    15 10
    //行方向の定義
    3 
    2 2 
    6 
    2 5 
    2 
    3 
    2 
    3 
    3 
    2 
    8 
    10 
    1 2 2 
    1 7 
    5 
    //列方向の定義
    1 3 
    2 2 
    2 2 
    4 6 
    1 2 8 
    4 4 2 2 
    7 2 2 
    3 2 2 
    5 
    3 
    

前田稔の超初心者のプログラム入門
PHP Program