Как написать морской бой на javascript

Вступление.

Игру «Морской бой», думаю, представлять не надо. Вряд ли найдётся хоть один человек, который хотя бы раз не играл в неё. Давайте и мы создадим полноценный «Морской бой», используя HTML-вёрстку для отображения структуры игры на экране монитора, визуально оформим её с помощью таблицы стилей, а с помощью чистого JavaScript напишем «движок», определяющий поведение игры.

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

Как говорится — лучше один раз увидеть. На приведённом ниже скриншоте прекрасно видно, что представляет из себя игровое поле, сколько типов кораблей, в зависимости от количества палуб будет в эскадре, как выглядит промах, попадание и отметка, где корабля точно быть не может.

Игра Морской бой. Пример игрового поля во время игры.

Надеюсь всё всем понятно. Ну а теперь составим техническое задание, чтобы ничего не забыть и не пропустить.

Это очень важно.
При создании игры использовался JavaScript версии ES6/ES2015 и более поздних версий. Для понимания изложенного материала, необходимо предварительно изучить изменения и нововведения в версиях ES2015+.

Техническое задание на создание игры «Морской бой»

  • 1

    Количество и типы кораблей, их расположение.

    Как и в реальной игре, эскадра будет состоять из:
    — одного четырёхпалубного;
    — двух трёхпалубных;
    — трёх двухпалубных;
    — четырёх однопалубных кораблей.
    Корабли могут располагаться вертикально и горизонтально, но при этом между кораблями должна быть хотя бы одна пустая клетка, в том числе и по диагонали. Корабли не могут иметь Г-образную форму.

  • 2

    Расстановка кораблей и редактирование их положения.

    Корабли игрока могут расставляться по случайному закону или самим игроком, путём перетаскивания их на игровое поле. И в первом, и во втором случае можно редактировать направление положения палуб (вертикальное или горизонтальное), путём клика правой кнопкой мышки по первой палубе и расположение, путём перетаскивания. Первая палуба — при горизонтальном расположении корабля — самая левая, при вертикальном — самая верхняя.
    При перетаскивании или повороте корабля скрипт постоянно отслеживает корректность координат корабля. Если корабль будет соприкасаться с соседним или выходить за пределы игрового поля, то рамка корабля окрасится в красный цвет, а корабль, после отпускания кнопки мышки, вернётся в исходное положение. В противном случае, рамка корабля будет зелёная.
    Нет необходимости пытаться установить корабль точно по границам клеток — программа сама откорректирует его положение, установив в ближайшие валидные координаты.
    В момент начала игры, редактирование положения кораблей отключается.

  • 3

    Ведение морского боя

    Если сражение первое, рандомно определяем, кто стреляет первым. Далее, первым стреляет победитель.
    Координаты выстрела передаются путём клика по выбранной клетке — ввода координат с клавиатуры не будет.
    Попадание отмечается красным крестиком, промах — точкой, клетку, где корабля точно быть не может, отмечаем / снимаем отметку кликом правой кнопкой мышки.
    Сообщений «ранил», «убил / потопил» не будет — самостоятельно определяем тип корабля.
    Под игровыми полями выводятся сообщения о том, чей выстрел и результат выстрела.

  • 4

    Искусственный интеллект противника.

    Для ведения морского боя, компьютер должен обладать хотя бы примитивным искусственным интеллектом. Он должен уметь:
    — формировать координаты выстрела, согласно заложенного алгоритма ведения морского боя.
    — при попадании в палубу корабля, продолжить его обстрел, пока корабль не будет потоплен.
    — отмечать клетки, куда стрелять не имеет смысла и исключать координаты этих клеток из матрицы возможных выстрелов.

    На этих требования к ИИ мы пока и остановимся, хотя можно рассмотреть варианты размещения кораблей компьютером, при которых сильно уменьшается вероятность попадания в однопалубные корабли.

  • 5

    Окончание морского боя.

    Выводится сообщение о победителе. Если выиграл компьютер, то отображаются его не потопленные корабли.

HTML-вёрстка для игры «Морской бой».

При создании HTML страницы, наша главная задача — воспроизвести внешний вид игрового поля морского боя максимально похожий на тот, к которому вы привыкли, играя на тетрадочных листах. Разметка HTML не сложная и будет включать в себя следующие основные блоки:

  1. Основной родительский элемент, background которого похож на лист тетрадки в клетку.
  2. Два игровых поля — игрока, где мы будем расставлять свои корабли и контролировать выстрелы компьютера, и компьютера, где мы будут отмечаться наши попадания и промахи. Им задаётся background в виде рамки размером 10х10 клеток с буквенным обозначением строк и цифровым обозначением колонок.
  3. Контейнер с инструкцией по ручной расстановке кораблей и набором кораблей, которые необходимо перетащить на своё игровое поле.
  4. Два блока, верхний и нижний, для вывода информационных сообщений.
  5. Кнопка запуска игры.

Первоначально, при загрузке страницы, в контейнере инструкции отображаются только элементы для выбора способа размещения кораблей:
— рандомное размещение при помощи js-скрипта;
— самостоятельное размещение игроком.
Кнопка запуска игры изначально не видна. Она появится только после того, как игрок разместит свои корабли.

Исходный код разметки HTML:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

<div class=«wrap»>

<div class=«battlefield»>

<div id=«text_top» class=«flex text-top»>Расстановка кораблей</div>

<div class=«flex outer»>

<!— playing field human —>

<div class=«field field-human»>

<div id=«field_human» class=«ships»></div>

</div>

<!— /playing field human —>

<!— playing field computer —>

<div class=«field field-computer» hidden>

<div id=«field_computer» class=«ships»></div>

</div>

<!— /playing field computer —>

<!— instruction —>

<div id=«instruction» class=«instruction»>

<div id=«type_placement» class=«type-placement-box»>

1. <span class=«link» data-target=«random»>Случайным образом</span><br>

2. <span class=«link» data-target=«manually»>Методом перетаскивания.</span>

</div>

<div id=«ships_collection» class=«ships-collection» hidden>

<p>Перетащите мышкой корабли на игровое поле. Для установки корабля по вертикали, кликните по нему правой кнопкой мышки.</p>

</div>

</div>

<!— /instruction —>

</div>

<div class=«service-row»>

<div id=«service_text» class=«service-text»></div>

<button id=«play» type=«button» class=«btn-play» hidden>Играть</button></button>

<button id=«newgame» type=«button» class=«btn-play btn-newgame» hidden>Продолжить</button>

</div>

</div>

</div>

<ul class=«initial-ships» hidden>

<li>

<div id=«fourdeck1» class=«ship fourdeck»></div>

<div id=«tripledeck1» class=«ship tripledeck tripledeck1»></div>

<div id=«tripledeck2» class=«ship tripledeck tripledeck2»></div>

</li>

<li>

<div id=«doubledeck1» class=«ship doubledeck»></div>

<div id=«doubledeck2» class=«ship doubledeck doubledeck2»></div>

