Tuesday, September 22, 2015

jQuery'de Seçiciler

jQuery, istemci tarafta Javascript (JS) tabanlı uygulama geliştirirken karşılaşılan bazı temel problemleri ortadan kaldırmak ve uygulama geliştirmeyi kolaylaştırmak amacıyla geliştirilmiş bir JS kütüphanesidir. jQuery'yi çeşitli amaçlarla JS projelerimizde kullanıyoruz:
  • HTML'i ya da CSS'i değiştirme
  • HTML olaylarını işleme
  • Animasyon ve efekt programlama
  • AJAX programlama
  • JS programlama için kolaylık fonksiyonları
Bu yazıda jQuery seçicilerini kullanarak HTML sayfaya yeni yetenekler nasıl kazandırılabileceğini inceleyeceğiz. jQuery seçicileri ile HTML dokümanındaki elemanları (takı ve öznitelik gibi) seçiyoruz. Seçilen elemanlar daha sonra jQuery fonksiyonları kullanılarak değiştirilebilir. Eğer CSS seçicilerini biliyorsanız, jQuery seçicilerini öğrenmek çok kolay olacaktır. jQueryCSS 1-3 seçicilerinin hepsini kullanır, bunun dışında kendine özel seçicileri bulunur. 
Şimdi üzerinde jQuery seçicilerini çalışacağımız HTML dokümanıyla tanışalım: movies.html.
Örneklerde kullanacağımız HTML doküman
movies.html içinde 299 adet film ile ilgili bilgiler yer alıyor: filmin adı, yönetmenleri, türleri ve yapım yılı. Bu bilgiler HTML tablo olarak düzenlenmiş olarak karşımıza çıkıyor.
jQuery ile HTML elemanları seçerken, seçimi üç farklı kritere göre yapabiliriz:
1. Takıya göre seçim
$(document).ready(function(){
   $('tr').css('border','solid red 3px');
});
Burada $('tr') ile tüm tablo satırlarını seçiyoruz:
Tüm çift satırları seçmek için aşağıdaki satırı yazmamız yeterli olur:
$(document).ready(function(){
   $('tr:even').css('border','solid red 3px');
});
2. id özniteliği ile seçim
Seçmek istediğimiz elemanın id özniteliğini biliyorsak, # sembolü ile tanımlayarak seçimi gerçekleştirebiliriz:
$(document).ready(function(){
   $('#movies').css('border','solid red 3px');
});
3. class özniteliğine göre seçim
HTML elemanı class değerine göre seçmek istersek, . sembolü ile class değerini tanımlayarak seçimi gerçekleştirebiliriz:
$(document).ready(function(){
   $('.panel-heading').css('border','solid red 3px');
});
Seçim yaparken bu üç kriteri beraberce de kullanabiliriz. Şimdi daha karmaşık seçimleri çalışmak için örnek alıştırma problemleri çözelim.
  • Basit bir seçici ile başlayalım. Sayfada çok sayıda <a> bulunuyor. Bu bağlantıları takip ettiğimizde bizi http://www.imdb.com sitesine götürüyor. Ancak biz sayfanın tarayıcıda yeni bir sekmede açılmasını istiyoruz:
$(document).ready(function(){
   $('#movies tbody a[href]').attr('target','_blank');
});
  • Şimdi ise tabloda 1970'li yıllara ait filmlerin listelenmesini sağlayalım:
$(document).ready(function(){
   $('#movies tbody tr').each(function(){
      var year= Number($(this).find('td:eq(4)').text().trim());
      if (year<1970 || year>1979)
         $(this).hide(); 
   });
});
1970'li yıllara ait filmlerin listesi
  • Birden fazla yönetmeni olan filmleri tabloda gösterelim:
$(document).ready(function(){
   $('#movies tbody tr').each(function(){
     var numberOfDirectors= $(this).find('td:eq(2) a').length;
     if (numberOfDirectors<2)
        $(this).hide(); 
   });
});
Birden fazla yönetmeni olan filmlerin listesi
  • Sadece Comedy ve Drama türündeki filmleri tabloda gösterelim:
