-
Notifications
You must be signed in to change notification settings - Fork 15
Task 3: Comparison with SnakeGame1
აღწერეთ რა ცვლილებები გახდებოდა საჭირო თქვენთვის მონიჭებული Issue-ს SnakeGame1 -ის კოდში განსახორციელებლად.
SnakeGame1-ის კოდში ლეველების ცნება წარმოდგენილია მხოლოდ Grid-ის არჩევის საშუალებით, რომელსაც არც კონკრეტული სახელი გააჩნია და არც აღწერა, არამედ მომხმარებლის მიერ სირთულის არჩევით განისაზღვრება (easy - insert-ზე დაჭერა, difficult - enter-ზე დაჭერა). მომხმარებლისთვის ლეველის არჩევის საშუელების მიცემასა და შესაბამისი აღწერის გამოტანას შემდეგი მექანიზმით განვახორციელებდი: ლეველებსა და მომხმარებლის მიერ შეყვანილ რიცხვს შორის ერთი-ერთზე დამოკიდებულებას დავაწესებდი (მაგ, ღილაკი '1' - easy level, '2' - medium, 3 - 'hard' და ა.შ.), while ციკლის პირობას შევცვლიდი არა ლეველის არჩევით (რომელიც ჯობია რიცხვის შეყვანით იყოს არჩევადი და არა insert/enter-ით, რათა მოგვიანებით არჩევანის დადასტურება enter-ით მოხერხდეს), არამედ არჩევის დადასტურებით (while (!confirmed)) და ყოველი არჩევის დროს switch-case-ის საშუალებით დავწერდი ტერმინალში ლეველის აღწერის შეტყობინებასა და confimation მესიჯს. შემოთავაზებული დიზაინის ფარგლებში ამ გადაწყვეტის მინუსი ისაა რომ ლეველის აღწერა და თავად ლეველი მხოლოდ იმპლემენტატორისთვის ცნობილი ლოგიკითაა დაკავშირებული და არა რაიმე დიზაინის პრინციპებით.
dificulty ცვლადის არჩევის შემდეგ, მომხამარებელს მსგავსად ავარჩევინებდი მოძრავი ყოფილიყო საწამლავი და თაგვი თუ მხოლოდ უძრავი თაგვი. შემოვიღებდი ცვლადს და ტერმინალში შემოყვანილი ღილაკის მიხედვით ავარჩევდი Being-ის ტიპს. ჩემი ამოცანის განხორციელებას ორივე ტიპის დაფაზე, მარტივზეც და რთულზეც, შევძლებდი. დამატებითი ზღუდეების წარმოქმნაც ჩვეულებრივად დარჩებოდა. main-ის იმ მომენტებში, როცა მოძრავ და უძრავ Being-ებს შორის განსხვავება წარმოიქმნებოდა დავწერდი if else-ს საწყის ეტაპზე შენახული განმასხვავებელი ცვლადის მიხედვით. მოძრავი თაგვისა და საწამლავისთვის შევქმნიდი ახალ კლასს. ამ კლასს აუცილებლად ექნებოდა ეხლანდელი პოზიცია, წინა პოზიცია და მიმართულება. შემდეგ პოზიციაზე გადასვლა და ახალი რანდომ მიმართულების არჩევაც კლასშივე იქნებოდა იმპლემენტირებული. while(true)-მდე შევქმნიდი ახალი კლასის თითო-თითო ობიექტს თაგვისთვის და საწამლავისთვის. შევქმნიდი long tStart-ის მსგავს ცვლადს, რომელშიც ისევ დროს ავითვლიდი, ოღონდ ამჯერად თაგვის და საწამლავის მიმართულებების შესაცვლელად. ციკლის ბოლოს შევამოწმებდი 5 წამი გავიდა თუ არა და დამატებული კლასის მეთოდით შევუცვლიდი Being-ებს მიმართულებებს. while(true)-ში განსხვავებულ მომენტებს ჩავწერდი if else-ში. ასეთი მომენტები იქნებოდა გადაკვეთებზე შემოწმება და ახალი თაგვის დამატება. გადაკვეთაზე შესამოწმებლად გამოვიყენებდი ახალი კლასის წინა პოზიციის მნიშვნელობას, რადგან როცა ორ Being-ს შორის ლუწი რაოდენობის უჯრაა და ისინი ერთმანეთს უახლოვდებიან, მათი თავები ერთდროულად ერთ უჯრაში არ ხვდება და წარმოიშვება ბაგები. foodDraw მეთოდში if-ით დავამატებდი საწამლავის პოზიციაზე შემოწმებას, ანუ მეთოდს დასჭირდებოდა დამატებითი პარამეტრის გადაცემა თამაშის ტიპის დასადგენად.
თავიდან მომხმარებელს ავარჩევინებდი თამაშის როგორი რეჟიმი უნდა. მომხმარებლის მიერ შემოყვანილი inputით, შევქმნდიდი boolean ცვლადს EvilSnakeIsSelected. შემდეგ სადაც დამატებით Evil Snake-სთვის კოდი იქნება იქ მხოლოდ იმ შემთხვევაში შევა თუ EvilSnakeIsSelected არის true. ბოროტი გველის იგივენაირად შევქმნიდი როგოროც არის ჩვეულებრივი გველი. იქნებოდა ახალი Queue ბოროტო გველისთვის EvilSnakeElements, თავი EvilSnakeHead, newEvilSnakeHead, ასევე Direction ამ გველისთვისაც. იმისთვის რომ გველმა პოზიცია 5 წამში ერთხელ შეიცვალოს , შემოვიღებდი ცვლადს სადაც დამახსოვრებული იქნებოდა მიმართულების ბოლო შეცვლის დრო, მთავარ while ციკლში შევამოწმებდი თუ ახალანდელ დროს გამოკლებული მიმართულების ბოლო შეცლვის დრო იქნებოდა 5 წამზე მეტი, მაშინ Random ით მივანიჭებდი ახალ მიმართულებას. ბოროტი გველის დახატვა იქნებოდა ისეთივე როგოიც არის ჩვეულებრივი გველი. სადაც ხდება შემოწმება გველმა შეჭამა თუ არა თაგვი, იქვე უნდა მოხდეს გველმა უკბინა თუ არა ბოროტ გველს. ამისთვის დავწერდი მეთოდს HitSnake რომელიც აბრუნებს booleanს, რომელსაც გადაეცემა მთავარი გველის თავი, და ბოროტი გველის ტანი. თუ თავის და ტანის რომელიმე პოზიცია დაემთხვა, ესეიგი გველს უკბენია მეორე გველისთვის და დავაბრუნებდი trueს. ანალოგიურად გამოვიძახებდი ამ მეთოდს ბოროტი გველისთვისაც, ოღონდ ახლა მეთოდს გადავცემდი ბოროტი გველის თავს და მთავარი გველის ტანს. იმ შემთხვევაში თუ ბოროტმა გველმა მოკლა მთავარი გველი, უნდა მორჩეს პროგრამა, ანუ while ციკლიდან გამოვიდეთ break-ით. თუ პირიქით მოხდა მაშინ ბოროტი გველის ტანის Queue დაცარიელდება, და ამის შემდეგ დავხატავ მას Randomით არჩეულ ახალ ადგილას.
მოცემულ კოდში უკვე არის Ghost mouse, რომელიც აღნიშნულია @ სიმბოლოთი(Position food) და ყოველ 10500 მილიწამში იცვლის დაფაზე ადგილს. რადან ჩემს დავალებაში აღნიშნული იყო, რომ შემთხვევითი დროის ინტერვალით უნდა ხდებოდეს თაგვის სხვა ადგილას გაჩენა, შემოვიტანდი Random ობიექტს, რაც ამ პრობლემას გადაწყვეტდა (შემთხვევითად დაბრუნებულ მთელ რიცხვს 1-დან 10-მდე გავამრავლებდი 1000 მილიწამზე). Ghost poison-ს დავამატებდი იგივენაირად, როგორც კოდში უკვე არის თაგვის დამატება. თამაშის დაწყებამდე შევქმნიდი Position poison და double poisonDissapearTime ცვლადს. შემდეგ food = foodDraw(terminal, terminalSize, Grid, snakeElements) გამოვიძახებდი იგივე მეთოდს საწამლავისთვისაც poison = foodDraw(terminal, terminalSize, Grid, snakeElements), იმ განსხვავებით რომ ამ მეთოდის შემდეგ დაფაზე დავხატავდი საწამლავს terminal.putCharacter('$'). მოცემულ კოდში გველი კვდება მაშინ, როდესაც კედლებს ეჯახება, ამ შემოწმებას დავამატებდი იმ შემთხვევას, როდესაც გველი ჭამს საწამლავს if (isBitingTheGrid==true || snakeBite==true ||(snakeNewHead.col==poison.col&&snakeNewHead.row==poison.row))და შედეგად თამაში კვლავ დასრულდება და ეკრანზე გამოჩნდება შესაბამისი წარწერა და მოთამაშის დაგროვილი ქულა. საწამლავის გაქრობა-გაჩენის დროის ინტერვალს შევამოწმებდი ისევე, როგორც ეს უკვე ხდება თაგვის სხვა ადგილას გაქრობა-გაჩენის შემთხვევაში, იმ განსხვავებით , რომ თაგვის ნაცვლად შევქმნიდი საწამლავს და მის შესაბამის $ სიმბოლოს დავხატავდი დაფაზე terminal.putCharacter(' '); poison = foodDraw(terminal, terminalSize, Grid, snakeElements); terminal.putCharacter('$');
SnakeGame1-ში საერთოდ არ არის level-ების ცნება. ამის გამოსწორების შემდეგ და ლეველების არჩევის შემდეგ იქნება JFrame-ის ინიციალიზაცია (ეს ნაწილი იქნება იგივე რაც მაქვს) და შემდგომ while ბლოკი რომელიც დამთავრდება იმ შემთხვევაში თუ user აირჩევს level-ს (ესეც იგივე)
SnakeGame1 - ში ხდება კითხვა და წერა ტერმინალზე ჩემს შემთხვევაში კი საჭიროა ახალი კლასის გაკეთება, რომელიც JPanel - ის შვილობილი იქნებოდა და მოგვიწევდა public void paint(Graphics graphics);
მეთოდის გადატვირთვა, როგორც ეხლა ხდება ჩემს კოდში, რადგან მხოლოდ ჩვენ ვიცით ის, თუ როგორ უნდა მოიქცეს პანელი ხატვისას. რადგანაც სვინგზე გადაწერისას ტერმინალი ზედმეტი გახდებოდა შეცვლა მოგვიწევდა ასევე "მსმენელისა". მოგვიწევდა შეგვექმნა KeyListener ის შვილობილი კლასი, რომელსაც დავარეგისტრირებდით ჩვენს მიერვე შექმნილ JPanel ზე და გავუწერდით შემდეგ მეთოდებს:
public void keyPressed(KeyEvent e)
public void keyReleased(KeyEvent e)
public void keyTyped(KeyEvent e)
რა თქმა უნდა გვეცვლება ხატვის ფუნქციაც, ეს ცვლილება მოხდება ჩვენს მიერ შექმნილ JPanel - ის შვილობილ კლასში public void paint(Graphics graphics);
მეთოდში, ამასთანავე მოგვიწევს ამ კლასისთვის ინფორმაციის მიწოდება დაფის შესახებ და შეიძლება ობსერვერ პატერნის გამოყენება ამ შემთხვევაში. სავარაუდოდ ეს არის იმ ცვლილებების სია რომელთა განხორციელებაც მომიწევდა.
პირველ რიგში SnakeGame1 არის ერთჯერადი, ამიტომაც მთელ public void main() მეთოდს ჩავსვავდი while ციკლში, რათა User-მა გადაწყვიტოს როდის დაასრულოს და როდის არა. 185 ხაზზე წერია Write("GAME OVER", terminal). ამ ხაზის მაგივრად დავამატებდი boolean მეთოდს ContinueOrNot();. ამ მეთოდში მეწერებოდა ამგვარი ლოგიკა: ტერმინალის საშუალებით ვატყობინებ User-ს რომ თამაში დამთავრად და უნდა აირჩიოს გაგრძელება უნდა თუ თამაშის საბოლოოდ დამთავრება(დახურვა), ასევე ტერმინალში წერია რომ პირველი არჩევანისთვის საჭიროა Y და მეორე არჩევანისთვის N ან ESC. შემდეგ ვუსმენ input-ს . დაჭერილი Key-ს მიხედვით დავაბრუნებდი შესაბამისად false ან true-ს. false-შემთხვევაში main-ის მთლიან ციკლს გავწყვეტდი true-ს შემთხვევაში უბრალოდ continue; ამ ბრძანებით შემდეგ იტერაციაზე დავიწყებდი ანუ while-ციკლის თავიდან დავიწყებდი.
თამაშის შეჩერება და გაგრძელება ანალოგიურად მოხდებოდა, თამაშის მიმდარეობისას შეყვანილ key-ს შევამოწმებდი თუ space-ს დააჭირა მოთამაშემ თამაში შეჩერდება და კიდევ ერთხელ დაჭერისას გაგრძელდება. მდგომარეობის შენახვისასაც ტექნიკურად თითქმის იდენტურად მოვიქცეოდი, შევქმნიდი ფსევდო memento* კლასს, რომელიც შეინახავდა ყველა საჭირო ინფორმაციას და caretaker კლასი რომელსაც მდგომარეობის შენახვას და შენახული მდგომარეობის აღდგენაში გამოვიყენებთ. (ფაილში ჩაწერა/ წაკითხვა და აღდგენა, მაგალითად)
ფსევდო memento* - რადგან არსებული არქიტექტურა არ იძლება საშუალებას "ნამდვილი" memento პატერნის რეალიზების საშუალებას. ამ პატერნის მთავრი თავისებურება და იდეა არის ობიექტი, რომელასაც გააჩნია მდგომარეობა, რომელიც შეგვიძლია შევინახოთ და აღვადგინოთ. შეგვიძლია "შევაკოწიწოთ" თვისებები რომელიც მდგომარეობას აღწერს და შევინახოთ, ტექნიკურად არც სირთულეს წარმოადგენს და არც დიდად განსხვავებულს რეალური დავალებისგან, მაგრამ იდეურად ეს არ იქნება მემენტო პატერნი.
სირთულის არჩევის შემდეგ მომხმარებელს შევთავაზებდი სურდა თუ არა ორი მაგიური უჯრის არსებობა . თანხმობის შემთხვევა გამოვიძახები მეთოდს(რომელიც მე უნდა დავამატო) რომელიც რაღაც სიმბოლოებით შემოფარგლავდა რომელიმე რანდომად არჩეულ ორ უჯრას , შესაბამისად მომიწევდა გველის გადაადგილებაში მცირე ცვლილების შეტანა , გველის თავი თუ რომელიმე ამ უჯრაზე აღმოჩნდებოდა იძულებული ვიქნებოდი ჩვეულებრივი გადაადგილებიშტვის გვერდი ამევლო და გველი ზუსტად ისე გადამეხატა როგორც ეს ამოცანის პირობაშია აღწერილი. (ერთი მაგიური უჯრიდან მეორეში გადასვლა იგულისხმება).
ამ დავალების შესასრულებლად SnakeGame1
-ში snake კლასის main()
მეთოდში დავამატებდი ცვლადს int livesLeft
რომლის საწყისი მნიშვნელობა იქნებოდა 3
. 182 ხაზის
ქვემოთ როდესაც ვიცით , რომ გველი მოკვდა შევამხირებდი livesLeft
ცვლადს ერთით და სიცოცხლეების view
-ს განვაახლებდი, და თუ livesLeft > 0
მაშინ გავაგრძელებდი ციკლს, თუ არადა else
-ში გაგრძელდებოდა ლოგიკა რომელიც წყვეტს თამაშს და წერს ქულებს.
ამ დავალების შესასრულებლად,პირველ ყოვლისა შევქმნიდი ცალკე კლასს HighScoreData, სადაც დავწერდი მთელ ლოგიკას რაც შეეხება ტექსტურ ფაილთან ურთიერთობას(ფაილის შიგთავსის წაკითხვა,ფაილის განახლება.მსგავსი კლასი შექმნილი მაქვს დავალების პირველ ნაწილში). ასევე შემოვიღებდი სტატიკურ მეთოდს private static void highScoreView(Terminal terminal,int userPoints),რომელშიც შევქმნიდი HighScoreData ტიპის ობიექტს და წავიკითხავდი ფაილს ეკრანზე გამოსატანად,ასევე ტერმინალში დავბეჭდავდი ბოლოს დაფიქსირებულ ანგარიშსაც(userPoints),თუკი გადმოცემული ანგარიში ახალი high score-ია მაშინ ფაილს შევცვლიდი HighScoreData ობიექტზე შესაბამისი მეთოდის გამოძახებით,ასევე მოხდებოდა მომხმარებელთან ურთიერთობაც(სახელის შემოყვანა).highScoreView მეთოდის გამოძახება მოხდებოდა ამ if(isBitingTheGrid==true || snakeBite==true)-ის scope-ში და გადაეცემოდა userPoints ცვლადი,ასევე ტერმინალ ცვლადი რომელიც მეინის დასაწყისშია აღწერილი.ხოლო კოდის ის ნაწილი რომელსაც ამჟამად გამოაქვს ანგარიში ტერმინალში აღარ იქნებოდა საჭირო,რადგან ანგარიშის დაბეჭდვა ისედაც მოხდებოდა highScoreView მეთოდში სხვა high score-ებთან ერთად(დავალების პირველ ნაწილში ასე მომეთხოვებოდა).
SnakeGame1-ის არსებული არქიტექტურა არ გვაძლევს ფუნქციონალის მარტივი გადაკეთების საშუალებას, თითოეული ახალი თვისების დამატება მოითხოვს კოდის მასიურ ცვლილებას, რაც მრავალ პრობლემასთანაა დაკავშირებული.
კონკრეტულად HugeMap-ის დასამატებლად საჭირო გახდებოდა, პირველ რიგში, კამერის შექმნა ცალკე კლასად. კამერას ექნებოდა მოძრაობის მეთოდი, ასევე, მეთოდი, რომელიც გაარკვევდა კონკრეტული წერტილი არის თუ არა კამერის ხედვის არეში, და ჩვეულებრივი გეთერები. ეს საშუალებას მოგვცემს, გავაკონტროლოთ ვიზუალზე დაფის რა არე უნდა დაიხატოს. კამერის წარმატებით ინტეგრირებისთვის გარდაუვალ აუცილებლობას წარმოადგენს დაფისა და ვიზუალურად გამოსახვის კოდის განცალკევება (სასურველია ცალცალკე კლასში :)) და მთავარ კლასში მათი შესაბამისი მეთოდების გამოძახება (როგორებიცაა makeMove (მომხმარებლის ინფუთიდან მიღებული ახალი მიმართულების შესაბამისად), draw (კამერის დამატებამდე, მთელი დაფის ხატვა) და ა.შ.). ამის შემდეგ საჭირო იქნება ხატვის მეთოდის შეცვლა და მისი ისე გადაკეთება, რომ დაიხატოს მხოლოდ ის არე, რასაც კამერა ხედავს.
მთლიანობაში, კოდის დეკომპოზიცია და მისი იდეურად რამდენიმე კლასად დაყოფა, გაცილებით გაამარტივებს ახალი ფუნქციონალის დამატებას, კოდს უფრო გასაგებსა და ადვილად გასააზრებელს გახდის.
SnakeGame1-ში HungrySnake-ის დასამატებლად პირველ რიგრში შევქმნიდი Snake კლასს და main მეთოდიდან snake-ის ინფორმაციას გავიტანდი ამ კლასში. ამ კლასს დავამატებდი მეთოდს move, რომელსაც გადაეცემოდა მიმართულება და snake-ის ყველა ელემენტს შეცვლიდა შესაბამისად. ასევე snake ფაილში არსებულ drowSnake მეთოდს გადავაკეთებდი snake კლასის ობიექტისთვის. ამის შემდეგ შევქმნიდი HungrySnake კლასს, რომელიც იქნებოდა Snake-ის შვილობილი, და ამ კლასს გადავუტვირთავდი move მეთოდს, რათა დაეთვალა სვლების რაოდენობა და ყოველ მეოცე სვლაზე snake ობიექტს ერთი ელემენტი მოაკლდებოდა. (რათქმაუნდა სვლების რაოდენობა განულდებოდა თაგვის შექმის შემთხვევაში.) etc.
ეს issue იმ სახით, რომელშიც აქ უნდა დაწერილიყო, საერთოდ არ დაიწერებოდა SnakeGame1 იმპლემენტაციის პირობებში. პირველ რიგში ამის მიზეზი ისაა, რომ აქ ჩვენ ვიყენებთ Observer pattern-ს, SnakeGame1-ში კი სულ ერთადერთი კლასია, რაც პატერნის რთულ სტრუქტურას ვერანაირად ვერ დაიტევდა. იმ იმპლემენტაციის პირობებში ერთადერთი გზა, რომ გველს თაგვისკენ ემოძრავა, იყო ის, რომ უბრალოდ წასულიყო თაგვის კოორდინატებისკენ, რომელზეც წვდომა ექნებოდა, რისთვისაც თაგვის კოორდინატების გლობალურად გატანა საკმარისი იქნებოდა. მთლიანად შესაცვლელი იქნებოდა keyPressed ლოგიკა, ეს ყველაფერი ჩვენ თაგვის სამოძრავებლად დაგვჭირდებოდა და არა გველის, თუმცა ამის გაკეთება განსაკუთრებულ პრობლემებთან არ იქნებოდა დაკავშირებული. გაუგებარი იქნებოდა, რა უნდა მოეხერხებინა თაგვს obstacle-ბისთვის, სავარაუდოდ ის უბრალოდ უნდა დაბლოკილიყო, ხოლო გველი კი, რომელიც უბრალოდ თაგვისკენ იქნებოდა მომართული, საერთოდაც ძალიან ხშირად და ძალიან მარტივად დაეჯახებოდა obstacle-ებს იუზერის სწორი თამაშის პირობებში.