<div id=«doubledeck3» class=«ship doubledeck doubledeck3»></div>

</li>

<li>

<div id=«singledeck1» class=«ship singledeck»></div>

<div id=«singledeck2» class=«ship singledeck singledeck2»></div>

<div id=«singledeck3» class=«ship singledeck singledeck3»></div>

<div id=«singledeck4» class=«ship singledeck singledeck4»></div>

</li>

</ul>

Самый сложный блок, как вы заметили, это набор кораблей.

Таблица стилей для игры «Морской бой».

Полностью всю таблицу стилей приводить здесь не буду, вы можете скачать её и спрайт с картинками в архиве. Представлю только стили, непосредственно определяющие стилевое оформление игры «Морской бой»:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

/* спрайт с контурами кораблей, маркерами промахов и попаданий, кнопкой запуска игры */

.ship, .icon-field, .btn-play {

background: url(‘img/sprite.png’) no-repeat;

}

/* игровое поле находится на середине страницы и имеет фон тетрадного листа в клеточку */

.battlefield {

width: 860px;

height: 530px;

position: relative;

margin: 0 auto;

background: url(‘img/grid.png’) repeat;

}

.outer {

margin-bottom: 21px;

}

/* Фоном данного блока является рамка игрового поля с буквами, обозначающими

строки и цифрами, обозначающими столбцы. Блок позиционирован так, что рамка совпадает

с клетками тетрадного листа. */

.field {

width: 366px;

height: 363px;

position: relative;

background: url(‘img/bg_play_field.png’) no-repeat;

}

.field-computer {

margin-left: 31px;

}

/* в этом блоке с помощью абсолютного позиционирование размещаются корабли эскадры,

координаты их первых палуб будут отсчитываться от его левого верхнего угла

блок позиционирован таким образом, чтобы точно совпадать с клетками фона */

.ships {

width: 330px;

height: 330px;

position: relative;

left: 29px;

top: 27px;

}

/* блоки для вывода сообщений */

.text-top {

height: 66px;

font-size: 22px;

line-height: 66px;

text-align: center;

margin-bottom: 7px;

}

.text-btm {

color: #c00;

text-align: center;

padding-top: 10px;

}

/* кнопки запуска и продолжения игры */

.btn-play {

width: 144px;

height: 45px;

font-family: SegoePrint;

font-size: 24px;

line-height: 40px;

color: #4530af;

text-align: center;

background-position: 0 -150px;

cursor: pointer;

}

.btn-newgame {

font-size: 19px;

margin-top: 10px;

}

/* визуальное отображение кораблей на игровом поле */

.ship {

height: 35px;

position: absolute;

}

.fourdeck {

width: 134px; background-position: 0 0; /* синий */

}

.fourdeck.success {

background-position: 0 -50px; /* зелёный */

}

.fourdeck.unsuccess {

background-position: 0 -100px; /* красный */

}

.tripledeck {

width: 101px;

background-position: -150px 0;

}

.tripledeck.success {

background-position: -150px -50px;

}

.tripledeck.unsuccess {

background-position: -150px -100px;

}

.doubledeck {

width: 68px;

background-position: -270px 0;

}

.doubledeck.success {

background-position: -270px -50px;

}

.doubledeck.unsuccess {

background-position: -270px -100px;

}

.singledeck {

width: 35px;

background-position: -360px 0;

}

.singledeck.success {

background-position: -360px -50px;

}

.singledeck.unsuccess {

background-position: -360px -100px;

}

/* отображение и позиционирование набора кораблей для самостоятельной расстановки */

.initial-ships li {

height: 35px;

position: relative;

overflow: hidden;

margin-top: 31px;

}

.initial-ships .ship {

left: 0;

top: 0;

float: left;

cursor: move;

}

.initial-ships .tripledeck1 {

left: 164px;

}

.initial-ships .tripledeck2 {

left: 297px;

}

.initial-ships .doubledeck2 {

left: 99px;

}

.initial-ships .doubledeck3 {

left: 197px;

}

.initial-ships .singledeck2 {

left: 65px;

}

.initial-ships .singledeck3 {

left: 131px;

}

.initial-ships .singledeck4 {

left: 197px;

}

/* стиль для вертикального положения корабля */

.vertical {

transform: rotate(90deg);

transform-origin: 17.5px 17.5px;

}

/* оформление блока инструкции */

.instruction {

margin-left: 430px;

-webkit-user-select: none;

user-select: none;

}

.type-placement-box {

line-height: 34px;

padding-bottom: 18px;

}

.type-placement-box .link {

font-size: 20px;

border-bottom: dashed 2px #4530af;

cursor: pointer;

}

/* маркеры */

.icon-field {

width: 33px;

height: 33px;

position: absolute;

z-index: 5;

}

/* маркер промаха */

.dot {

background-position: -410px 0;

}

/* маркер попадания */

.red-cross {

background-position: -410px -50px;

}

/* маркер клетки, где корабля быть не может */

.shaded-cell {

background-position: -410px -100px;

}

/* красный маркер клетки, где корабля быть не может */

.shaded-cell_red {

background-position: -410px -140px;

}

/* иконка взрыва */

.explosion {

background-position: -150px -150px;

z-index: 6;

opacity: 0;

transform: scale(.2);

}

.explosion.active {

animation-name: Explosion;

animation-duration: 0.4s;

transform: scale(1);

}

@keyframes Explosion {

0% {

opacity: 0;

transform: scale(.2);

}

50% {

opacity: 1;

transform: scale(1);

}

100% {

opacity: 0;

}

}

Начинаем писать Javascript для игры «Морской бой».

Весь наш код разместим в анонимной самозапускающейся функции для ограничения области видимости:

;(function() {

‘use strict’;

})();

При написании js-скрипта мы будем использовать классы. Почему выбран такой стиль программирования?
У нас есть два игровых поля — игрока и компьютера. Эти поля совершенно идентичны. У них есть ряд одинаковых свойств и одинаковые методы, с помощью которых можно получить координаты кораблей и разместить их на этих полях. Кроме игровых полей, у нас есть множество объектов кораблей, которые также обладают рядом одинаковых свойств и методов.

Для создания выше перечисленных объектов мы будем использовать конструкторы классов, т. к. они хорошо подходят для создания экземпляров похожих объектов, имеющих одинаковую структуру, с общими именами свойств и методов.

Игра «Морской бой». Глобальные переменные и функции.

Прежде чем рассматривать классы и их функции-конструкторы, определим ряд переменных и функций, которые будут глобальными в пределах области видимости анонимной самозапускающейся функции.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

// флаг начала игры, устанавливается после нажатия кнопки ‘Play’ и запрещает

// редактирование положения кораблей

let startGame = false;

// флаг установки обработчиков событий ручного размещения кораблей и

// редактирование их положения

let isHandlerPlacement = false;

// флаг установки обработчиков событий ведения морского боя

let isHandlerController = false;

// флаг, блокирующий действия игрока во время выстрела компьютера