$(document).ready(function(){
   $('#movies tbody tr').each(function(){
      td= $('td:eq(3)',$(this));
      var genres= td.text().trim().replace(/\s+/g,'').split(',');   
      if (genres.length!=2 || genres.indexOf('Comedy')<0 || genres.indexOf('Drama')< 0 )
         $(this).hide(); 
   });
});
Comedy ve Drama türündeki filmlerin listesi
  • Tablonun göstermek istediğimiz sütunlarını seçmemizi sağlayacak bir arayüz ekleyelim:
$(document).ready(function(){
 var columns= { 'No': 0, 
         'Title' : 1, 
         'Directors' : 2, 
         'Genres' : 3, 
         'Year': 4 
        };
 function createCheckbox(column){
  return   '<div class="checkbox-inline"><label><input id="'
         + column 
         + '" type="checkbox" checked />'
         + column+'</label></div>';
 }
 for (column in columns){
   $('#movies').before(createCheckbox(column));
      $('#'+column).click(function(){
         selectedColumn= $(this).parent().text();
         index= columns[selectedColumn];
         if (this.checked){
            $('#movies thead th:eq('+index+')').show();
            $('#movies tbody td:nth-child(5n+'+(index+1)+')').show();
         } else {
            $('#movies thead th:eq('+index+')').hide();
            $('#movies tbody td:nth-child(5n+'+(index+1)+')').hide();
         }
    });
  }
});
No, Title ve Year sütunları seçili durumdayken tablonun görünümü
Sadece Title sütunu seçili durumdayken tablonun görünümü
Herhangi bir sütun seçili olmadığı durumda tablonun görünümü
  • Tabloyu sütunlarına göre sıralayacak bir arayüz ekleyelim:
$(document).ready(function(){
    function numeric(val1,val2){
      return Number(val1)-Number(val2); 
    }
    function dictionary(val1,val2){
     return val1.localeCompare(val2); 
    }
    function sortTableByColumn(table,index,compareFunction){
     tbody= table.find('tbody');
     tbody.find('tr').sort(
         function(p,q){
            pval= $('td:eq('+index+')',p).text().trim();
            qval= $('td:eq('+index+')',q).text().trim();
            return compareFunction(pval,qval);
         }
     ).appendTo(tbody);
    }
    var columns=  { 
      'No'        : { index: 0 , sort: sortTableByColumn, compareBy: numeric }, 
      'Title'     : { index: 1 , sort: sortTableByColumn, compareBy: dictionary }, 
      'Directors' : { index: 2 , sort: sortTableByColumn, compareBy: dictionary }, 
      'Genres'    : { index: 3 , sort: sortTableByColumn, compareBy: dictionary }, 
      'Year'      : { index: 4 , sort: sortTableByColumn, compareBy: numeric } 
    };
    function createButton(column){
       return '<button class="btn btn-success" id="'+column.toLowerCase()+'">'
              +   column 
              + '</button>';
    }
    for (column in columns){
        th= $('table thead th:eq('+columns[column].index+')');
        th.text('');
        th.append(createButton(column));
        $('#'+column.toLowerCase()).click(function(){
             selectedColumn= columns[$(this).text()];
             selectedColumn.sort( 
$('#movies'), selectedColumn.index, selectedColumn.compareBy 
);
          });
        }
});
Filmin adına göre sıralamak yapmak için Title butonuna basıldığında tablonun görünümü
Yönetmen adına göre sıralamak yapmak için Directors butonuna basıldığında tablonun görünümü
Filmin yılına göre sıralamak yapmak için Year butonuna basıldığında tablonun görünümü
  • Film türlerini farklı arka plan renginde etiket olarak gösterelim:
colorIndex= 0;

colors = [ 
    'Khaki', 'LightSlateGray' , 'Blue', 
    'BlueViolet', 'Brown', 'CadetBlue',
    'Chocolate', 'Crimson', 'DarkBlue', 
    'DarkGoldenRod', 'DarkMagenta', 'DarkOrange',
    'DeepSkyBlue', 'Gold', 'Green',
    'Maroon', 'MediumAquaMarine', 'Moccasin',
    'Navy', 'OliveDrab', 'PaleVioletRed'
] ;

function getNextColor() {
   return colors[colorIndex++]; 
}

var colorMap = {} ;

function getColor(genre){
   if (!colorMap.hasOwnProperty(genre))
      colorMap[genre]= getNextColor();
   return colorMap[genre];
}

