Monthly Archives: Червень 2014

Прочитання коду гри 2048

От я їду в поїзді, інтернету нема, я забув скинути собі документацію з AngularJS, тому тепер не знаю чому не працює форма яку я написав по пам’яті. ng-value працює лише в одну сторону? Ах, точно, в іншу – ng-model. Тим не менш, я ще забув як визначити номер ітерації в ng-repeat, тому не можу зробити видалення зі списку. Але в мене є гра 2048 з кодом, от я й спробую її прочитати. Читати код корисно. Але ми звісно не намагатимемось розібрати все, а просто візьмемо собі за мету додати до гри штучний інтелект. :)

Код гри

Код взятий з https://github.com/gabrielecirulli/2048.git. Сommit: 6c12037b2a090ed0f1bd7ab1738637810f98da46.

Отож, гра складається з HTML5 веб-сторінки (на що вказує <!DOCTYPE html>), і завантажує наступні скрипти:

  <script src="js/animframe_polyfill.js"></script>
  <script src="js/keyboard_input_manager.js"></script>
  <script src="js/html_actuator.js"></script>
  <script src="js/grid.js"></script>
  <script src="js/tile.js"></script>
  <script src="js/local_score_manager.js"></script>
  <script src="js/game_manager.js"></script>
  <script src="js/application.js"></script>

Читати, очевидно варто починаючи з application.js. Його код короткий, тому вставимо тут повністю:

// Wait till the browser is ready to render the game (avoids glitches)
window.requestAnimationFrame(function () {
  window.game = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalScoreManager);
});

Насправді window.game в оригінальній грі не створювалась, але я її записав щоб мати можливість подосліджувати цей об’єкт за допомогою Firebug.

Додаємо нову клавішу

Клас KeyboardInputManager зберігається в файлі keyboard_input_manager.js і відповідає за ввід (не тільки з клавіатури, а й всіма іншими можливими способами. В тому файлі також можна знайти цікавий словник, що відображає коди клавіш в напрями, і звідки ми можемо дізнатись що 0 – це вверх, 1 – вправо, 2 – вниз і 3 – вліво. Радує те що є також можливість командувати рухами за допомогою клавіш Vim:

  var map = {
    38: 0, // Up
    39: 1, // Right
    40: 2, // Down
    37: 3, // Left
    75: 0, // vim keybindings
    76: 1,
    74: 2,
    72: 3,
    87: 0, // W
    68: 1, // D
    83: 2, // S
    65: 3  // A
  };

Всередині класу GameManager (з файлу game_manager.js), KeyboardInputManager використовується наступним чином:

  this.inputManager = new InputManager;

  ...

  this.inputManager.on("move", this.move.bind(this));
  this.inputManager.on("restart", this.restart.bind(this));
  this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));