let compShot = false;

// получаем объект элемента DOM по его ID

const getElement = id => document.getElementById(id);

// пересчитаем координаты относительно документа, для этого

// добавим величину прокрутки документа по вертикали и горизонтали

// Если вы расположили игровые поля в верхней части страницы и уверенны,

// что для их отображения прокручивать страницу не потребуется, то

// полученные координаты можно не преобразовывать

const getCoordinates = el => {

const coords = el.getBoundingClientRect();

return {

left: coords.left + window.pageXOffset,

right: coords.right + window.pageXOffset,

top: coords.top + window.pageYOffset,

bottom: coords.bottom + window.pageYOffset

};

};

// игровое поле игрока

const humanfield = getElement(‘field_human’);

// игровое поле компьютера

const computerfield = getElement(‘field_computer’);

Игра «Морской бой». Конструктор игровых полей.

Для создания игровых полей используем класс Field. Аргументом функции-конструктора класса будет объект игрового поля: или игрока, или компьютера. Других аргументов конструктору не требуется. В результате мы получим новые экземпляры игровых полей, дополненные новыми свойствами, которые будут использовать методы класса,+ отвечающие за расстановку кораблей.

Рассмотрим подробно ряд констант, используемых в классе, а также функцию-конструктор класса и её свойства, которые будут наследоваться созданными ею объектами human и computer:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

class Field {

// размер стороны игрового поля в px

static FIELD_SIDE = 330;

// размер палубы корабля в px

static SHIP_SIDE = 33;

// объект с данными кораблей

// ключом будет являться тип корабля, а значением — массив,

// первый элемент которого указывает кол-во кораблей данного типа,

// второй элемент указывает кол-во палуб у корабля данного типа

static SHIP_DATA = {

fourdeck: [1, 4],

tripledeck: [2, 3],

doubledeck: [3, 2],

singledeck: [4, 1]

};

constructor(field) {

// объект игрового поля, полученный в качестве аргумента

this.field = field;

// создаём пустой объект, куда будем заносить данные по каждому созданному кораблю

// эскадры, подробно эти данные рассмотрим при создании объектов кораблей

this.squadron = {};

// двумерный массив, в который заносятся координаты кораблей, а в ходе морского

// боя, координаты попаданий, промахов и заведомо пустых клеток

this.matrix = [];

// получаем координаты всех четырёх сторон рамки игрового поля относительно начала

// document, с учётом возможной прокрутки по вертикали

let { left, right, top, bottom } = getCoordinates(this.field);

this.fieldLeft = left;

this.fieldRight = right;

this.fieldTop = top;

this.fieldBottom = bottom;

}

}

На данном этапе, создаём только экземпляр поля игрока — human. Объект computer создадим с помощью конструктора позже, когда будем расставлять корабли компьютера.
Предварительно получим объекты ряда элементов, которые будут использоваться при подготовке к игре «Морской бой» и в процессе самой игры.

Запишем в самый конец анонимной самозапускающейся функции следующий код:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

// родительский контейнер с инструкцией

const instruction = getElement(‘instruction’);

// контейнер, в котором будут размещаться корабли, предназначенные для перетаскивания

// на игровое поле

const shipsCollection = getElement(‘ships_collection’);

// контейнер с набором кораблей, предназначенных для перетаскивания

// на игровое поле

const initialShips = document.querySelector(‘.wrap + .initial-ships’);

// контейнер с заголовком

const toptext = getElement(‘text_top’);

// кнопка начала игры

const buttonPlay = getElement(‘play’);

// кнопка перезапуска игры

const buttonNewGame = getElement(‘newgame’);

// получаем экземпляр игрового поля игрока

const humanfield = getElement(‘field_human’);

const human = new Field(humanfield);

// экземпляр игрового поля компьютера только регистрируем

const computerfield = getElement(‘field_computer’);

let computer = {};

Игра «Морской бой». Рандомная расстановка кораблей в игре.

Игра «Морской бой». Алгоритм расстановки кораблей шаг за шагом.

Прежде, чем начать писать код, отвечающий за рандомную расстановку кораблей на игровом поле, предварительно рассмотрим алгоритм расстановки по шагам:

  • 1

    У нас есть два интерактивных элемента, клик по которым запускает js-скрипт расстановки кораблей — рандомную или самостоятельную. Чтобы не вешать два обработчика события, воспользуемся делегированием и с помощью функции addEventListener вешаем один обработчик на родителя этих элементов. Родительским элементом является:

    <div id=«type_placement» class=«type-placement-box»></div>

    Запомните, это очень важно.
    Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы <span>, <button>, <div>, <li>. Именно на них вешаются обработчики событий.
    Не используйте для этой цели тег <a>. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.

    Используя метод делегирования событий определяем, по какому элементу был сделан клик и считываем значение его атрибута data-target. Если это значение равно random, то вызываем функцию randomLocationShips.

  • 2

    Функции randomLocationShips перебирает статичный объект SHIP_DATA с данными кораблей.

    При каждой итерации создаются координаты первой палубы корабля и направление расположений палуб. Зная начальные координаты и ориентацию корабля, в дальнейшем мы сможем сгенерировать координаты остальных палуб, которые будут находиться в соседних столбцах при горизонтальном расположении или соседних строках — при вертикальном.

  • 3

    Проверяем полученные координаты с помощью функции checkLocationShip. Зная координаты первой палубы и направление расположения палуб (горизонтальное или вертикальное), получаем координаты остальных палуб. При этом проверяем три условия:

    1. в полученных координатах не должны располагаться палубы ранее созданного корабля;
    2. в соседних клетках, включая диагональные, не должны находиться палубы ранее созданного корабля;
    3. корабль не должен выходить за пределы игрового поля.

    Если какое-то из этих условий не выполняется, снова вызываем функцию randomLocationShips.

  • 4

    Создаём экземпляр объекта корабля, используя конструктор кораблей класса Ships. Заносим координаты палуб корабля в двумерный массив объекта human и объект squadron, в котором содержаться данные по каждому кораблю эскадры.

Игра «Морской бой». Обработчик события запуска генерации начальных координат кораблей.

Ещё раз обратим внимание на следующую HTML-разметку:

<div id=«type_placement» class=«type-placement-box»>

1. <span class=«link» data-target=«random»>Случайным образом</span><br>

2. <span class=«link» data-target=«manually»>Самостоятельно с чистого листа.</span>

</div>

Как было написано выше, обработчик события повесим на <div> с id="type_placement".

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