$(document).ready(function(){
  $('#movies tbody tr').each(function(){
     td= $('td:eq(3)',$(this));
     genres= td.text().trim().replace(/\s+/g,'').split(',');
     td.text('');
     $.each(genres,function(index,genre){
         td.append(  '<span class="label" style="background-color: ' 
                   + getColor(genre) 
                   + ';">' + genre 
                   + '</span>'); 
     });
  });
});

Her bir film türü farklı bir arka plan renginde
  • Birden fazla yönetmeni olan filmler için sadece ilk yönetmeni gösterelim:
$(document).ready(function(){
  $('#movies tbody tr').each(function(){
     directors= $('td:eq(2)',this);   
     directorAnchors= $('a',directors);   
     $(directors).empty();
     $(directors).append(directorAnchors[0]);
  });
});
  • Tablodaki yönetmen ve film türleri sütunlarının yerlerini takas edelim:
$(document).ready(function(){
  $('#movies tbody tr').each(function(){
     directors= $('td:eq(2)',this);   
     genres= $('td:eq(3)',this);   
     $(genres).after($(directors));
  });
  thead= $('#movies thead tr');
  $('th:eq(3)',thead).after($('th:eq(2)',thead));
});
Yönetmen ve Film türleri sütunlarının takas edilmesi
  • Film türlerinin hemen yanında, o türden kaç film olduğunu gösterelim:
var histogram= {};
var badges= {};

function createBadge(genre){
   if (badges.hasOwnProperty(genre)){
      return badges[genre];
   }
   badge= '<li role="presentation" class="active">'
     + genre
     + '<span class="badge">'
     + histogram[genre]
      + '</span></a></li>';
   badges[genre]= badge;
   return badge;
} ;

function extractGenreColumn(tr){
   return $('td:eq(3)',$(tr));
};

function extractGenres(td){
   return td.text().trim().replace(/\s+/g,'').split(',');
};

function computeHistogram(){
   td= extractGenreColumn(this);
   $.each(extractGenres(td),function(index,genre){
      if(!histogram.hasOwnProperty(genre)){
        histogram[genre] = 0; 
      } 
      histogram[genre] = histogram[genre] + 1; 
   });
};

function badgeHistogram(){
   td= extractGenreColumn(this);
   genres= extractGenres(td);
   td.empty();
   ul= $('<ul class="nav nav-pills" role="tablist">').appendTo(td);
   $.each(genres,function(index,genre){
       ul.append(createBadge(genre));
   });
   $(this).append('</ul>');
};

$(document).ready(function(){
   $('#movies tbody tr').each(computeHistogram);
   $('#movies tbody tr').each(badgeHistogram);
});
Film türlerinin hemen yanında histogram bilgisi gösteriliyor
Projenin tamamına bu bağlantıdan ulaşabilirsiniz. jQuery ile ilgili daha detaylı bilgi almak isterseniz jQuery and jQuery UI eğitimini öneririm.

Monday, September 21, 2015

Knockout.js'de Bağlama (=Binding) Şekilleri

