image

От переводчика: этот пост — перевод оригинальной статьи Маки Чиза, опытного кодера, который не только пишет классные программы, но и демонстрирует возможности различных языков своим коллегам, как новичкам, так и профессионалам.

«Сапер» — веселая игра, многие из нас в нее играют. Может быть, вы хотите сделать «Сапера» сами?

Skillbox рекомендует: Образовательный онлайн-курс «Профессия веб-разработчик».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».
Уникальность игры в том, что она очень простая и при этом весьма увлекательная. В «Сапере» нет никакой хитрой геймплейной механики, вы просто нажимаете на квадратики, надеясь, что под ними нет мины.

Предлагаю попробовать написать «Сапера» на Processing. Это отличный инструмент, который позволяет создавать графические приложения на Java. Ссылка на него вот здесь.

Прежде чем начать, скажу, что этот туториал рассчитан на тех, кто уже знает Java. Опыт работы с Processing необязателен.

Итак, начинаем. Первый шаг — определение состояния игры. Я решил реализовать это вот так.

int gridW; // grid width
int gridH; // grid height
int numMines; // number of mines on the board
 
int[][] mines; // entry is 1 for having a mine and 0 for not
boolean[][] flags; // entry is true if you have flagged that spot
boolean[][] revealed; // entry is true if that spot is revealed

Здесь, кажется, имеет смысл все, кроме вот этого участка: int[][] mines. Почему сетка мин является целым числом, а не булевым? Дело в том, что это позволяет легко подсчитать, сколько мин находится рядом с определенной позицией.

int calcNear(int x, int y) {
  int i=0;
  for (int offsetX=-1; offsetX<=1; offsetX++) {
    for (int offsetY=-1; offsetY<=1; offsetY++) {
      i+=mines[offsetX+x][offsetY+y];
    }
  }
  return i;
}

Этот код определяет, сколько мин находится рядом с определенным участком. После того, как мы уберем исключения, мы получим что-то похожее на это:

boolean outBounds(int x,int y){
  return x<0||y<0||x>=gridW||y>=gridH;
}
 
int calcNear(int x, int y) {
  if(outBounds(x,y))return 0;
  int i=0;
  for (int offsetX=-1; offsetX<=1; offsetX++) {
    for (int offsetY=-1; offsetY<=1; offsetY++) {
      if (outBounds(offsetX+x, offsetY+y))continue;
      i+=mines[offsetX+x][offsetY+y];
    }
  }
  return i;
}

Главная задача самой игры — раскрывать квадраты, начиная с точки х, у.

void reveal(int x, int y){
  if(outBounds(x,y))return;
  if(revealed[x][y])return;
  revealed[x][y]=true;
  if(calcNear(x,y)!=0)return;
  reveal(x-1,y-1);
  reveal(x-1,y+1);
  reveal(x+1,y-1);
  reveal(x+1,y+1);
  reveal(x-1,y);
  reveal(x+1,y);
  reveal(x,y-1);
  reveal(x,y+1);
}

В итоге мы имеем следующий алгоритм:

  • Если позиция за пределами поля, end;
  • Если позиция раскрыта, end;
  • Раскрываем текущую позицию;
  • Если у нас бомба рядом с текущей позицией, end;
  • Если мы дошли до этого пункта во время выполнения функции, текущая позиция в пределах игрового поля была обнаружена и рядом с ней нет бомб, открываем смежные квадраты.

В принципе, вся остальная игра представляет собой то же самое. Полный код можно найти, перейдя по этой ссылке.

Если вы хотите узнать о визуализации, читайте дальше.

Визуализация

Здесь все должно быть понятно, но я объясню сложные части.

Итак, у нас есть переменная cellSize, определяющая количество пикселей в каждом квадрате.

void settings(){
  size(gridW*cellSize, gridH*cellSize);
}

Таким образом мы создаем поле со сторонами gridW x gridH, где размеры каждого квадрата будут равняться значению cellSize.

Затем мы возвращаем переменные в изначальное состояние.

void setup(){
  //initialize and clear the arrays
  mines=new int[gridW][gridH];
  flags=new boolean[gridW][gridH];
  revealed=new boolean[gridW][gridH];
  for(int x=0;x<gridW;x++){
    for(int y=0;y<gridH;y++){
      mines[x][y]=0;
      flags[x][y]=false;
      revealed[x][y]=false;
    }
  }
}