getElement(‘type_placement’).addEventListener(‘click’, function(e) {

// используем делегирование основанное на всплытии событий

if (e.target.tagName != ‘SPAN’) return;

// если мы уже создали эскадру ранее, то видна кнопка начала игры

// скроем её на время повторной расстановки кораблей

buttonPlay.hidden = true;

// очищаем игровое поле игрока, если уже была попытка расставить корабли

human.cleanField();

// способ расстановки кораблей на игровом поле

const type = e.target.dataset.target;

// создаём литеральный объект typeGeneration

// каждому свойству литерального объекта соответствует функция

// в которой вызывается рандомная или ручная расстановка кораблей

const typeGeneration = {

random() {

// скрываем контейнер с кораблями, предназначенными для перетаскивания

// на игровое поле

shipsCollection.hidden = true;

// вызов ф-ии рандомно расставляющей корабли для экземпляра игрока

human.randomLocationShips();

},

manually() {

// этот код мы рассмотрим, когда будем реализовывать

// расстановку кораблей перетаскиванием на игровое поле

...

}

};

// вызов функции литерального объекта в зависимости

// от способа расстановки кораблей

typeGeneration[type]();

});

Игра «Морской бой». Очистка игрового поля от ранее расставленных кораблей.

Прежде чем начать размещение кораблей (неважно, каким способом), предварительно необходимо очистить игровое поле и ряд объектов и массивов. За эту операцию отвечает функция cleanField, являющаяся методом класса Field.
Данная функция выполняет следующие задачи:

  1. удаление ранее установленных кораблей с игрового поля;
  2. очистка массива squadron от записанных в него объектов кораблей;
  3. заполняем матрицу игрового поля нулями, что соотвествует пустому месту.

В дальнейшем, эта функция будет использоваться и для очистки игрового поля компьютера. Код функции:

cleanField() {

// удаляем все объекты с игрового поля

while (this.field.firstChild) {

this.field.removeChild(this.field.firstChild);

}

// удаляем всё элементы объекта эскадры

this.squadron = {};

// заполняем матрицу игрового поля нулями

this.matrix = Field.createMatrix();

}

Теперь, при попытке создать новую эскадру с более оптимальным расположением кораблей, конфликтов со старой эскадрой не возникнет.

Статичная функция createMatrix формирует двумерный массив и заполняет его нулями. В этот массив и будут записываться координаты кораблей, а в дальнейшем координаты попаданий, промахов и отметки заведомо пустых клеток. Код этой функции несложный и в комментариях не нуждается:

static createMatrix() {

return [...Array(10)].map(() => Array(10).fill(0));

}

В дальнейшем, все статичные методы будем записывать сразу после функции-конструктора класса, а статичные свойства — перед конструктором.

Игра «Морской бой». Генерация начальных координат кораблей.

В данный момент, функция randomLocationShips вызывается, как метод экземпляра human. В дальнейшем, при расстановке кораблей компьютера, она будет вызываться, как метод экземпляра computer.
Как было сказано ранее, функция запускает перебор объекта SHIP_DATA с данными кораблей по каждому типу и создаёт экземпляр каждого корабля с заданными свойствами, используя для этого класс Ships.

randomLocationShips() {

for (let type in Field.SHIP_DATA) {

// кол-во кораблей данного типа

let count = Field.SHIP_DATA[type][0];

// кол-во палуб у корабля данного типа

let decks = Field.SHIP_DATA[type][1];

}

}

Внутри этого цикла создадим ещё один, вложенный, цикл. Количество итераций вложенного цикла равно количеству кораблей текущего типа. Внутри этого цикла, при каждой итерации будем получать координаты первой палубы и направление расположения палуб с помощью функции getCoordinatesDecks. Аргументом данной функции будет количество палуб создаваемого корабля. Также сформируем уникального имя корабля, которое будет использоваться, как значение его атрибута id.

Теперь полный код функции randomLocationShips выглядит так:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

randomLocationShips() {

for (let type in Field.SHIP_DATA) {

// кол-во кораблей данного типа

let count = Field.SHIP_DATA[type][0];

// кол-во палуб у корабля данного типа

let decks = Field.SHIP_DATA[type][1];

// прокручиваем кол-во кораблей

for (let i = 0; i < count; i++) {

// получаем координаты первой палубы и направление расположения палуб

let options = this.getCoordsDecks(decks);

// кол-во палуб

options.decks = decks;

// имя корабля, понадобится в дальнейшем для его идентификации

options.shipname = type + String(i + 1);

// создаём экземпляр корабля со свойствами, указанными в

// объекте options

const ship = new Ships(this, options);

ship.createShip();

}

}

}

Прежде чем приступать к функции getCoordinatesDecks, которая генерирует координаты первой палубы корабля и направление расположения его палуб, подумаем, какие ограничения мы должны наложить на диапазон создаваемых координат. Рассмотрим это на примере трёхпалубного корабля.

Запомните, это очень важно.
Данные о координатах корабля будут храниться в матрице (двумерном массиве), поэтому вместо буквенно-цифровых координат будут формироваться только цифровые. Отсчёт элементов массива начинается с 0. Строки матрицы — это координата ‘X’, а столбцы — координата ‘Y’.
Например, координата «Б3» в двумерном массиве будет иметь значение [1,2]. Как видно, координаты палубы корабля есть не что иное, как индексы элемента в двумерном массиве.

Игра Морской бой. Ограничения на координаты первой палубы.

Разобравшись, как накладываются ограничения на формирование координат, мы можем получить зависимость максимальной координаты от количества палуб корабля. Для координаты ‘Y’, при горизонтальном размещении корабля, это будет выглядеть так:
y = (9 - decks) + 1 или короче y = 10 - decks, где decks — количество палуб у корабля.
При этом, координата ‘X’ может принимать любое значение в диапазоне от 0 до 9.

Аналогичные ограничения накладываются и при вертикальном расположении корабля.

Так как координаты формируются рандомно, давайте напишем небольшую статичную функцию, которая будет генерировать случайные числа. С помощью аргумента, будем определять диапазон, в котором будут лежать полученные значения:

// n — максимальное значение, которое хотим получить

static getRandom = n => Math.floor(Math.random() * (n + 1));

Рассмотрим по шагам, как работает функция getCoordinatesDecks:

  • 1

    Случайным образом определяем направление расположения корабля, присваивая значения специальным коэффициентам kx и ky. Если kx == 0 и ky == 1 — корабль расположен горизонтально, если kx == 1 и ky == 0, то вертикально.

  • 2

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

  • 3

    Вызываем функцию проверки полученных координат первой палубы. Если координаты не валидны, то через рекурсию опять вызываем функцию getCoordinatesDecks.

  • 4

    Возвращаем объект, в котором содержаться значения координат x и y, а также коэффициентов kx и ky.

Теперь представим функцию getCoordinatesDecks в её полном виде:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

getCoordsDecks(decks) {

// получаем коэффициенты определяющие направление расположения корабля

// kx == 0 и ky == 1 — корабль расположен горизонтально,

// kx == 1 и ky == 0 — вертикально.

let kx = Field.getRandom(1), ky = (kx == 0) ? 1 : 0,

x, y;

// в зависимости от направления расположения, генерируем

// начальные координаты

if (kx == 0) {

x = Field.getRandom(9); y = Field.getRandom(10 decks);

} else {

x = Field.getRandom(10 decks); y = Field.getRandom(9);

}

const obj = {x, y, kx, ky}

// проверяем валидность координат всех палуб корабля

const result = this.checkLocationShip(obj, decks);

// если координаты невалидны, снова запускаем функцию

if (!result) return this.getCoordsDecks(decks);

return obj;

}