Zengin içerikli ve yüksek etkileşimli web uygulamaları için kullanıcı arayüz mantığını mutlaka tarayıcıda oluşturmak gerekir. Bu arayüz mantığını ise Javascript (JS) ile kodlamak gerekirse de dilin tarayıcılar arasındaki tutarsızlıkları, kullanıcı arayüzü oluşturmak için alt düzey bir dil olması gibi temel sebepler nedeniyle üst seviye çatılara ihtiyaç bulunur. jQuery, tarayıcılar arasındaki farklılıkları gidermiş ve DOM güncellemeyi kolaylaştırsa da basit karmaşıklıktaki bir arayüz mantığı için bile bir çözüm oluşturmaktan uzaktır. 
Son beş yılda, masaüstü uygulama geliştirirken yoğun olarak kullanılan MVC kalıbını, bu sefer Web tarayıcıda çalışan arayüzler oluşturmak için kullanan, çok sayıda JS çatısının doğduğuna tanık olduk. Bu çatılar arasında, basitliği ve sadeliği ile öne çıkan bir çatı bulunuyor: KnockoutJSKnockoutJS, Model-View-ViewModel (MVVM) kalıbını kullanıyor. Bu kalıpta Model sunucu tarafta, genellikle veri tabanında yer alır. View bileşenini HTML5 ve CSS3 kullanarak oluşturuyoruz. ViewModel ise arayüzde kullanıcıya sunmak istediğimiz veriyi modeller. View ile onu besleyen ViewModel arasındaki bağlantıyı ise HTML takılarına iliştirdiğimiz data-bind özniteliğini kullanarak bildirimsel (=declarative) olarak yapıyoruz. data-bind, HTML5 standardında tanımlı bir öznitelik (=attribute) değil. Ancak HTML5'in amacı Web tarayıcısını, uygulama geliştirebileceğimiz bir platforma dönüştürmektir. Bu nedenle KnockoutJS gibi çatıların ihtiyaçlarına uygun olarak yeni takı (=tag) ve öznitelik tanımlamalarına izin verir. KnockoutJS'de MVC'de deki Controller kodlamaya ihtiyaç bulunmaz. Controller, data-bind özniteliği ile tanımlanan bağın çift taraflı (2-way binding) çalışmasını sağlar ve göz önünde bulunmaz. 
Bu yazıda Knockout.js'deki çift taraflı bağlantı ele alınacaktır. Çift taraflı bağlantıyı çalışabilmek için örnek bir uygulama geliştireceğiz: Mastermind. Bu bir oyun. Bilgisayar n basamaklı, basamakları biri birinden farklı bir sayı seçer ve oyuncu bu sayıyı tahmin etmeye çalışır. n oyunun seviyesi. Başlangıç seviyesi ise 3. Oyunun başında bilgisayar 3 basamaklı, basamakları biri birinden bir sayı seçer. Bilgisayar kullanıcının tahminini değerlendirir ve bir sonraki tahmini daha iyi yapabilmesi için geri bildirimde bulunur. Her doğru basamakta bulduğu rakam için + ve yanlış basamakta bulduğu rakam için - kodlama yapar. Örneğin, bilgisayarın rastgele tuttuğu sayı 549 olsun. Kullanıcının 759 tahmini için bilgisayar 11, 579 tahmini için 2 ve 957 tahmini için ise 2 geri bildiriminde bulunur. Oyuncu, 549 tahmini ile bu seviyedeki sayıyı bildiğinde, oyunun seviyesi bir artar. Şimdi bilgisayar 4 basamaklı, basamakları biri birinden farklı rastgele bir sayı seçer. Oyuncunun amacı şimdi dört basamaklı olan bu sayıyı tahmin etmektir. Oyun en son sekizinci seviye için oynanır. Oyuncunun, bu seviyedeki sayıyı doğru tahmin etmesi ile oyun sona erer. Bu oyun için ViewModel aşağıdaki gibi olabilir:
var GameViewModel = function() {
 var self = this;

 self.guess = 123 ;
 self.gameLevel = 3;
 self.tries = 0;

 self.history = [];
 self.secret = createSecret(self.gameLevel);
}
View ise aşağıdaki gibi bir HTML olarak tasarlanabilir:
<!--  Game Board -->
<div class="panel panel-primary">
 <div class="panel-heading">
  <h3 class="panel-title">Mastermind</h3>
 </div>
 <div class="panel-body">
  <div class="form-group">
   <label for="gameLevel">Game Level:</label> <span></span>
  </div>
  <div class="form-group">
   <label for="tries">Tries:</label> <span></span>
  </div>
  <div class="form-group">
   <label for="tries">Time:</label> <span></span>
  </div>
  <div class="form-group">
   <label for="guess">Guess:</label> <span></span>
  </div>
  <div class="form-group">
   <button class="btn btn-success">Play</button>
  </div>
 </div>
</div>
<!--  Moves -->
<div class="panel panel-success">
 <div class="panel-heading">
  <h3 class="panel-title">Moves</h3>
 </div>
 <div class="panel-body">
  <table class="table table-striped">
   <thead>
    <tr>
     <th>Guess</th>
     <th>Evaluation</th>
    </tr>
   </thead>
   <tbody>
    <tr>
     <td></td>
     <td></td>
    </tr>
   </tbody>
  </table>
 </div>
