diff --git a/jtd1-06-workshop.tex b/jtd1-06-workshop.tex new file mode 100644 index 0000000..4b401b1 --- /dev/null +++ b/jtd1-06-workshop.tex @@ -0,0 +1,36 @@ + +% СЕМИНАР: Давайте пилить стартовое окно. +% Объявим константы, заботливо подобранные заранее, ширина окна 350 высота 230, минимальная выигрышная длина 3, минимальный размер поля 3, и максимальные размеры 10, 10. эти значения нам нужны будут для создания слайдеров. И давайте сразу объявим переменную типа GameWindow gameWindow, и зададим ей значение из конструктора. this.gameWindow = gameWindow. установим ширину и длину. Теперь давайте, чтобы сделать красиво и ровно посередине, воспользуемся ещё одним новым на сегодня классом, Rectangle, который представляет собой абстрактный прямоугольник. назовём его gameWindowBounds и создадим, = gameWindow.getBounds(), сразу передав в него значения границ нашего окна. у окон есть и такой метод, getBounds(), да. В общем это просто удобный класс для работы с прямоугольниками, то есть с окнами например. И теперь делаем немного математики, задаём переменную +% int pos_x = (int)gameWindowBounds.getCenterX() - WIN_WIDTH / 2; +% int pos_y = (int)gameWindowBounds.getCenterY() - WIN_HEIGHT / 2; +% setLocation(pos_x, pos_y); +% setResizeable(false); +% setBorder(new EmptyBorder(3, 3, 6, 3)); +% Сделаем заголовок, СОЗДАНИЕ НОВОЙ ИГРЫ. расположение сделаем сеткой setLayout(new GridLayout(10, 1)); Дальше нам надо добавить сюда много-много разных управляшек, и эти однотипные по сути действия мы для удобства вынесем в отдельный метод void addGameControlsMode(){}; +% Для начала добавим надпись, для этого существует класс JLabel, пишем add(new JLabel(“Choose gaming mode”)); в конструктор можем передать собственно надпись, а можем ничего не передавать и задать её потом, но для этого надо будет поместить надпись в какой-то именованный контейнер, для нас сейчас это, понятно, лишнее. +% Дальше нужны будут две радиокнопки, которые пригодятся не только в этом методе, поэтому объявление вынесем вверх, а присваивание значений сделаем в нашем методе, который всё добавляет. выносим +% private JRadioButton humVSAI; +% private JRadioButton humVShum; +% и присвоим humVSAI = new JRadioButton(“Play VS Computer”); с надписью переданной в конструктор, и вторую также. добавим на наше окно и увидим что всё неправильно работает. add(hva); add(hvh); +% Чтобы всё заработало как мы ожидаем есть класс который связывает радиобатоны в группы, что видно из его названия ButtonGroup gameMode = newBG(); gmode.add(hvm); gmode.add(hvh); и саму радиогруппу никуда класть не надо, она просто объединит. и чтобы одна из них была выбрана по умолчанию, используем перегруженный конструктор радио кнопки, (“play vs comp”, true); +% ***** Слайдеры +% пишем следующий метод addGameControlsFieldWinLen(){} в котором будем описывать наши слайдеры, то есть понятно, что это всё ещё конструктор, просто мы делаем его чисто внешне менее жирным. Ну и чтобы логически разграничить, потому что код уже получается достаточно непростой. +% Напишем некий подзаголовок add(new JLabel(“Выбери длину”)); и пишем новенький лейбл на котором будем писать выигрышную длину +% JLabel lbl_win_len = new JLabel(“WIN_LEN” + WIN_LEN)); +% add(lbl_win_len); этот видите мы создаём в два действия, потому что цифру на нём мы будем менять, а не задавать из программы. Давайте объявим слайдеры где-то рядом с JRadioButton'ами. +% private JSlider sField_size; +% JSlider sWin_len; и в методе также создадим. в конструкторе слайдеру можно передать три значения, мин, макс и текущее пишем +% sWin_len = new Slider(MIN_WIN, MIN_FIELD, MIN_WIN); +% на слайдер мы тоже вешаем слушателя изменений, только тут он называется ChangeListener пишем sWin_len.addChangeListener(new ChangeListener)); и в нём делаем изменение текста для нашего лейбла лбл_вин_лен.сетТекст(“WIN_LEN” + sWin_len.getValue); +% и добавим его на окно после лейбла. add(sWin_len); дальше создаём такой-же лейбл с размерами поля и аналогично ставим ему текст, добавим на окно. Теперь поинтереснее, создаём слайдер для поля, тоже аналогично, пишем чендж листнер и начинаем прикалываться, введём ещё одну переменную, current_field_size = sField_size.getValue() и будем использовать его в двух местах, во первых нам надо значения на лейбле менять, а во вторых, максимальное значение выигрышной длины тоже менять. лейбл.сетТекст(ФИЛД + каррент); и сВинЛен.сетМаксимум(каррент); и давайте аккуратно по порядку добавим слайдеры и надписи. Вот теперь вы никак не сможете ошибиться и сделать выигрышную длину больше чем размер поля. +% ***** И осталась нам только кнопочка “начать игру” +% в конце конструктора пишем JButton btnStartGame = new JButton(“New Game”); add(btnSG); вот у нас и получился один столбец и десять строчек. Осталось только оживить кнопочку, для этого напишем последний на сегодня метод назову его btnStart_onClick(), +% в кнопке пишем btnSG.addActionListener(new ActionListener()); и внутри любезно предоставленного шаблона просто вызовем наш метод, чтобы много не писать. А обработчик очень интересный, в нём нам надо вызвать метод startNewGame из основного окошка, но чтобы сделать это нам надо наполнить сигнатуру разными параметрами, которые мы возьмём с наших управляшек. +% Давайте в классе Map объявим пару констант, раз мы не изучали с вами перечисления. GAME_MODE_H_V_A = 0, GAME_MODE_H_V_H = 1; и в классе в котором работаем создадим новую переменную инт game_mode; +% пишем if(rb_hva.isSelected()) значит если кнопка выделена - game_mode = Map.HVA иначе rb_hvh.isSelected() game_mode = Map.HVH; ну и совсем иначе генерим исключение. мало-ли мы через пол года захотим ещё режимов надобавлять, батон добавим, а сюда забудем дописать, или пользователю не отобразится радиобатон, throw new RuntimeException(“No Buttons Selected”); и программа нам подскажет где искать. +% также с размером поля, которое пока что у нас будет квадратным, хоть мы и архитектурно заложили, что оно может быть не квадратным. эти показания мы просто снимем со слайдеров и передадим в конструктор slider.getValue(); ну и перед тем как закончить обработчик нам надо наше окошко спрятать от глаз пользователя до следующего раза, setVisible(false); +% ну вот мы выполнили план на сегодня, хоть мы и немного отошли от методички, но мы сделали хороший задел для нашего последнего занятия, нам там надо будет только написать логику, и расчертить полянку +% ***** Домашняя работа +% 1. Полностью разобраться с кодом (попробовать полностью самостоятельно переписать одно из окон) +% 2. Составить список вопросов и приложить в виде комментария к домашней работе +% 3. * Раcчертить панель Map на поле для игры, согласно fieldSize diff --git a/scenarios/jtd1-06b.tex b/scenarios/jtd1-06b.tex index 9fc1eb2..549786e 100644 --- a/scenarios/jtd1-06b.tex +++ b/scenarios/jtd1-06b.tex @@ -154,7 +154,7 @@ 06-25 & Итак нам понадобится генератор псевдослучайных чисел, потому что мы не будем на лекции создавать очень умного противника, символы, которыми мы будем обозначать на поле игрока, компьютер и пустую ячейку, собственно поле и его размеры. Размеры - на будущее. %% 2 -Метод инициализации поля - просто создаём новый массив и заполняем его пустотой. +Метод инициализации поля - просто создаём новый массив и заполняем его пустотой. Его вызов будет логично сразу разместить в метод старта новой игры. %% 3 Когда кто-то, мы или компьютер, будет совершать ход, нам будет важно, попал ли игрок ввобще в какую-то ячейку поля и пустота ли там, потому что, насколько я помню, нельзя ставить крестик поверх нолика и наоборот @@ -163,98 +163,48 @@ Компьютер, как я и сказал, будем делать очень примитивный, он просто будет делать ход в случайные места на карте. Перед тем, как я покажу следующий слайд я прошу слабонервных удалиться от экранов и напоминаю, что мы делаем программу в учебных целях %% 5 -как вы видите, учебные цели предполагают не только демонстрацию того, как надо делать, но также и демонстрацию того как делать не надо. все трюки выполнены профессионалами, ни один тимлид после ревью кода не пострадал. на работе никогда так не делайте, всегда пишите с помощью циклов, потому что стоит нам захотеть поиграть на поле 4х4 или 5х5 - этот метод будет расти в геометрической прогрессии. +как вы видите, учебные цели предполагают не только демонстрацию того, как надо делать, но также и демонстрацию того как делать не надо. все трюки выполнены профессионалами, ни один тимлид после ревью кода не пострадал. на работе никогда так не делайте, всегда пишите с помощью циклов, потому что стоит нам захотеть поиграть на поле 4х4 или 5х5 - этот метод будет расти в геометрической прогрессии. метод принимает на вход символ, который нужно проверить и проверяет - не победил ли он. %% 6 -вот, например, достаточно хороший метод проверки поля на состояние ничьей. напомню, ничья в крестиках ноликах - это когда никто не победил и пустых клеток не осталось. \\ \hline +а вот, например, достаточно хороший метод проверки поля на состояние ничьей. напомню, ничья в крестиках ноликах - это когда никто не победил и пустых клеток не осталось. \\ \hline - & ход игрока, рисование крестика и нолика \\ \hline - & конец игры \\ \hline +06-26 & Итак, теперь наша программа не просто умеет быть красивым окошком, но и может понять ряд интересных вещей, таких как, верный ли сделан ход, наступила ли ничья или кто-то победил. Всё, что мы будем делать дальше, будет сконцентрировано на методах обновления игрового состояния и отрисовки игрового поля. Ну что, нашли мы кликнутую ячейку, давайте в неё ходить, только нам для начала надо проверить, валидная-ли ячейка, и можно-ли туда ходить, вот и проверим: (если не валидная ячейка по координатам (cellX, cellY) или не пустая ячейка по тем же координатам) return; то есть если что-то пошло не так, игнорим клик, выходим из метода, ну а если всё хорошо - просто ходим филд[селХ][селУ] = humandot; +%% 2 +И добавим в метод отрисовки ещё немного логики. сделаем обход всех ячеек поля и внутри пишем, если ячейка пустая - просто идём дальше, ничего не делаем, дальше пишем маленькую заготовку: если в ячейке крестик - что-то сделаем, если нолик - сделаем что-то другое и в противном случае выбросим новый рантайм эксепшн с текстом (не пойму что в ячейке + и,ж), мало ли что мы там перекодили и допилили, может решили сделать баттл для трёх игроков? +%% 3 + +И теперь пришли к отрисовке. хотите, можете сюда картинку вставлять, хотите закрашенные квадратики, хотите рисуйте крестики-нолики, я для простоты буду пользоваться методом объекта графики g.fillOval() и буду рисовать кружки. ему в сигнатуре передаётся левая верхняя координата прямоугольника, в который потом будет вписан овал, и его ширина и высота соответственно. тем, кто хоть раз рисовал в пэинте - знакома эта техника рисования кружков. ну и чтобы придать ему цвета - перед тем как рисовать изменим цвет объекта graphics, g.setColor(Color.BLUE); Итак я предлагаю для человека рисовать синие кружки, а для компа красные. и чтобы сделать небольшой отступ от границ ячейки сделаем следующее, заведём константу DOTSPADDING = 5 пикселя. и первые координаты смещаем на + пэддинг вторые на - 2 пэддинга. Так посимпатичнее. +%% 4 + +по сути, нам осталось сделать две вещи - прикрутить так называемую бизнес логику, то есть в правильном порядке повызывать методы с логикой игры, избавиться от пары выпадающих исключений и нарисовать сообщение об окончании игры. начнём с конца. с конца игры. для того чтобы вывести результат, мы не будем придумывать ещё одно окно, это не слишком интересно, лучше напишем поверх игрового поля сообщение, а для этого нам надо как-то передать информацию о том, что игра завершена, для этого придумаем состояние окончания игры и опишем его вариации. \\ \hline + +06-27 & Получается, теперь осталось это только оживить и нарисовать. Идём в метод апдейт и уточняем, что когда пользователь поставил точку, надо проверить состояние поля на наличие победы или ничьей, дать возможность компу поставить точку и сделать тоже самое +%% 2 + +дальше в принципе нам только и надо что нарисовать все красотульки. значит идём в метод рендер и думаем там. как только мы вывели поле, и если игра закончилась нам надо вывести сообщение с одним из вариантов исхода. так и запишем if (gameOver) шоуМессаджГеймовер(g); вот просто перевели наши слова на английский. теперь смотрим, чего у нас для этого не хватает в коде, как минимум метода шоумессаджгеймовер() и переменной признака конца игры. да не вопрос, мыжпрограммисты, напишем войд шоумессаджгеймовер(Graphics g), и где-то там вверху private boolean окончена ли игра. и поехали наполнять эти штуки смыслом. метод окончания игры рисует тёмно серый прямоугольник с жёлтой надписью о победе одного из игроков или ничьей в зависимости от геймоверстейта. если вдруг не поняли какой стейт у геймовера - кинули исключение. +%% 3 + +Осталось куда-то деть булево с окончанием игры и понять что за исключение у нас возникает при старте приложения. Почитав текст исключения мы понимаем, что оно возникает, когда программа не может что-то поделить на ноль. А что и когда она не может поделить на ноль? размеры поля до их инициализации, поэтому нам понадобиться ещё одна булева переменная - инициализирована ли игра. + +В конструкторе панели, очевидно, поле ещё не инициализировано + +В апдейте, очевидно, что нет смысла обрабатывать клики по неинициализированному полю или полю на котором закончилась игра + +В старте новой игры, очевидно, игра перестаёт быть законченой, а поле становится инициализированным + +Рендерить на неинициализированном поле нам тоже ничего не нужно. + +А вот там, где мы проверяли не победил ли кто, нужно добавить истинность булевой с геймовером. \\ \hline + +06-28 & Результат наших трудов вы можете видеть на экране, мне кажется, что получилось неплохо, да и с графическими интерфейсами короткое знакомство провели. \\ \hline + +06-29 & На этой лекции мы познакомились с тем, как создавать окна, поговорили о менеджерах размещений, элементах графического интерфейса, и обработчиках событий. Многое осталось за границей этой лекции, во многом из-за того, что мы ещё не изучили такие интересные темы, как многопоточность и программные интерфейсы, но обязательно изучим. Изучение графических фреймворков не должно становиться для вас целью, если не хотите связать своё будущее с фронтенд разработкой, а раз уж вы здесь, думаю, вы уже сделали свой выбор. Разработка графического интерфейса подобного рода должна стать утилитарной задачей, то есть, решаться по мере поступления каких-то вопросов, поэтому мы оставим вопрос разработки графических интерфейсов на будущее самостоятельное изучение или даже лучше - просто периодически будем к ним возвращаться и делать наши приложения более привлекательными визуально. \\ \hline + +06-29 & В качестве домашнего задания именно по графическим интерфейсам нужно будет полностью и досконально разобраться с кодом. Не бойтесь записывать свои вопросы и задавать их на семинарах и наставникам. А чтобы попрактиковаться в программировании нужно переделать метод проверки победы, чтобы на него не было больно смотреть. Будет классно, если получится сделать метод проверки победы для поля любого размера и любого числа крестиков-ноликов для победы, то есть, например, чтобы победить на поле 5х5 нужно собрать линию из 4х крестиков. Ну и если останется время, я отметил это задание аж двумя звёздочками, подумайте, как сделать компьютер немного умнее, чем просто случайным. специально отмечу в формулировке задания слово примитивно, потому что никто не ожидает от вас каких-то разборов графовых алгоритмов, минимакса и прочей теории игр. \\ \hline + +06-30 & На сегодня это всё. Напоследок соглашусь с Иоганном Вольфгангом фон Гёте, если не очень нравится что-то делать, научиться этому будет довольно трудно. а если трудно, то правильно ли выбрано направление? Любите то, чему учитесь, знания будут сами собой оставаться в голове, без усилий. \\ \hline \end{longtable} \end{document} -***** Ход игрока - private static void humanTurn() { - int x, y; - do { - System.out.print("Введите координаты X и Y через пробел>> "); - x = SCANNER.nextInt() - 1; - y = SCANNER.nextInt() - 1; - } while (!isValidCell(x, y) || !isEmptyCell(x, y)); - field[y][x] = HUMAN_DOT; - } - - -***** Игровой цикл - public static void main(String[] args) { - initMap(7, 5, 4); - printMap(); - while (true) { - humanTurn(); - printMap(); - if (checkWin(HUMAN_DOT)) { - System.out.println("Выиграл игрок!!!"); - break; - } - if (isMapFull()) { - System.out.println("Ничья!!!"); - break; - } - aiTurn(); - printMap(); - if (checkWin(AI_DOT)) { - System.out.println("Выиграл компьютер!!!"); - break; - } - if (isMapFull()) { - System.out.println("Ничья!!!"); - break; - } - } - SCANNER.close(); - } - - - ***** берём наш код от крестиков-ноликов с третьего занятия - свой я вам выкладывал, так что если не готовы воспользоваться своим - возьмите мой. и копипастим его от мэйна до метода humanTurn(). всё, где логика и нет работы с консолью. вот вам пример необходимости писать методы, которые ничего лишнего не выводят. код не переписываем, а приложение уже совсем другое. Ну и видим, что нам не хватает кое каких констант. объявим private static final int EMPTY_DOT = 0, HUMAN_DOT = 1, AI_DOT = 2; убираем статики из методов, и чиним наш проект дальше. - - ***** Ну что, нашли мы кликнутую ячейку, давайте в неё ходить, - только нам для начала надо проверить, валидная-ли ячейка, и можно-ли туда ходить, вот и проверим: (!isValid(cellX, cellY) || !isEmptyCell(cellX, cellY)) return; игнорим клик, выходим из метода - ну а если всё хорошо - просто ходим филд[селХ][селУ] = hum_dot; - И добавить в наш метод отрисовки ещё немного логики. сделаем двойной массив по всем ячейкам нашего поля фори(филдСайзУ) фори(филдСайзХ) и внутри пишем - if(isEmptyCell(ж, и)) continue; то есть если ячейка пустая - просто идём дальше, ничего не делаем, дальше пишем маленькую заготовку: - if(field[i][j] == H_D) {} else if (field[i][j] == A_D) {} else throw new RTE (не пойму что в ячейке + и,ж) мало ли что мы там перекодили и допилили, может решили сделать баттл для трёх игроков? - И теперь пришли к отрисовке. хотите, можете сюда картинку вставлять, хотите закрашенные квадратики, хотите рисуйте крестики-нолики, я для простоты буду пользоваться методом g.fillOval() и буду рисовать кружки. ему в сигнатуре передаётся левая верхняя координата прямоугольника, в который потом будет вписан овал, и его ширина и высота соответственно. тем, кто хоть раз рисовал в пэинте - знакома эта техника рисования кружков. ну и чтобы придать ему цвета - перед тем как рисовать изменим цвет объекта graphics, g.setColor(Color.BLUE); или задать интами от 0 до 255 в формате РГБ new Color(RGB); - Итак я предлагаю для человека рисовать синие кружки, а для компа красные. и после проверки собственно его рисовать, в проверках пишем setColor(); а после проверок пишем g.fillOval(j*CellW, i*CellH, CW, CH); и чтобы сделать небольшой отступ сделаем следующее, заведём классовую константу DOTS_PAGGING = 5 пикселя. и первые координаты смещаем на + пэддинга вторые на - 2 пэддинга. - Так посимпатичнее. -***** Давайте завязывать с этим безумием - объявим константы которые будут содержать исходы игры, псфи DRAW = 0; HUM_WIN = 1; AI_WIN = 2; и нужна переменная stateGameOver в которой будет храниться статус. - Значит теперь в методе апдейт мы поставили точку, перерисовали и жедаем по старому сценарию - if(checkWin(H_D)) stGO = H_W; return; - if (isMapFull()) stGO = DR; return; - aiTurn(); - repaint(); - if(checkWin(A_D)) stGO = A_W; return; - if (isMapFull()) stGO = DR; -***** дальше в принципе нам только и надо что нарисовать все красотульки. - значит идём в метод рендер и думаем там. как только мы вывели поле, и если игра закончилась нам надо вывести сообщение с одним из вариантов исхода. так и запишем if (gameOver) showOverMessage(g); вот просто перевели наши слова на английский. теперь смотрим, чего у нас для этого не хватает в коде, как минимум метода showOverMessage() и переменной признака конца игры. да не вопрос, мыжпрограммисты, напишем войд sOW(Graphics g), private boolean gameOver. и поехали наполнять эти штуки смыслом: - в методе начала новой игры нам, очевидно, надо сказать что геймовер = фолс. и закончим на этом метод СНГ. - идём в метод апдейт и в самом начале пишем, что if(gameover) return; очевидно, что если геймовер случился - надо игнорировать вообще все клики. ну а если кто-то выиграл или ничья - геймовер также становится тру. и на этом закончим с методом апдейт и останется только дописать собственно метод показывающий, что у нас закончилась игра. - Ну и для того чтобы нарисовать в классе графики какой-то текст есть метод g.drawString() с выводимой строкой и координатами по х и по у, внимание, координата по у это координата нижней части букв, как строчка в тетради. Ну и для того чтобы этот текст кастомизировать да и вообще для работы со шрифтами есть класс не поверите Font. создадим классовый private final Font который назовём не поверите font = new Font( и на вход конструктор внезапно принимает название шрифта, стиль и размер “Times new roman”, Font.BOLD, 48); - будьте осторожны при работе с такими неочевидными классами. и теперь наш метод с отрисовкой строки немного дополняем, setColor(); setFont(font). вот так он и выводится. Давайте создадим классовые константы, в которых будем хранить наши сообщения о победах. psfS MSG_DRAW= “DRAW”; MSG_HW; MSG_AW; -***** в showGameOver - пишем свич (стейтГеймОвер) и кейсы ДРО, Х_В, А_В. дефолт throw new RTE(unexpected GO message); - теперь если дро, g.drawString(MSG_DRAW, 180, getHeight/2); - если H_W, g.drawString(HW, 70, gH/2); - если A_W (20); - и перед ним пишем setColor(YELLOW) setFont(font); - ну и чтобы было виднее, я предлагаю рисовать тёмно серый прямоугольник в качестве фона работает он также как fillOval только рисует прямоугольник пишем fillRect(0, 200, getWidth, 70); естественно надо все магические цифры либо вывести в константы, либо как-то динамически считать). и БУМ! всё классно, мы закончили. - - -***** Домашнее задание - 1. Полностью разобраться с кодом; - 2. Переделать проверку победы, чтобы она не была реализована просто набором условий. - 3. * Попробовать переписать логику проверки победы, чтобы она работала для поля 5х5 и количества фишек 4. - 4. *** Доработать искусственный интеллект, чтобы он мог блокировать ходы игрока, и пытаться выиграть сам.