Данная функция также является методом класса Field.

Игра «Морской бой». Валидация координат всех палуб корабля.

Для проверки корректности координат палуб корабля используется функция checkLocationShip, аргументами которой являются начальные координаты корабля и направление расположения палуб.
Чтобы было более понятно, как работает данная функция, проиллюстрируем это:

Игра Морской бой. Отображение координат требующих проверки.

Как видно на приведённом рисунке, проверяется группа координат, совпадающая с координатами палуб корабля и прилегающих к ним клеток. Эта группа может быть меньше, если корабль примыкает к одной или сразу двум границам игрового поля. Значение проверяемых координат берётся из матрицы игрового поля, принадлежащей объекту, от которого в данный момент наследуется функция getCoordinatesDecks. Это может быть или поле игрока, или поле компьютера. Напоминаю, что в данный момент мы рассматриваем рандомную расстановку кораблей для игры «Морской бой» на поле игрока.
Полученное из матрицы значение сравнивается с 1, т. е. в проверяемой клетке есть палуба ранее установленного корабля.

Проверка реализована с помощью встроенной функции filter. Сначала из матрицы берётся массив координат по оси Х. Его границы определяются значениями fromX и toX. Далее этот массив ограничивается по оси Y — границы определяются значениями fromY и toY. Значение каждого элемента получившегося двумерного массива с помощью функции filter сравнивается с 1.
Если найдутся элементы содержащие 1, то функция checkLocationShip вернёт false, в противном случае — true.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

checkLocationShip(obj, decks) {

let { x, y, kx, ky, fromX, toX, fromY, toY } = obj;

// формируем индексы, ограничивающие двумерный массив по оси X (строки)

// если координата ‘x’ равна нулю, то это значит, что палуба расположена в самой

// верхней строке, т. е. примыкает к верхней границе и началом цикла будет строка

// с индексом 0, в противном случае, нужно начать проверку со строки с индексом

// на единицу меньшим, чем у исходной, т.е. находящейся выше исходной строки

fromX = (x == 0) ? x : x 1;

// если условие истинно — это значит, что корабль расположен вертикально и его

// последняя палуба примыкает к нижней границе игрового поля

// поэтому координата ‘x’ последней палубы будет индексом конца цикла

if (x + kx * decks == 10 && kx == 1) toX = x + kx * decks;

// корабль расположен вертикально и между ним и нижней границей игрового поля

// есть, как минимум, ещё одна строка, координата этой строки и будет

// индексом конца цикла

else if (x + kx * decks < 10 && kx == 1) toX = x + kx * decks + 1;

// корабль расположен горизонтально вдоль нижней границы игрового поля

else if (x == 9 && kx == 0) toX = x + 1;

// корабль расположен горизонтально где-то по середине игрового поля

else if (x < 9 && kx == 0) toX = x + 2;

// формируем индексы начала и конца выборки по столбцам

// принцип такой же, как и для строк

fromY = (y == 0) ? y : y 1;

if (y + ky * decks == 10 && ky == 1) toY = y + ky * decks;

else if (y + ky * decks < 10 && ky == 1) toY = y + ky * decks + 1;

else if (y == 9 && ky == 0) toY = y + 1;

else if (y < 9 && ky == 0) toY = y + 2;

if (toX === undefined || toY === undefined) return false;

// отфильтровываем ячейки, получившегося двумерного массива,

// содержащие 1, если такие ячейки существуют — возвращаем false

if (this.matrix.slice(fromX, toX)

.filter(arr => arr.slice(fromY, toY).includes(1))

.length > 0) return false;

return true;

}

Игра «Морской бой». Создаём экземпляр корабля и выводим его на экран.

Итак, мы получили начальные координаты корабля и направление расположения его палуб. Теперь нам нужно создать корабль и вывести его на экран.

Вернёмся к функции randomLocationShips. В конце функции есть такие строки:

// создаём экземпляр корабля со свойствами, указанными в

// объекте options с помощью класса Ship

const ship = new Ships(this, options);

ship.createShip();

Настало время рассмотреть их подробнее.

Игра «Морской бой». Конструктор кораблей.

В качестве конструктора кораблей используем функцию-конструктор класса Ships. В качестве аргументов конструктор принимает два параметра:

  1. this, указывает, для кого создаётся данный корабль — игрока или компьютера.
  2. options, это объект, имеющий свойства:
    x и y — координаты первой палубы;
    kx и ky — направлении расположения палуб;
    decks — количество палуб корабля;
    shipname — уникальное имя корабля, которое будет использоваться в качестве его идентификатора.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

class Ships {

constructor(self, { x, y, kx, ky, decks, shipname }) {

// с каким экземпляром работаем

this.player = (self === human) ? human : computer;

// this.player = self;

// на каком поле создаётся данный корабль

this.field = self.field;

// уникальное имя корабля

this.shipname = shipname;

//количество палуб

this.decks = decks;

// координата X первой палубы

this.x = x;

// координата Y первой палубы

this.y = y;

// направлении расположения палуб

this.kx = kx;

this.ky = ky;

// счётчик попаданий

this.hits = 0;

// массив с координатами палуб корабля, является элементом squadron

this.arrDecks = [];

}

}

Игра «Морской бой». Создание корабля и сохранение информации о нём.

Для создания корабля используется функция createShip. Эта же функция используется и для создания кораблей компьютера. Информация, для кого создаётся корабль, находится в свойстве player — это экземпляр или human, или computer.

При создании корабля нам нужно в цикле записать координаты его палуб в двумерный массив matrix и массив arrDecks, который будет записан в объект squadron. В этом объекте храниться полная информация по каждому кораблю, что потребуется в дальнейшем при создании выстрела и оценки его результата.

После того, как корабль создан и координаты его палуб сохранены в соответствующих массивах, выводим корабль на экран с помощью функции showShip. Предварительно необходимо убедиться, что корабль принадлежит игроку, а корабли компьютера мы, естественно, отображать не будем.
Теперь рассмотрим полный код функции создания кораблей:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

createShip() {

let { player, field, shipname, decks, x, y, kx, ky, hits, arrDecks, k = 0 } = this;

while (k < decks) {

// записываем координаты корабля в двумерный массив игрового поля

// теперь наглядно должно быть видно, зачем мы создавали два

// коэффициента направления палуб

// если коэффициент равен 1, то соответствующая координата будет

// увеличиваться при каждой итерации

// если равен нулю, то координата будет оставаться неизменной

// таким способом мы очень сократили и унифицировали код

let i = x + k * kx, j = y + k * ky;

// значение 1, записанное в ячейку двумерного массива, говорит о том, что

// по данным координатам находится палуба некого корабля

player.matrix[i][j] = 1;

// записываем координаты палубы

arrDecks.push([i, j]);

k++;

}

// заносим информацию о созданном корабле в объект эскадры

player.squadron[shipname] = {arrDecks, hits, x, y, kx, ky};

// если корабль создан для игрока, выводим его на экран

if (player === human) {

Ships.showShip(human, shipname, x, y, kx);

// когда количество кораблей в эскадре достигнет 10, т.е. все корабли

// сгенерированны, то можно показать кнопку запуска игры

if (Object.keys(player.squadron).length == 10) {

buttonPlay.hidden = false;

}

}

}