</div>
Amacımız View değiştiğinde otomatik olarak ViewModel'i değiştirecek, ViewModel değiştiğinde ise View'i otomatik olarak değiştirecek bir çözüm oluşturmaktır. Knockout.js bunu, ViewModel ile View arasında bağlantı tanımlayarak gerçekleştirmemize yardımcı oluyor. Knockout.js'de View ile ViewModel arasında üç tür bağlantı kurulabilir:
1. ko.observable()
Eğer bağlanacak değişken skaler bir değişken ise bu değişken ko.observable() çağrısı ile yaratılır.
2. ko.observableArray()
Eğer bağlanacak değişken bir dizi ise bu değişken ko.observableArray() çağrısı ile yaratılır.
3. ko.computed()
Eğer bağlanacak değer, diğer "observable" değişkenlerden hesaplanarak oluşturulacak ise bu durumda ko.computed() çağrısı ile bir fonksiyon olarak tanımlanır.
Yukarıdaki örnek ViewModel'i şimdi bu "observable" yapıları kullanarak yeniden tanımlayalım:
var GameViewModel = function() {
   var self = this;

   self.guess = ko.observable();
   self.gameLevel = ko.observable(3);
   self.tries = ko.observable(0);
   self.history = ko.observableArray([]);
   self.secret = createSecret(self.gameLevel());
}
ViewModel ile View arasındaki bağlantı, HTML'de data-bind özniteliği kullanılarak sağlanır:
<!--  Game Board -->
<div class="panel panel-primary">
 <div class="panel-heading">
  <h3 class="panel-title">Mastermind</h3>
 </div>
 <div class="panel-body">
  <div class="form-group">
   <label for="gameLevel">Game Level:</label> 
   <span data-bind="text: gameLevel"></span>
  </div>
  <div class="form-group">
   <label for="tries">Tries:</label> 
   <span data-bind="text: tries"></span>
  </div>
  <div class="form-group">
   <label for="guess">Guess:</label> 
   <span data-bind="value: guess"></span>
  </div>
  <div class="form-group">
   <button>Play</button>
  </div>
 </div>
</div>
<!--  Moves -->
<div class="panel panel-success">
 <div class="panel-heading">
  <h3 class="panel-title">Moves</h3>
 </div>
 <div class="panel-body">
  <table class="table table-striped">
   <thead>
    <tr>
     <th>Guess</th>
     <th>Evaluation</th>
    </tr>
   </thead>
   <tbody>
    <tr>
     <td></td>
     <td></td>
    </tr>
   </tbody>
  </table>
 </div>
</div>
data-bind ile farklı amaçlarla bağlama yapılabilir: modeli sadece okumak, modeli hem okumak hem de değiştirmek. data-bind="text: tries" ile tries modelini sadece okumak için bağlıyoruz, data-bind="value: guess" ile guess modelini hem okumak hem de değiştirmek için bağlıyoruz. Bu durumda ön yüzden tahmin "guess" girildiğinde otomatik olarak ViewModel'deki guess değişecektir. Bağladığımız sadece öznitelik değil, bir fonksiyon da olabilir. Örneğin Play butonuna basıldığında ViewModel'deki bir fonksiyonun çalışmasını isteriz:
self.play = function() {
 var move = evaluateGuess(self.guess(), self.secret, self.gameLevel());
 self.tries(self.tries() + 1);
 if (move.eval.win) {
    self.gameLevel(self.gameLevel() + 1);
    self.init();
 }
 self.history.push(move);
}
Bu fonksiyonu View'daki Play butonuna aşağıdaki şekilde bağlıyoruz:
<div class="form-group">
   <button data-bind="click: play">Play</button>
</div>
Tüm bu bağlantıların işe yarayabilmesi için tüm sayfa yüklendikten sonra ko.applyBindings() çağrısının yapılması gerekir:
viewModel = new GameViewModel();
$(document).ready(function() {
   ko.applyBindings(viewModel);
});
Kullanıcının hamlelerini history dizisinde tutuyoruz. Oyuncunun hamlelerini bir tabloda göstermek için nasıl bir bağlama yapıldığına bakalım:
<!--  Moves -->
<div class="panel panel-success">
 <div class="panel-heading">
  <h3 class="panel-title">Moves</h3>
 </div>
 <div class="panel-body">
  <table class="table table-striped">
   <thead>
    <tr>
     <th>Guess</th>
     <th>Evaluation</th>
    </tr>
   </thead>
   <tbody data-bind="foreach: history">  
    <tr>
     <td data-bind="text: guess"></td>
     <td data-bind="visible: eval.win">
         <span class="label label-success" data-bind="visible: eval.win">You win!</span>
     </td>
     <td data-bind="visible: !eval.win"> 
          <span class="label label-success" 
              data-bind="visible: eval.perfectMatch>0, text: eval.perfectMatch"></span>
          <span class="label label-danger" 
              data-bind="visible: eval.partialMatch>0, text: eval.partialMatch"></span>
          <span class="label label-info" 
              data-bind="visible: eval.perfectMatch==0 && eval.partialMatch==0">No Match</span>
     </td>
    </tr>
   </tbody>
  </table>
 </div>