Для инициализации поля:
//Place numMines mines on the grid
void placeMines(){
  int i=0;
  while(i<numMines){//We don't want mines to overlap, so while loop
    int x=int(random(gridW));
    int y=int(random(gridH));
    if(mines[x][y]==1)continue;
    mines[x][y]=1;
    i++;
  }
}
//Clear the mines
void clearMines() {
  for (int x=0; x<gridW; x++) {
    for (int y=0; y<gridH; y++) {
      mines[x][y]=0;
    }
  }
}

И далее включаем реакцию на клики мышью.

//We don't want the first click to be a mine
boolean firstClick=true;
 
void mousePressed() {
  int x=int(mouseX/cellSize);
  int y=int(mouseY/cellSize);
  //Right-click is flagging or de-flagging a square
  if (mouseButton==RIGHT) {
    flags[x][y]=!flags[x][y];
    return;
  } else {//left-click
    //Avoid making the first click a mine
    if (firstClick) {
      firstClick=false;
      do {
        clearMines();
        placeMines();
      } while (calcNear(x,y)!=0);
    }
    //Check for game loss
    if (mines[x][y]!=0) {
      println("Dang!");
      exit();
    } else {//If game not lost, reveal starting from that square
      reveal(x, y);
    }
  }
}

И функция отображения, вызываем ее один раз для каждого кадра.

void draw() {
  background(0);
  //For each cell
  for (int x=0; x<gridW; x++) {
    for (int y=0; y<gridH; y++) {
      //The colors alternate for texture
      color col1=color(2*255/5);
      color col2=color(3*255/5);
 
      color txtColor=color(0);
 
      int near=calcNear(x, y);
 
      if (flags[x][y]) {
        col1=color(255, 0, 0);
        col2=color(4*255/5, 255/5, 255/5);
      } else if (revealed[x][y]) {
        col1=color(255/2);
        col2=color(255/2);
      }
 
      if (near==1)txtColor=color(255*1/4, 255*1/4, 255*3/4);
      if (near==2)txtColor=color(255*1/4, 255*3/4, 155*1/4);
      if (near==3)txtColor=color(255, 0, 0);
      if (near==4)txtColor=color(0, 0, 255);
      if (near==5)txtColor=color(255, 0, 0);
 
      boolean alternate=(x+y)%2==0;
      if (alternate) {
        fill(col2);
        stroke(col2);
      } else {
        fill(col1);
        stroke(col1);
      }
 
      //if(mines[x][y]>0){
      //fill(0,255,0);
      //stroke(0,255,0);
      //}
 
      rect(x*cellSize, y*cellSize, cellSize, cellSize);
      //If there is a mine near this spot and it is revealed
      if (near>0&&revealed[x][y]) {
        fill(txtColor);
        noStroke();
        textAlign(LEFT, TOP);
        textSize(cellSize);
        text(""+near, x*cellSize, y*cellSize);
      }
    }
  }
}

И все, вот и наш «Сапер».

Игра выглядит просто, но в целом она полностью функциональна. И помните: «Сапер» вызывает привыкание!

Skillbox рекомендует:

Комментарии (31)


  1. dlinyj
    04.10.2018 13:43
    +18

    Хотелось бы видео, как программист такое пишет за четыре минуты. С секундомером.


    1. maslyaev
      04.10.2018 13:48
      +7

      Ctrl+C ? Ctrl+V с исходников в статье


      1. dlinyj
        04.10.2018 13:58
        +4

        Это не спортивно


        1. maslyaev
          04.10.2018 14:19
          +9

          А демотивировать молодёжь — разве спортивно?
          Вот сидит начинающий прогер, втыкается в исходник и понимает, что только на понимание логики у него ушло полчаса. Задаётся резонным вопросом: а смог бы я сделать такое за 4 минуты? Нет, не смог бы. Значит, думает, я жалкий неудачник. И это, видимо, навсегда, и максимум, чего удастся достигнуть в IT — торговать мобильниками в салоне.


          1. dlinyj
            04.10.2018 14:20
            +6

            Я исключительно поэтому и спросил. Потому, что, говоря про программирование за 4 минуты — это прям демотивирует. Поэтому я и прошу видео.


          1. nikitasius
            04.10.2018 22:46

            Мне 32 года и я до сих пор не умею играть в сапера. Мне такое и за месяц не написать.


            1. maslyaev
              04.10.2018 23:28

              Мне больше, и я уже научился ;)
              Не отчаивайтесь, всё впереди.


      1. svistkovr
        04.10.2018 23:13

        Даже копипастом сделать такое нереально за 4 минуты.
        Код дан кусками и вам придётся несколько раз копировать.
        Код в блокноте не запустится сам. Вам надо развернуть окружение чтоб скомпилить код. Возможно потребуется починить зависимости/пути или установить библиотеку/фреймворк.
        Вероятно с первого раза не всё заработает как надо и автор упустил какие-то детали.


    1. Perlovich
      04.10.2018 14:04
      +7

      Что интересно, ссылка на оригинальную статью содержит слова «10-minutes»: medium.com/@mackycheese21/making-minesweeper-in-10-minutes-e4c4e810fa06

      Видимо, автор позже решил, что 10 минут — это не достаточно тру и хардкор. Хотя, разумеется, и за 10 минут это никто не напишет.


      1. dlinyj
        04.10.2018 14:22
        +3

        Ну вот пускай преподаватели этого горе университета, запишут видео, как они за десять минут пишут сапёр. Без шпаргалок и ctrl-c|ctrl-v


        1. igorp1024
          04.10.2018 21:08
          +2

          Блин, несколько секунд пытался понять что это за ROPE-университет. %)


      1. MacIn
        04.10.2018 20:43

        А заголовок статьи:
        Making Minesweeper in 4 minutes


    1. nafgne
      04.10.2018 19:01

      Вот тут можно на такое полюбоваться и немного поплакать от качества кода: www.youtube.com/user/shiffman


      1. Tortortor
        04.10.2018 19:38

        давайте ссылку на конкретный пример. иначе вас заминусуют, ой…


        1. nafgne
          04.10.2018 21:19

          Во-первых, там все видео на такую тематику, во-вторых, мало волнует.


  1. Aquahawk
    04.10.2018 13:54

    Особенно интересно что классический виндовый сапёр устроен так что при клике первый раз в закрытое поле нельзя подорваться. Он или генерит или модифицирует карту при клике.


    1. Perlovich
      04.10.2018 14:31
      +2

      Я думаю, гораздо проще генерировать поле при клике, чем модифицировать уже существующее поле.


    1. ninJo
      04.10.2018 15:21
      -1

      Я на винде играл, часто бывало что несколько раз подряд подрывался на первом клике. Еще на 95ой винде так было


      1. AlexPancho
        04.10.2018 15:39

        нет, на первом нельзя, только на втором


        1. ninJo
          04.10.2018 16:32

          Я точно помню как обламывался проигрывать с первого клика, может даже на верси 3.11 так было, не помню точно какая версия винды, но это было когда я еще был ребенком

          www.reddit.com/r/gaming/comments/8sytu3/think_you_suck_at_a_video_game_this_is_my_first


          1. AlexPancho
            04.10.2018 17:29
            +1

            однако, даже там на скриншоте счетчик ходов с цифой 4.
            Вики пишет, что были версии Спаера на геймБой где можно было подрываться на первом ходу, или просто у нас бывают ложные воспоминания.


            1. photino
              04.10.2018 21:47

              Если я правильно помню, это не счетчик ходов, а индикатор времени. Но вроде бы он запускался только после первого нажатия на поле. В общем, какая-то странная картинка.


  1. postgres
    04.10.2018 19:34

    4 минуты — это скорее всего время требуемое на прочтение этой статьи. После которых можно сказать: «Круто, но делать этого я, конечно, не буду»


    1. GlukKazan
      04.10.2018 20:29
      +1

      Почему не буду? Уже сделал. Не за 10 минут конечно. За полчаса где-то.


      1. Arris
        04.10.2018 23:20
        -1

        Иии… многому ли вы научились благодаря этой статье? :)


        1. GlukKazan
          05.10.2018 08:49

          А кто сказал, что я учился благодаря ей?


          1. Arris
            05.10.2018 10:53

            Тогда какой смысл?


  1. fogone
    05.10.2018 12:52

    Хотя за 4 минуты такое конечно не напишешь, но качество кода очень похоже на то, что пытались написать именно за 4.


  1. vadimus
    05.10.2018 15:37

    Как-то учился я на гикбейнсе на курсе JS. Нам задали домашку написать рабочую змейку. Мы написали. Преподаватель сказал, что мы написали плохо и сказал:

    — я напишу вам сейчас нормальную змейку за 40 минут.

    В итоге он потратил 2 часа, и написал меньше половины. Появилось только поле и змейка побежала.

    Правило умножения на 3 работает. Правда, ця думаю, что в случае с 4 минутами надо было умножать на 10 в реале.


    1. MacIn
      06.10.2018 19:01

      На Пи же.


  1. bask
    06.10.2018 11:40

    Из серии книг «Выучи С++ за 21 день»