Игра «Морской бой». Вывод корабля на экран монитора.

Прежде чем рассматривать реализацию функции showShip, отвечающую за отображение корабля, нужно разобрать, каким образом осуществляется позиционирование корабля на игровом поле с учётом его координат в двумерном массиве.
Все корабли формируются элементами div с абсолютным позиционированием относительно своего родительского элемента:

<!— родительский элемент для кораблей игрока —>

<div id=«field_user» class=«ships»></div>

<!— родительский элемент для кораблей компьютера —>

<div id=«field_comp» class=«ships»></div>

Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная координаты, можно преобразовать их в смещение в пикселях относительно родительского элемента.

Игра Морской бой. Преобразование координат.

Кроме смещения, элементу div нужно добавить класс по названию совпадающий с именем корабля. Этот класс определит стиль корабля. Если корабль размещён вертикально, то необходимо добавить ещё класс vertical.

Функция showShip является методом класса Ships и не связана с конкретными экземплярами класса (в ней отсутствует this). Поэтому объявим её статичной и разместим внутри класса, после функции-конструктора. Полный код функции:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

static showShip(self, shipname, x, y, kx) {

// создаём новый элемент с указанным тегом

const div = document.createElement(‘div’);

// из имени корабля убираем цифры и получаем имя класса

const classname = shipname.slice(0, 1);

// получаем имя класса в зависимости от направления расположения корабля

const dir = (kx == 1) ? ‘ vertical’ : »;

// устанавливаем уникальный идентификатор для корабля

div.setAttribute(‘id’, shipname);

// собираем в одну строку все классы

div.className = `ship {classname}${dir}`;

// через атрибут ‘style’ задаём позиционирование кораблю относительно

// его родительского элемента

// смещение вычисляется путём умножения координаты первой палубы на

// размер клетки игрового поля, этот размер совпадает с размером палубы

div.style.cssText = `left:${y * Field.SHIP_SIDE}px; top:${x * Field.SHIP_SIDE}px;`;

self.field.appendChild(div);

}

В качестве результата работы функции showShip, мы видим выведенные на экран корабли эскадры в пределах игрового поля игрока. Корабли компьютера создаются теми же самыми функциями, только они на экран не выводятся.

Заключение

Итак, мы рассмотрели, как создать эскадру кораблей для игры в «Морской бой» и разместить её на игровом поле случайным образом. Код, который мы написали, используется не только для создания эскадры игрока, но и для создания кораблей компьютера.

В следующей статье будет рассказано, как с помощью JavaScript реализовать размещение кораблей игрока перетаскиванием на игровое поле, используя метод Drag’n’Drop.

Комментарии

Всего: 19 комментариев

Требования при посте комментариев:

  1. Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
    Комментарии содержащие обсуждение политики, будут безжалостно удаляться.
  2. Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега <pre>:

    <pre class="lang:xhtml"> — HTML;
    <pre class="lang:css"> — CSS;
    <pre class="lang:javascript"> — JavaScript.
  3. Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.

Поддался

пороку

тренду недели и решил написать крошечный морской бой в 30 строк кода. Вот он: ровно 30 строк JavaScript, 2 строки HTML и 6 — CSS. Сжать можно было и гораздо сильнее, в текущем виде читабельность практически не пострадала (за исключением инициализирующих массивов).

Морской бой на JSFiddle.

Сделал на div-ах. Можно было использовать и canvas, но для данной задачи это было бы чересчур, т.к. никакой анимации и сложных фигур не предусматривалось. К тому же, к div-ам удобнее биндить клики и выбирать их селекторами (что было важно для оптимизации, как станет ясно в дальнейшем).

Разбор исходника

Исходник на JSFiddle.

Развернуть исходник

JavaScript:

(function(w, h) {
    var p1map = ['~ss~~~~s~~','~~~~~~~~~~','~~~~s~~~~s','~s~~~~s~~s','~s~~~~s~~s','~s~~~~~~~~','~~~~~~~~s~','~~~~ss~~~~','~s~~~~~~~~','~~~~ssss~~'];
    var p2map = ['~~~s~~~~ss','~s~s~~~~~~','~~~s~~~~~~','~~~s~~~s~~','~~~~~~~s~~','~s~~s~~s~~','~s~~~~~~~~','~s~s~~~~~~','~~~~~ss~~~','ss~~~~~~s~'];
    var p1 = document.querySelector('#p1'), p2 = document.querySelector('#p2');
    for (i=0;i<w;i++) for (j=0;j<h;j++) {
        div1 = document.createElement('div');
        div1.id = i+'_'+j, div1.className = p1map[i][j] == 's' ? 's' : 'w';
        p1.appendChild(div1);
        div2 = document.createElement('div');
        div2.className = p2map[i][j] == 's' ? 's' : 'w';
        div2.onclick = function () { if (fire(this)) backfire(); };
        p2.appendChild(div2);
     }
    function fire(el) {
        if (el.className == 'd' || el.className == 'm') return false;
        el.className = el.className == 's' ? 'd' : 'm';
        if (document.querySelectorAll('#p2 .s').length === 0) {
            alert('You have won!'); 
            return false;
        }
        if (el.className == 'm') return true;
    }
    function backfire() {
        for (i=w*h;i>0;i--) {
            var targets = document.querySelectorAll('#p1 .s, #p1 .w');
            if (targets.length === 0 || fire(targets[Math.floor(Math.random() * targets.length)])) break;
        }
        if (document.querySelectorAll('#p1 .s').length === 0) alert('You have lost!');
    }
})(10, 10);

CSS:

#p1 .s { background: #222; }
#p2 .s, .w { background: skyblue; }
.d { background: red;}
.m { background: gray; }
#p1 div, #p2 div { width: 20px; height: 20px; float: left; }
#p1, #p2 { width: 200px; height: 200 px; float: left; margin: 10px; }

HTML:

<div id="p1"></div>
<div id="p2"></div>

Карты для обоих игроков задаются через массивы p1map и p2map. Нагляднее, конечно, рисовать корабли, когда массивы выстроены в двух измерениях, но мы ведь экономим строки. Никаких ограничений на установку кораблей нет, полагаемся на честность игрока (как было во времена этой игры в школьных тетрадках).

Компьютерный противник ужасно примитивен: рандомно бомбит квадраты, не учитывая ситуацию на игровом поле. Но это, на мой взгляд, немного уравновешивается тем, что мы не знаем, «ранен» корабль или «убит», и вынуждены делать больше выстрелов, чтобы разведать обстановку. Оба эти решения позволили упростить и тем самым сэкономить код.