</div>
history dizisini data-bind="foreach: history" ile bağlıyoruz. Burada bir döngü oluşturulur. Bu döngü history dizisindeki her bir hamle için döner. Her hamle için <tr></tr> ile verilen tablo satırı dinamik olarak oluşturulur. <tr></tr> ile tablonun satırı için bir şablon verilir. Bu şablonun içinde kurulan bağlantılar ise GameViewModel ile değil history dizisindeki nesneler ile gerçekleştirilir. Knockout.js'de yukarıdaki verilen text, value, foreach gibi daha çok sayıda bağlantı etiketleri bulunur. Tüm bağlantı etiketlerine bu bağlantıdan erişebilirsiniz.
Burada dikkat edilmesi gereken önemli bir nokta bulunuyor. Değişkenleri observable olarak tanımladığımızda, onlar artık basit birer öznitelik olmaktan çıkıyorlar. Değerlerine ulaşmak ya da değerlerini değiştirmek için fonksiyon çağırır gibi çağırmamız gerekir. Örneğin tries'ın değerini bir arttırmak için aşağıdaki ifadeyi yazmamız gerekir:
self.tries( self.tries() + 1 );
Bazen observable alanları View'a bağlarken geçerli bir JS ifade içinde kullanmak gerekir. Bu durumda, yine observable alanın değerine parantez çifti ile fonksiyon çağırısı olarak erişmek gerekir:
<div class="alert alert-danger" role="alert" 
      data-bind="visible: validationMessage().length>0">
Burada validationMessage, ViewModel içinde ko.observable() olarak tanımlı bir alandır.
Şimdi oyundaki süreler ile ilgili bazı istatistikler toplamak isteyelim: kronometre, her bir seviyede hamle sayısı, tamamlama süresi, ortalama hamle süresi gibi. Bu kez bağlanacak değerlerin hesaplanarak getirilmesi gerekiyor. Bu durumda üçüncü bağlantı şeklini kullanacağız, ko.computed():
self.average = ko.computed(function(){
 if (self.tries() == 0) return self.duration();
 return self.duration()/ self.tries();
});

self.totalTime = ko.computed(function() {
 if (self.statistics().length == 0)
  return "No game played yet!";
 tt = 0.0;
 for (i in self.statistics()) {
  stat = self.statistics()[i];
  tt += Number(stat.time);
 }
 return tt;
});

self.averageTime = ko.computed(function() {
 if (self.statistics().length == 0)
  return "No game played yet!";
 return self.totalTime() / Number(self.statistics().length);

});
computed() fonksiyonuna parametre olarak bir fonksiyon veriyoruz. Javascript, fonksiyonel programlama dilidir. Dolayısı ile bunu yapmak yasal. computed() bağlantısını ilginç kılan ise, fonksiyon içinde kullanılan "observable" değerlerin herhangi biri değiştiğinde, otomatik olarak fonksiyonun yeniden çalıştırılmasıdır. Hesaplanan yeni değer, önyüzde (View) bağlandığı elemanın da değişmesini sağlayacaktır. Örneğin, self.average, self.tries ya da self.duration observable değişkenlerden herhangi biri değiştiğinde yeniden hesaplanacaktır. 
Şimdi, View ve ViewModel'in tamamına bakalım:

index.html:


<!doctype html>
<html>
<head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1"> 
   <style type="text/css">
 @import url('css/bootstrap.css');
 @import url('css/bootstrap-theme.css');
   </style>
   <script type="text/javascript" src="js/lib/jquery.js"></script>
   <script type="text/javascript" src="js/lib/knockout.js"></script>
   <script type="text/javascript" src="js/lib/bootstrap.js"></script>
   <script type="text/javascript" src="js/mastermind.js"></script>
   <title>MasterMind</title>
</head>
<body>