Тобто є лише три події, які виконують три методи. Найцікавіший, звісно, метод move, тому що скоріш за все ним, ми змусимо наш штучний інтелект керувати грою. Давайте створимо для його ходу заглушку, і змусимо її виконуватись при натисненні Enter. В keyboard_input_manager.js є обробник натиснення клавіші з наступним кодом:

  document.addEventListener("keydown", function (event) {
      ...
      if (event.which === 32) self.restart.bind(self)(event);

Що означає “при натисненні прогалика – виконати self.restart()“. Додамо туди наступний рядок:

      if (event.which === 13) self.run_ai.bind(self)(event);

Тепер треба додати відповідну функцію:

KeyboardInputManager.prototype.run_ai = function (event) {
  event.preventDefault();
  this.emit("run_ai");
};

Тепер в нашому GameManager можна підписатись на подію:

  this.inputManager.on("run_ai", this.run_ai.bind(this));

Тепер давайте опишемо наш штучний інтелект в функції run_ai:

GameManager.prototype.run_ai = function () {
    this.move(this.random_choice([0, 1, 2, 3]));
};
GameManager.prototype.random_choice = function(items) {
    return items[Math.floor(Math.random() * items.length)];
};

Вона просто робить хід випадковим чином. Дуже непогана стратегія, можна дуже швидко скласти клітинку на 32, тепер можна було б подумати про вдосконалення стратегії.

Копіювання поля

Для цього просто треба вибирати хід хоч трохи кращий ніж випадковий. Давайте почнемо з того, що будемо обирати той хід, після якого на полі залишається менше чисел (тобто той, при якому найбільша кількість чисел об’єднуються). Для цього треба подивитись де то поле зберігається, зробити копію поля, спробувати ходи і оцінити ситуацію.

Переглядаючи код бачимо що щось схоже на поле створюється наступним кодом:

GameManager.prototype.setup = function () {
  this.grid        = new Grid(this.size);

Ліземо в grid.js.

function Grid(size) {
  this.size = size;
  this.cells = [];
  this.build();
}

// Build a grid of the specified size
Grid.prototype.build = function () {
  for (var x = 0; x < this.size; x++) {
    var row = this.cells[x] = [];

    for (var y = 0; y < this.size; y++) {
      row.push(null);
    }
  }
};

Бачимо що поле має розмір і клітинки. Клітинки це масив масивів null, де null напевне позначає порожні клітинки. І справді, якщо подивитись значення game.grid.cells в Firebug, можна побачити таку структуру:

[[Tile { x=0, y=0, value=2}, null, null, null], [null, null, null, null], [null, null, null, null], [Tile { x=3, y=0, value=2}, null, null, null]]

Якщо продивитись код grid.js трохи далі, можна помітити метод

Grid.prototype.availableCells = function () {
  var cells = [];

  this.eachCell(function (x, y, tile) {
    if (!tile) {
      cells.push({ x: x, y: y });
    }
  });

  return cells;
};

Він повертає нам масив всіх координат які ще не зайняті. Чим їх більше, тим краще, і це ми використаємо для оцінки позиції. Також це показує нам як користуватись методом eachCell, який ми використаємо для того щоб робити копію поля:

Grid.prototype.copy = function () {
    var grid = new Grid(this.size);

    this.eachCell(function (x, y, tile) {
        grid.cells[x][y] = tile;
    });
    return grid;
};

Тепер, функція яка перевіряє скільки буде доступно вільних клітинок на полі, при цьому самого поля не чіпаючи:

GameManager.prototype.try_direction = function(direction) {
        var grid_copy = this.grid.copy();
        this.move(direction);
        var cells_available = this.grid.availableCells().length;
        this.grid = grid_copy;
        return cells_available;
};

Правда при тестах виявляється, що поле вона все таки чіпає. Можливо тому що переміщення чисел по полю відбуваються асинхронно, і коли ми перевіряємо хід і потім відновлюємо початковий стан поля ще не всі числа перемістились? А може тому що треба робити глибшу копію об’єкту Grid, роблячи також копії всіх об’єктів Tile? Друге – простіше пояснення, і варто було б перевірити спершу його, але поїзд вже прибуває.


Filed under: Кодерство Tagged: JavaScript

Кулінкулятор

2014-06-02_23-59-43_642Написав калькулятор для обчислення енергетичної цінності, ціни і терміну зберігання страв що створюються з набору наперед відомих інгредієнтів за всілякими рецептами.

Наприклад рецепт на одну дію:

> print(кукурудза + тунець)
Кукурудза ніжна вакуумована стерилізована і
Консерви рибні. Тунець подрібнений стерилізований.
Ціна: 32.48 грн. Вихід: 470 грам
Енергетична цінність в 100 г продукту: 487.6 кДж
білки - 8.34 г, жири - 1.54 г, вуглеводи 15.79 г. 
Після відкриття зберігати не більше ніж: 48 год

Кожен продукт окрім додавання ще можна множини на скаляр. Але інгредієнти звісно треба задати наперед:

кукурудза = Product(
    bar_code=4824024004211,
    name='Кукурудза ніжна вакуумована стерилізована',
    producer='Бондюель',
    country='HU', 

    price=12.49,
    amount=340, # g
    
    # g per 100g:
    proteins=3.1,
    carbohydrates=21.8,
    fats=1.6,
    energy=505000, # joules

    opening_shelf_life=48, # hours
)

тунець = Product(
    bar_code=4824024002439,
    name='Консерви рибні. Тунець подрібнений стерилізований.',
    producer='Chotiwat manufacturing',
    country='TH', 

    price=19.99,
    amount=130, # g

    # per 100g:
    proteins=22.06,
    carbohydrates=0.07,
    fats=1.4,
    energy=442000, # joules

    opening_shelf_life=48, # hours
)

Але ідеальним для мене буде сайт рецептів, на якому писатиме: макарони в твоєму холодильнику пора буде викинути через 5 годин, тому сьогодні на вечерю ти їстимеш їх. Для повного денного раціону тобі бракує ще 500 грам яблук, найшвидше їх купити буде на Стрийському ринку де вони вчора коштували 8 грн/кг, тому очікуємо що це коштуватиме тобі сьогодні 4 грн.

Або хоча б на вікіпедії й на вікіпідручнику завести такі таблички, щоб кулінарити було легше.


Filed under: Кодерство Tagged: Лайфхаки, Python