В альфа-версии боя для хода компьютера использовался цикл for с количеством итераций по количеству клеток на игровом поле. Если за эти итерации не удавалось рандомно обнаружить нетронутую клетку, ход компьютера не выполнялся. Изъян, мягко говоря.

    for (i=w*h;i>0;i--) {
        var r1 = Math.floor(Math.random() * w), r2 = Math.floor(Math.random() * h);
        el = document.getElementById(r1+'_'+r2);
        if (el.className != 'd' && el.className != 'm') if (fire(el)) break;
    }

Полез было вдохновляться замечательным постом про уникальные случайные числа на Stack Overflow, но решение оказалось куда более простым: с помощью querySelectorAll получать список «необстрелянных» клеток и выбирать цель уже среди них. Здесь возник и потенциал для увеличения сложности без намёка на AI: отсекать некоторую часть пустых клеток из выборки, чтобы CPU чаще попадал в корабли.

Также, думаю, не было особого смысла (кроме экономии строк) в передаче высоты и ширины игрового поля через параметры в главную функцию — всё равно без изменения карт игра в таком случае не заработает.

Принимаю предложения по улучшению. Можно ли было впихнуть в этот объем кода различение подбитых и потопленных кораблей, простенький AI, генератор или редактор карт, звуки взрывов?

Когда-то давно я, как и большинство начинающих программистов, хотел написать компьютерную игру. Выбор пал на «Морской бой» — игру простую, но не до тривиальности. Я начал с разработки теории (как именно должен выбирать себе цель «искусственный интеллект»). Затем мне стало интересно изобрести собственную модификацию правил. В моём варианте не было обычных двух полей, а корабли обеих сторон расставлялись на одном большом поле вперемешку. Как это возможно, я расскажу ниже.

На днях я решил вспомнить язык JavaScript и заодно изучить возможности объекта canvas, который появился в стандарте HTML5. И первым делом вспомнил эти свои старые идеи и попробовал реализовать их хотя бы на самом примитивном уровне.

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

Нажмите на кнопку, чтобы запустить игру. Требуется браузер с поддержкой HTML5 (то есть практически любой современный, в том числе должен работать IE версии выше 9). Игра поддерживается только для одного игрока против компьютера.

Отмечу, что на планшете iPad (в любом браузере) почему-то наблюдается неприятное моргание экрана после каждого хода. Я пока не могу разобраться, в чём дело. В настольных браузерах такого эффекта нет.

Правила игры

Имеется одно большое прямоугольное поле. Размер поля выбирается случайным образом — я заложил выбор случайного числа в промежутке от 10 до 20 включительно. Однако общая площадь поля не может быть меньше 200 и больше 250 ячеек. На маленьком поле корабли не поместятся, на большом игра будет слишком долгой и скучной.

Расстановкой кораблей перед началом матча занимается компьютер. В отличие от человека компьютер «умеет» быть честным, то есть он не будет в процессе игры использовать информацию о расположении кораблей противника.

Набор кораблей традиционный: 1 размером 4 клетки, 2 по 3 клетки, 3 по 2 и 4 по одной. Чтобы было веселее, я добавил пару специальных объектов — плавучих мин. Если кто-то из игроков вместо вражеского корабля попадает на мину, то компьютер случайным образом «подбивает» (или «убивает», если корабль занимает одну ячейку) корабль этого игрока.

Игроки ходят по очереди, по одному выстрелу за ход (вне зависимости от его результативности — видел я модификации, где результативный ход даёт право на дополнительный выстрел). Игра заканчивается, если у одной из сторон не осталось «живых» кораблей (мины не считаются).

Помимо своих кораблей и мин игрок может видеть на экране «подбитые» или «убитые» корабли противника и те пустые ячейки, по которым он стрелял (ячейки, по которым стрелял компьютер, на экране не отмечаются, иначе партии будут совсем короткими). Аналогично, компьютер «не видит» отметок игрока. Выглядит это примерно так:

Зелёным цветом выделены корабли игрока, красным — «убитые» корабли противника, крестики без рамок отмечают его же «подбитые» корабли. Чёрный шарик — мина игрока (красный фон означает, что она уже «убита»). Наконец, жёлтая ячейка — та, над которой сейчас находится курсор мыши (если играть с сенсорного устройства вроде планшета, то её не будет видно).

По завершении партии компьютер показывает серым цветом, где были спрятаны оставшиеся фигуры противника, если игрок не нашёл их все сам.

Как компьютер выбирает цель

Дано: поле для игры «Морской бой» конечного размера, часть ячеек заполнена. Нужно определить, где вероятнее всего можно обнаружить корабль (избежав, естественно, полного перебора всех мыслимых вариантов).

Рассмотрим отдельную ячейку (на рисунке обозначена крестиком). Кружки отмечают те ячейки, в которых не может быть никаких кораблей. Гарантированно непригодной для заполнения является ячейка, которая либо уже была проверена («обстреляна») ранее, либо непосредственно граничит с любой из 8 сторон с собственным или «убитым» вражеским кораблём (мы считаем, что корабли не могут соприкасаться никак, даже углами). Посчитаем, сколькими способами может размещаться корабль, чтобы занимать эту ячейку. Возьмём для примера корабль, состоящий из трёх ячеек. Видно, что в нашем примере существует всего пять разных способов его размещения.

Обобщив эти соображений на всё поле, можно вывести общую формулу.

Обозначим CN количество кораблей длиной N, L — расстояние от нужной ячейки до ближайшей гарантированно непригодной для заполнения ячейки или до края доски слева, включая исходную ячейку, R — такое же расстояние справа, T — сверху, B — снизу.

Введём дополнительное обозначение для упрощения: MAB — это наименьшее из чисел A и B.

 Далее, для каждого значения N > 1 можно вывести формулу: ячейка может быть заполнена таким количеством способов:

C(MLN MRN MTN MBN − 2N)

Здесь множитель 2 при N намекает на количество измерений (соответственно, если бы мы играли в трёхмерном пространстве, множитель был бы 3, а число слагаемых вида MAB было бы равно 6).

Просуммировав эти значения по всем возможным размерам кораблей N (мы обозначим буквой M наибольшую длину корабля, в нашей игре M = 4), мы получим количество способов, которым ячейка может вообще быть заполнена (кроме случая с N = 1, так как единичным «кораблём» может быть заполнена любая ячейка, если она не граничит с «убитым» вражеским или с собственным кораблём игрока):

M

C(MLN MRN MTN MBN − 2N)

N = 2

Посчитав наибольшие значения этой формулы по всей доске, мы получим самые вероятные расположения кораблей противника. Посчитав наименьшие значения на этапе заполнения доски, мы получим наиболее выгодные для нас позиции собственных кораблей (это условие я не использовал, в этой версии корабли расставляются случайным образом).