<div class="container" role="main">
   <!--  Game Board -->
   <div class="panel panel-primary">
 <div class="panel-heading">
  <h3 class="panel-title">Mastermind</h3>
 </div>
 <div class="panel-body">
  <div class="form-group">
   <label for="gameLevel">Game Level:</label> 
   <span data-bind="text: gameLevel"></span>
  </div>
  <div class="form-group">
   <label for="tries">Tries:</label> 
   <span data-bind="text: tries"></span>
  </div>
  <div class="form-group">
   <label for="tries">Time:</label> 
   <span class="label label-danger" data-bind="text: duration"></span>
  </div>
  <div class="form-group">
   <label for="tries">Average time:</label> 
   <span class="label label-danger" data-bind="text: average"></span>
  </div>
  <div class="form-group">
   <label for="guess">Guess:</label> 
   <input type="text" data-bind="value: guess" class="form-control" id="guess" />
  </div>
  <div class="form-group">
   <button class="btn btn-success" data-bind="click: play">Play</button>
  </div>    
 </div>
   </div>
   <!--  Moves -->
   <div class="panel panel-success">
 <div class="panel-heading">
  <h3 class="panel-title">Moves</h3>
 </div>
 <div class="panel-body">
  <table class="table table-striped">
   <thead>
      <tr>
    <th>Guess</th>
    <th>Evaluation</th>
      </tr>
   </thead>
   <tbody data-bind="foreach: history">  
      <tr>
    <td data-bind="text: guess"></td>
    <td data-bind="visible: eval.win">
                <span class="label label-success" 
                      data-bind="visible: eval.win">You win!</span>
    </td>
    <td data-bind="visible: !eval.win"> 
          <span class="label label-success" 
              data-bind="visible: eval.perfectMatch>0, text: eval.perfectMatch"></span>
          <span class="label label-danger" 
              data-bind="visible: eval.partialMatch>0, text: eval.partialMatch"></span>
          <span class="label label-info" 
              data-bind="visible: eval.perfectMatch==0 && eval.partialMatch==0">No Match</span>
    </td>
      </tr>
   </tbody>
  </table>
 </div>
 <div class="alert alert-danger" role="alert" 
      data-bind="visible: validationMessage().length>0">
  <strong data-bind="text: validationMessage"></strong>
 </div>    
    </div>
    <!--  Statistics -->
    <div class="panel panel-info">
 <div class="panel-heading">
  <h3 class="panel-title">Statistics</h3>
 </div>
 <div class="panel-body">
  <table class="table table-striped">
   <thead>
      <tr>
    <th>Level</th>
    <th>Tries</th>
    <th>Time (seconds)</th>
    <th>Average Move Time (seconds)</th>
      </tr>
   </thead>
   <tbody data-bind="foreach: statistics">  
      <tr>
    <td data-bind="text: level"></td>
    <td data-bind="text: tries"></td>
    <td data-bind="text: time"></td>
    <td data-bind="text: average"></td>
      </tr>
   </tbody>
  </table>
 </div>
    </div>
    <div class="alert alert-danger" role="alert">
      <strong>Total game time:</strong> 
      <span data-bind="text: totalTime"></span>
    </div>  
    <div class="alert alert-info" role="alert">
       <strong>Average game level time:</strong> 
       <span data-bind="text: averageTime"></span>
    </div>  
</div>  
</body>
</html>

mastermind.js:


function countDistinct(value){
   val= new String(value);
   hash = {};
   for (i = 0; i < val.length; ++i) {
       c= val.charAt(i);
       hash[c]= true;
   }
   return Object.keys(hash).length;
}

function getTimeDifference(startTime,endTime){
   return Math.round((endTime.getTime() - startTime.getTime()) / 1000);
}

function getTimeDifference(startTime){
   return Math.round((new Date().getTime() - startTime.getTime()) / 1000);
}

function getRandomDigit(min, max) {
   return Math.floor((Math.random() * (max - min + 1))) + min;
}

function createSecret(numberOfDigits) {
   var numbers = new Array();
   var candidate = new Number();
   numbers[0] = getRandomDigit(1, 9);
   for (i = 1; i < numberOfDigits; ++i) {
     do {
       candidate = getRandomDigit(0, 9);
     } while (numbers.indexOf(candidate) > -1);
     numbers[i] = candidate;
   }
   number = 0;
   for (i = 0; i < numberOfDigits; ++i)
       number = 10 * number + numbers[i];
   return number;
}