Вот и всё. Остаётся добавить условие: если любая из 4 ячеек, граничащих с этой, содержит фрагмент «подбитого» (но не «убитого»!) корабля, то вероятность попасть в неё резко возрастает. Так что если на доске есть такие ячейки, то должны проверяться только они.


Вот такая получилась «проба пера». Исходник можно посмотреть в Google Docs здесь [последнее обновление от 19.12.2013] или в JSFiddle [доработано 23.06.2017 с заменой обработчиков событий на addEventListeners].

На Хабре последнюю неделю популярна тематика морского боя. Хочу показать вам свою реализацию этой игры на JavaScript с использованием библиотеки JQuery. Я считаю себя начинающим программистом и многие мои решения могут быть крайне не оптимальными. Игру делал для себя, чтобы развить свои навыки программирования.

image

Поиграть можно прямо сейчас

Вступление

Работая веб-программистом, я всегда хотел коснуться игровой индустрии. Самостоятельно писать мелкие игры. С чего-то нужно было начинать, но знания были только соответствующей профессии. Потому я и решил попробовать сделать что-то прямо в браузере, используя доступные мне языки программирования. За несколько дней, в свободное от работы время и родилось на свет это чудо. В чем-то крайне кривое, но работает, компьютер стреляет по кораблям в отместку и иногда даже выигрывает. Поделюсь основными, на мой взгляд самыми интересными моментами.

Безумные решения

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

1: Для сохранения состояния игрового поля не используются массивы.
Игровое поле игрока и компьютера состоит из сотни div-ов. Наличие дочерних элементов в них и их класс определяют состояние игрового поля.

2: Позиция игровой ячейки определяется индексом тега.
Из порядкового номера элемента на поле вычисляются его координата, при необходимости. В большинстве случаев соседние ячейки вычисляются из текущей координаты (+1, -1, +10, -10)

3: Проверка границ поля осуществляется 9-ю проверками для каждой возможной границы (левый верхний, левый, левый нижний и т.д.)

Размещение кораблей

Корабли нельзя ставить вплотную, поэтому при размещении корабля необходимо проверять наличие конфликтов по ближайшим клеткам. Алгоритм обещал быть сложным, так что я схитрил, и при установке корабля сразу помечал соседние ячейки как недоступные для размещения.
image

Искусственный «интеллект»

Какой же морской бой без соперника? Пусть и компьютерного.
Компьютер ходит абсолютно случайно до тех пор, пока не обнаружит вражеский корабль. Обнаружив корабль, он начинает обстреливать его, в поисках нужного размещения.
image
При этом сначала компьютер определяет плоскость, по которой он будет бить. Если по горизонтали уже есть два попадания в корабль, то он продолжает бить по горизонтали.

На этом все. Все исходники доступны через просмотр исходного кода страницы.

Автор: Satellence

Источник

Практическая
работа

Тема:  «Морской бой» Цель работы: 

Получить практические навыки использования фреймворка jQuery.

Теоретические сведения:

jQuery — это быстрая, небольшая и
многофункциональная библиотека JavaScript. Он значительно упрощает такие вещи,
как обход и манипулирование документами HTML, обработку событий, анимацию, с
помощью простого в использовании API, который работает во множестве браузеров.

Благодаря сочетанию универсальности и расширяемости jQuery
изменил способ написания JavaScript. (URL: https://jquery.com/)

Задание:

Необходимо средствами HTML5, CSS3, JS разработать браузерную
игру «Морской бой». 

Общие требования:

1.                      
Игра должна быть кроссбраузерной и иметь адаптивную верстку.

2.                      
Стили и код JS должны быть вынесены в отдельные файлы “seaBattles.css”,
“ seaBattles.js”.

3.                      
Должен быть использован фреймворк jQuery.

4.                      
Верстка, код и стили должны содержать комментарии.

5.                      
Верстка, стили и код должны быть валидными и соответствовать стандартам W3C.

6.                      
Стилизация и цветовое оформление на усмотрение разработчика.

7.                      
Все элементы должны быть хорошо видны, текст должен быть контрастным и
легко читаться.

Техническое задание, описание и принципа
работы:

Часть 1

При заходе на страницу игры отображается заголовок и форма
со следующими полями:

 
поле ввода имени игрока;

 
поле ввода адреса электронной почты;

 
чекбокс с текстом подтверждения согласия с правилами игры, с
обязательной ссылкой на страницу правил;

 
чексбокс с текстом подтверждения использования куки;

  кнопка начала игры.

Страница правил представляет собой отдельный HTML файл.
Страница должна открываться в новой вкладке. Содержимое страницы (текст правил)
на усмотрение разработчика.

Все поля формы – обязательны для заполнения. Кнопка начала
игры изначально не активна. Активация кнопки происходит динамически, после
заполнения всех полей формы и установки чекбоксов. При удалении содержимого
любого из полей или отключения чекбокса, кнопка начала игры деактивируется.

При клике на кнопку начала игры производиться валидация
данных формы с последующим переходом на новый экран (не страницу), или выводом
сообщения об ошибке в форме.

Переход на новый экран
подразумевает скрытие содержимого формы и отображение содержимого игрового
экрана.

На игровом экране, помимо названия, должно быть отображено:

 
Имя игрока

 
Дата и время начала игры

 
Таймер игры

 
Кол-во ходов игрока, компьютера, общее

 
Игровые поля для игрока и компьютера
Индикатор хода (игрок или компьютер)

 
Кнопка расстановки кораблей.

 
Кнопка выполнения хода.

Расстановка кораблей (4
однопалубных, 3 трехпалубных, 2 двухпалубных, 1 четырехпалубный) производится
вручную (автоматический режим приветствуется) как для игрока, так и для
компьютера.

При расстановке кораблей
производиться контроль корректности на расстановку и количество. Должна быть
возможность установки корабля по горизонтали и по вертикали.

Кнопка выполнения хода
активируется только после расстановки кораблей. 

Часть 2

После того как все корабли
расставлены (игрок, компьютер), необходимо случайным образом определить

Кто ходит первым, игрок или
компьютер. После чего совершается ход.

Если ходит компьютер, то ход осуществляется
автоматически в случайную ячейку поля игрока. Если ходит игрок, то выбор ячейки
в поле компьютера осуществляется соответствующим кликом мыши.

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

После выполнения ходя должна быть
произведена смена хода игрок-компьютер или компьютеригрок.

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

В процессе разработки игры должно
быть продемонстрировано умение применения следующих инструментов JS:

 
let, var, const

 
if / else

 
for

  function

Критерии оценки:

Выполнение всего задания и всех
требований оценивается как «отлично», выполнение всего задания, но с
некорректно работающим функционалом оценивается как «хорошо», выполнение только
первой части задания оценивается как «удовлетворительно».

Понравилась статья? Поделить с друзьями:
  • Как написать методическую разработку для аттестации
  • Как написать маленькое письмо солдату
  • Как написать маленький стишок на день рождения
  • Как написать буквы цифрами
  • Как написать логлайн сценария