var GameLevelStat = function(level, tries, time, average) {
   var self = this;

   self.level = level;
   self.tries = tries;
   self.time = time;
   self.average = average;
}

var Move = function(guess, eval) {
   var self = this;

   self.guess = guess;
   self.eval = eval;
};

function Evaluation(perfectMatch, partialMatch, win) {
   var self = this;

   self.perfectMatch = Number(perfectMatch);
   self.partialMatch = Number(partialMatch);
   self.win = win;
};

function evaluateGuess(guess, secret, level) {
   if (guess == secret) {
      return new Move(guess, new Evaluation(level, 0, true));
   }
   secret = new String(secret);
   guess = new String(guess);
   var perfectMatch = 0;
   var partialMatch = 0;
   for (i = 0; i < secret.length; ++i) {
       for (j = 0; j < guess.length; ++j) {
          if (secret.charAt(i) == guess.charAt(j)) {
            if (i == j)
               ++perfectMatch;
            else
               ++partialMatch;
          }
       }
   }
   return new Move(guess, new Evaluation(perfectMatch, partialMatch, false));
};

var GameViewModel = function() {
   var self = this;

   self.guess = ko.observable();
   self.gameLevel = ko.observable(3);
   self.start = new Date();
   self.duration = ko.observable(0);
   self.tries = ko.observable(0);
   
   self.history = ko.observableArray([]);
   self.statistics = ko.observableArray([]);
   self.secret = createSecret(self.gameLevel());
   self.validationMessage = ko.observable("");
 
   self.average = ko.computed(function(){
     if (self.tries() == 0) return self.duration();
     return self.duration()/ self.tries();
   });
 
   self.totalTime = ko.computed(function() {
      if (self.statistics().length == 0)
         return "No game played yet!";
      tt = 0.0;
      for (i in self.statistics()) {
         stat = self.statistics()[i];
         tt += Number(stat.time);
      }
      return tt;
   });

   self.averageTime = ko.computed(function() {
     if (self.statistics().length == 0)
        return "No game played yet!";
     return self.totalTime() / Number(self.statistics().length);
   });
   
   self.validateGuess= function(){
     self.validationMessage("");
     guess = new String(self.guess());
     pattern= new RegExp('\^\\d{'+self.gameLevel()+'\}\$');
     if (!pattern.test(guess)){
        self.validationMessage("You must enter "+self.gameLevel()+"-digit integer!");
        return false;
     }
     if (countDistinct(guess) != self.gameLevel()){
        self.validationMessage("You have entered repeated digits!");
        return false;
     }
     for (i in self.history()){
         move= self.history()[i];
         if (self.guess()==move.guess){
            self.validationMessage("You have already entered "+self.guess()+"!");
            return false;
         }
     }
     return true;
   }
 
   self.init = function() {
     self.history([]);
     self.tries(0);
     self.start = new Date();
     self.secret = createSecret(self.gameLevel());
     setInterval(function() {
        now = new Date();
        self.duration( getTimeDifference(self.start) );
      }, 1000);
   };

   self.play = function() {
      if (self.validateGuess()){
      var move = evaluateGuess(self.guess(), self.secret, self.gameLevel());
      self.tries(self.tries() + 1);
      if (move.eval.win) {
         self.statistics.push(new GameLevelStat(self.gameLevel(), 
             self.tries(), self.duration(), self.duration()/self.tries()));
         self.gameLevel(self.gameLevel() + 1);
         self.init();
      }
      self.history.push(move);
    }
  }
};

viewModel = new GameViewModel();
$(document).ready(function() {
   ko.applyBindings(viewModel);
   viewModel.init();
});
Son olarak oyunun ekran görüntülerine bir göz atalım:
4. seviyede oyun ekranı
Tekrar eden basamaklar için sistemin uyarı verdiği ekran
Sistem, doğru basamakta bilinen rakam sayısını yeşil arka planda, yanlış basamakta bilinen rakam sayısını ise kırmızı arka planda gösteriyor:
Kullanıcının hamleleri ve bilgisayarın geri bildirim ekranı
Projenin tamamına bu bağlantıdan ulaşabilirsiniz. Knockout.js ve diğer MV* JS çatıları ile ilgili daha detaylı bilgi almak isterseniz Client-side and Server-side Javascript eğitimini öneririm.