From 88edf5b23756506de803cc25ea2780a6190e62a2 Mon Sep 17 00:00:00 2001 From: Michael Kowalczyk Date: Thu, 21 Nov 2024 12:08:41 -0500 Subject: [PATCH] Day 50 --- .../Chess.class | Bin 6762 -> 7949 bytes .../Chess.java | 61 ++++++++++++++---- .../GameState.class | Bin 3455 -> 4621 bytes .../GameState.java | 53 +++++++++++---- .../Move.class | Bin 1168 -> 1700 bytes .../Move.java | 41 +++++++++--- .../Square.class | Bin 757 -> 842 bytes .../Square.java | 5 ++ .../defensive programming.txt | 21 ++++++ 9 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 Day 43, 45, 46 (chess application design)/defensive programming.txt diff --git a/Day 43, 45, 46 (chess application design)/Chess.class b/Day 43, 45, 46 (chess application design)/Chess.class index a37f7e6c040787ce3954c13b7cb31322ba49b4ff..3fffbeef59a9c59dbcb3b84c38140e1bcf68fb87 100644 GIT binary patch delta 3120 zcmZ`*X<$^<75=_SGVkSafk{}xlCUUC5-?E~HEaf>ENw!p2}BmrAw06rz|2bk1;>R5 z$|CSk0YQPVxu8N|*b!(eEpFYcZSA6MEiGN_YO6)s7S7|F8op)V+y4BevP3?X{JpN> zLb9y;j)Cu52;h`fE}D2F8E@h(e|~1$QqPK!)s;bRSdO_lYZ=(PTw3Tl>x&r;#TC#m9C z)JA;n&tGxY^P+3=g@!K$C~MA>T9rJ`)0fNyCTd8saDaY){?xY64$zPXF=k3EOtvtc zmcdS1Dv5!e_53cHb=45ikfI?~LpKfGHT2NXQ$w1DUK)B!X6&P(Z!*)Fp*{4o*q;MT z4wUlp8jII*kio$g<~4GN9v_;DL}ptY#^D;S)3zLoxx8M(4HjxSLO*e&!BHkhTUaa= z=Zz+BGI_JfJdeVYlLK*zZ;+jp*CN+li!tN;?`g zI7x9brzlS4*N8qYJ9Y0-5DYa0qY6#@x?&-V6gI+jKS;V}$R<2VbSenfN2=_|fNAz( zgVPj?d56JoDBj5u#ZpdpGyUoPXDF8OF2$Lgr8t{&6z}F-#d|c|%eij3|Avev1`Wz3TlF*m{E- z6gSdU+{7mhKBf3HHxp3Y!YKx~Dn7$yL}F36L8@~YM-;blyAt|L+^Y{%Qxi3=P%q7xvxWfp2OHmk6G&&OIk#Abpd(&~aWc0o8)9(2m8 zoJwI)Cm3>~a-7>2RtIBk4ojR!Rj6Vtk#Timp@sEMZc(^CYTw>qha|{E2}L$hi13A z*tspqwG?Agu-pkpmJ*vY-GM3n+&2RSy<3x?{gvQ~=T+O~jyor%f8rTpusbm&%{`N1 zxZ6{P#TwmVj6aVT^!(9!uzePY_0RyO4&S&@g-EAa6O43-%<@TEK^!H}jPe4-k7`3edROwVll zWPJwtCdpT3%5|cj>E`tsoYN8T3bDzt&?Dktt%RgQn|=rjvg5cjj*2)c-9Yy-vFyW0 zO|6QfTI>ZKsSJ^XAzRrQYtT#955+o+kPRCz*ZgGJraQ0+rLs-b=dRVuT=9}9t1??% z6b+AK{&plDJcb%gSJteXkUcyj4*M)>-m~ zCZsX}LtY?!`kHEWwn!IAMn`IA!{P`P4Zk3+B%RUX>*g{cd1Cg+m|-UnhBS<5ty#9X z?T45l&)6-=?m-ucBu#d0fOyT3vW}3#w)^j5$Xg6`&$ar$0K-dmxj0$a9mU~w$BdR@M0Q~CSFEL5&@(&Z7^1Nw7?+(c<}qIBFFh~>y@>n(T-Q}H&6@QxQ-0;pu$kGJ>cyWUm<8%|>J%+w`Ev9uX;J$|3Z z@A3Hk9-rm$WgegH@#P+$&M894R_}?Xqf8d|^S1bDuK5b?HZD&{T KDKlkl*8c$93)v$8 delta 2068 zcmai!d7O=96vuzRJMMkndFL^vD;Z-Z6k)QIHI*7vQb|IxyT-*Z=8l;=#*!{&NqfDG zv}#c)r5FuS(!Ohv_Dw34LfWNt&SUBG;g8;X@Av%9^PcB@&U4Q5-fK?#zC-HOf7Yx6 z(3$gmOu;fAm3Y+PF&}AIX0Y5x7%Mzf`&fy`6*UH{A_Y@rid?>YRx6%Jmw9Up*80f8 zGIgHx@Kie1;ps?0ivz*y@r>So*28l?p2rK~r0}8+y`*?q@rvSAgV!Pj9rn#Lc-=>1 zypfI#cvIC|n%vtNcn2E|-u1A_M@zit;e8Ju7G&1DDSr2{2!BKhdL2k@5&qN%rFLsBfB6`IJqCZP{YP`yEAfOVQV~+5 zDQYRgiipA}JVm;~S7gY}Xce{7naMh`0<(M`!Kh(fk4O3_l5(-0$D_Q)tnV?~V*`&l z9vgbh^_b`JXpfBystp?l!$Ui4MqzPL;e;NE;);nS)fc6WXA{3PR#cMgo`{u=CTjOf z#tJ9&j+OS0jVg{CHZeRV*ivh~SA_+Z$KuvtX}Dc>6`K;_9^>NW<%Z2Hk7IMo7Hk>( z8g5j(iusnuvz5hc%n6!AjyY~F<`Ee^5*1}d@v_F}$14q6Tee|a!xJn|WIN0D><|=1 zavOED?8MHNC$Wp=$vnmKRCcvIO>sKA28$yn=2fx4u$yIf_OLvIXIh@co`z>z_To9g zu1NE=DmD*tO>3`;=USe}-a%iJotlbi(uSU~iE-IuGHx*g(=1Yo%W;{-EL=upbx$Oc z<;k*GY43P)Tw-*2oe0m*VxKx;UXX>XMEXsth?T`Hi#XmQzOyBm5S>W*DG#N+$i8gt!D`vBn(R}C$Y@3oJqq9%gL-HvV#q2 z+11;<;o-C?oEr4W%uTE0w4fw&Y{N=Um&TRHlYL6M6()-kC34PriDE|e&djaMHN4a^ z#mg<0NOACx6a@Dw?!)4sVYDOX1bw4hBZhN{X2IrYUXXWWt5l`@Jppy)HI$ynYdKxK z5H69QwB(?Fsrb^JgZ@nMrAIDS8s@B=FlXh2iOtbUI4i|)R;uN!6wFzv8RVfkS~oW! z1#?zv=B$*>S*e_}QhjHo_F^+68F>+eH;b1imSV&CtH9+Vh=sz48>}mD{4Kauw1zej zyDxM#LXG~5^<_^$8`MHuxqlmO*ZAr&^=in4L0~x9K zL!3@HJ=Ez^r-wN`$?1_!mpL7Cy4>kePA8o%bh^UnfvW37Cp%}*{>ldPj_%U)VM#HB pN2o{K2RW0oISUQ2Mf`j@DqqPqzQ#A$x?g>dZTr=a2i10L`xmjNqzwQ7 diff --git a/Day 43, 45, 46 (chess application design)/Chess.java b/Day 43, 45, 46 (chess application design)/Chess.java index c1bd178..77784a8 100644 --- a/Day 43, 45, 46 (chess application design)/Chess.java +++ b/Day 43, 45, 46 (chess application design)/Chess.java @@ -23,7 +23,7 @@ public class Chess extends Application { //User's first click for a move. private Integer clickRow; //null means no first click made currently. - private Integer clickColumn; + private Integer clickCol; private GraphicsContext graphics; private TextArea moveHistoryTextArea; @@ -87,7 +87,7 @@ public class Chess extends Application { moveNotationHistory = new ArrayList(); //No clicks yet - clickRow = clickColumn = null; + clickRow = clickCol = null; //Set up mouse/click events board.setOnMousePressed(this::mouseClick); @@ -96,7 +96,7 @@ public class Chess extends Application { aiMove.setOnAction(this::moveAI); //Refresh the board - paintBoard(); + updateScreen(); //Fix mysterious broken width and prevent shrinking window stage.setWidth(664); @@ -106,7 +106,7 @@ public class Chess extends Application { stage.show(); } - private void paintBoard() { + private void updateScreen() { for(int x = 0; x < 8; x++) { for(int y = 0; y < 8; y++) { graphics.setFill((x + y) % 2 == 0 ? Color.BURLYWOOD : Color.FORESTGREEN); @@ -134,37 +134,76 @@ public class Chess extends Application { //Reset to the intial game position. public void reset(ActionEvent e) { - System.out.println(board().getAllPossibleMoves() ); - //Set up the initial board - //Clear out clickRo and clickCol - //Clera out all the histories. + System.out.println(board().getAllPossibleMoves()); + //Clear out all the histories + gameStateHistory = new ArrayList(); + gameStateHistory.add(new GameState()); //Set up the initial board + moveHistory = new ArrayList(); + moveNotationHistory = new ArrayList(); + + //Clear out clickRow and clickCol + clickRow = clickCol = null; + moveHistoryTextArea.setText(""); + updateScreen(); } //AI makes a move public void moveAI(ActionEvent e) { //Ask the gameState for a list of all legal moves. + ArrayList a = board().getAllPossibleMoves(); + if (a.size() <= 0) return; //no moves. + //Get one of those in the list uniformly at random. -// makeMove(...); + int r = (int)(Math.random() * a.size()); + makeMove(a.get(r)); } private void makeMove(Move move) { //Clone the gameState object for the current state of the game, tell it to make the move, add it to the move history - //add the notation for it + GameState g = board().getStateAfterMove(move); + if (g==null) return; //didn't actually move it; must not have been legal. + gameStateHistory.add(g); + //add the move to the history as well + moveHistory.add(move); + + //add the notation for it + moveNotationHistory.add(move+""); + + //Refresh the board + updateScreen(); } //For the human moving public void mouseClick(MouseEvent e) { //If it is the first click (null check) then record it, updtae the GUI to show highlighting //else attempt the move: + int x = (int)(e.getX() / 50); + int y = (int)(e.getY() / 50); -// makeMove(...); + if (board().getSquare(x,y).isWhite() == board().isWhitesTurn() && board().getSquare(x,y).getType() != Square.EMPTY) { //if clicking on own piece... + clickRow = y; + clickCol = x; + } + else if (clickRow != null) { + makeMove(new Move(clickCol, clickRow, x, y)); + clickRow = clickCol = null; + } + updateScreen(); } //Undo button for the most recent move public void undo(ActionEvent e) { //Pop those 3 history object things and update the GUI. + if (gameStateHistory.size() <= 1) return; //can't pop the initial state. + + //Pop those 3 history object things and update the GUI. + gameStateHistory.remove(gameStateHistory.size() - 1); + moveHistory.remove(moveHistory.size() - 1); + moveNotationHistory.remove(moveNotationHistory.size() - 1); + clickRow = clickCol = null; + updateScreen(); } } \ No newline at end of file diff --git a/Day 43, 45, 46 (chess application design)/GameState.class b/Day 43, 45, 46 (chess application design)/GameState.class index d1724e960460fb998ae39d91d0077a6e993ec0d9..d46ce432600f6f7ba55264eb4ea15f91974c2ff1 100644 GIT binary patch delta 2068 zcmaJ?OK%fb6#njb?6Jo~9wb9ZYzKz~aO^-JZ4HefJQ4^_obYhKnD<~0j)^nl+LH(n zl2wUa1ybZLN;X9-$fk=_ZDR@Abl*SFRV(!ebk{{eKzgoi0w}0ba_+t7eD}<`-}&zK zTE~N)(r<5`tpeDOyo?TnU1-F92?u1@@Hsbw+!K+>9F)I~QS)*9~L)co9aT6JaAP4%H-!Lsd)tt>~H*&iFrdjZ3vh%v( z&k$4oBByE(L))x2qYs-#O0yDKYno;hF{~m(N1#q*?A^)ZRv~L-A`EI^qj!u`?heFi zR8||HWOQppFIrI}t>0!K;-d@=V zO-rXGOmhx%3_b6v$r|(KjNV@>s&4m_!K19~#2(KKu=d_krDej^<^Ud~Er^$EMhC*FY- zv<$tSwT4BX@EDFIHtJjC!x3MTquk{lkZdbZSFokr={_J)gnv_%Z1aE_j?*;!^58fBp(wlNK{3I9pbLsnd-OWiS!3jPlmF{FYlBr8dWfo4VoLZHj&^amZ_wUuUk#H?B~?-# zPjIu{UQ(q!VW%hTQeCR^df3HzRb8Cou&l~eW-+MBkE(fUk$T3Kb`p16(xbX6*t&xD z#JjC@J$#F6%l9P``y=}BIyp5BU!s;@rVDxvpP_Ue2k3!6OtKwE8dLPP-=cT?E~fDn z0TwV*C8Qh9SMU-cZ#XUejCg5cBvcPmU!w5^n*LoUW-rOfhkbQ*`e@{uwR0N2qY3?# xk*x{Y1e>H=@<|=rQO61--w%NniT6|T2~_b6jWQv#n59zNsr1_Wa)f+G{{wi)s+|A; delta 992 zcmZuvOHUI~6#i}>b34QIB@EEgDguQ*kZOSfEr^JMXo)6(2yPglVi6TfAt5oT3pOrX zkXfM%35j8;i3So$NZc9|P2Bhs+`1z~i{};siE(ky<2&CybI&*DMcsjSeMoPQ@kksJM(?2Fu{%rQ-c7Dz2iBLGITdrs?asG=rr3Mk6bM(L@zwE8oAhx;gV0pVSJO!HX&Z8U=fG z-g86*0)+YC7Zt+190eOO6b(YuBY{q)0u@e|fON~n-OnmA!;OjteE z-MD0qbB$}#2Btk$jW@+@Rhn%gE(;knS?CZ~L|jFoN`dJ$iGi4=6vaVEQ`S&+Og>_i zdMrDzk5y^%HXK`Uj;?T)-#mEbv78JZhC=V@g-i=rgFXb&PorNWdmXoM1NV@^64jnk zR!z(XNC&hdHVzl#2+-leF`z)QFlN*C;M_Z&zn*@012sKfj2a66w;3aZheQS8ztv}< ACIA2c diff --git a/Day 43, 45, 46 (chess application design)/GameState.java b/Day 43, 45, 46 (chess application design)/GameState.java index 10fa681..41b255b 100644 --- a/Day 43, 45, 46 (chess application design)/GameState.java +++ b/Day 43, 45, 46 (chess application design)/GameState.java @@ -32,15 +32,36 @@ public class GameState { } + //Construct a new GameState based on a previous state and a move made on that previous state. + private GameState(GameState prevState, Move move) { + ArrayList moves = prevState.getAllPossibleMoves(); + if (!moves.contains(move)) { + throw new IllegalArgumentException("Move "+move+" not available for given game state."); + } + + isWhitesTurn = !prevState.isWhitesTurn; + boardSquares = new Square[prevState.boardSquares.length]; + for(int i = 0; i < boardSquares.length; i++) { + boardSquares[i] = new Square(prevState.boardSquares[i]); + } + boardSquares[move.getDestIndex()] = new Square(boardSquares[move.getSourceIndex()]); + boardSquares[move.getSourceIndex()] = new Square(Square.EMPTY); + + } + + // zero-based index for col and row. Note that -1 and -2 are valid indices into the array, just OUT_OF_BOUNDS. public Square getSquare(int col, int row) { return boardSquares[12*2 + 2 + col + row * 12]; } public GameState getStateAfterMove(Move m) { - //See if it is legal. If not, return null. - //Create a new object based on this one, update the board state accordingly. - return null; + try { + return new GameState(this, m); //Create a new object based on this one, update the board state accordingly. + } + catch (IllegalArgumentException e) { + return null; //If it is not a legal move, return null. + } } public ArrayList getAllPossibleMoves() { @@ -49,6 +70,7 @@ public class GameState { //Loop through all squares for(int index = 0; index < boardSquares.length; index++) { int type = boardSquares[index].getType(); + boolean w = boardSquares[index].isWhite(); if (type == Square.OUT_OF_BOUNDS) continue; if (type == Square.EMPTY) continue; @@ -58,6 +80,8 @@ public class GameState { if (type == Square.BISHOP) moves.addAll(findAllMovesFor(index, new int[] {13,-13,11,-11}, true)); if (type == Square.ROOK) moves.addAll(findAllMovesFor(index, new int[] {1,-1,12,-12}, true)); if (type == Square.KNIGHT) moves.addAll(findAllMovesFor(index, new int[] {10,14,23,25,-10,-14,-23,-25}, false)); + if (type == Square.PAWN && w) moves.addAll(findAllMovesFor(index, new int[] {12}, false)); + if (type == Square.PAWN && !w) moves.addAll(findAllMovesFor(index, new int[] {-12}, false)); } return moves; @@ -65,21 +89,28 @@ public class GameState { private ArrayList findAllMovesFor(int index, int[] directions, boolean canMoveMultipleSquares) { ArrayList moves = new ArrayList(); + if (boardSquares[index].isWhite() != isWhitesTurn) return moves; //don't move other person's pieces! + //loop through the directions, see if they work - return the resulting arraylist. for(int dir : directions) { - int destIndex = index + dir; - int type = boardSquares[destIndex].getType(); - if (boardSquares[index].isWhite() != isWhitesTurn) continue; //don't move other person's pieces! - - if (type == Square.OUT_OF_BOUNDS) continue; - if (type == Square.EMPTY || boardSquares[destIndex].isWhite() != isWhitesTurn) { - moves.add(new Move(index, destIndex)); - } + int destIndex = index; + do { + destIndex += dir; + int type = boardSquares[destIndex].getType(); + if (type == Square.OUT_OF_BOUNDS) break; + if (type == Square.EMPTY || boardSquares[destIndex].isWhite() != isWhitesTurn) { + moves.add(new Move(index, destIndex)); + } + } while(boardSquares[destIndex].getType() == Square.EMPTY && canMoveMultipleSquares); } return moves; } + public boolean isWhitesTurn() { + return isWhitesTurn; + } + // returns something like "checkmate", "stalemate", etc. public String getFeedback() { return null; diff --git a/Day 43, 45, 46 (chess application design)/Move.class b/Day 43, 45, 46 (chess application design)/Move.class index f4ea5be09400313368f21f0d8de1a42addb14efc..e71a805b006da1192c4bbd926450ce7f3bd70072 100644 GIT binary patch literal 1700 zcmaJ>&2Jk;9DU3Jdq~`Dca3)( z=zjvY-njGt5)D!h2(J7|2qE5hH)M%dm9=l?H}mGr?`!<$KQDg+a2p*PMO-Yv!6gm# zJQ_BdShZ$Bk7 zXg4z3fhS-YzGJOs=nC4Yq;%T4kZU= zlz6q<>ks7DyL%SibMQXa9h}92gC#6GcvH_7HB_-6P`)1oDpX^4v@O+MFbGRIwDP#) z-~%w_roM%&;{YT71ej2kBC&B4Z6TcWD99`boYoaf1Q&5yHtymc>nX$)Ap{nlxcjml z_F;sh;h0666Gqaw|OnkUAtCx0%g^zR$4+Uz+&JX>AU|+^Xif-I+`zjbeweYb( zZYv1(CnI)3$M@y9?YUtn>Bjd-H)4$_U7M~y@MKtv4z^}pORW6w;G?G_nL__6!9VJR z#8NELc0F%rsAS8+roe@n_|kk6SbjAR=lCv*e3lu0Y$hUfKM_wsu*WLI*J)iR!{AtI z{tD5ozCfnA_yWeyTw<>iD=>)`F^>X&bFilZ^Rd7tk7voKO$8Rq&R(^f?Uv7UwbZ;x zd+ErYQFW+Vq#JWMPuJLCScp}ZI0~v}o4+9QoRi3z+;maHqP~X+iCZS*@Dg zLu7wCk(yO9-@@B5f1Q=l@vbzRhsYhm;*dAH+A2hrZFXO>BXCmZ8qb|$zM{Ojag#Jx z|A6xYYOBA)*}lvi>O4L}g;Rr5;~6aTN0`R*X&o|&{<*;py2+Njg*yMk&eNwNjK6Ww if^$#&Mbkiyn85`+;HX>dJ~q&1@!p}Gr~NL!>;D3*S5Q;{ delta 605 zcmY*VO-~b16g_vQ!*qrz#Zt=0AhckyGX?55CMIg)ieOyWh3g4vDgrY}8FlGjki4HD zYom*5!os*V`WyHs#&ZV~^dg`Bd++g&-)G+dY~#KUA4?uQTvb^12`dUs zg_go0;-xX{&kPB!5NjwA%S2tD&##k( zK<%$Ui?_oJM+Gjw`Sqf7h5xh^m!tXSycEu=>I6O27Ts9ERGojB24h@f!94npS!9g6 zEtt#+vrDxV#^$l047ImdS!*=nFDTI*+62u#<1qL{=3>5!PTE|;B4ZXpt^}M@a1(cN Oi+XGH%k;0&+J69sw@MWN diff --git a/Day 43, 45, 46 (chess application design)/Move.java b/Day 43, 45, 46 (chess application design)/Move.java index 9cd2fe9..081a502 100644 --- a/Day 43, 45, 46 (chess application design)/Move.java +++ b/Day 43, 45, 46 (chess application design)/Move.java @@ -1,25 +1,33 @@ public class Move { //source and dest coordinates - int startIndex; + int sourceIndex; int destIndex; - - public Move(int startIndex, int destIndex) { - this.startIndex = startIndex; + public Move(int sourceIndex, int destIndex) { + this.sourceIndex = sourceIndex; this.destIndex = destIndex; } - public Move(int sourceRow, int sourceColumn, int destinationRow, int destinationColumn) { - //TODO + public Move(int sourceColumn, int sourceRow, int destColumn, int destRow) { + sourceIndex = sourceRow * 12 + sourceColumn + 2 + 12*2; + destIndex = destRow * 12 + destColumn + 2 + 12*2; + } + + public int getSourceIndex() { + return sourceIndex; + } + + public int getDestIndex() { + return destIndex; } public int getSourceRow() { - return startIndex / 12 - 2; + return sourceIndex / 12 - 2; } public int getSourceCol() { - return startIndex % 12 - 2; + return sourceIndex % 12 - 2; } public int getDestRow() { @@ -31,10 +39,23 @@ public class Move { return destIndex % 12 - 2; } + private String letterFor(int col) { + return "abcdefgh".substring(col, col+1); + } - //returns something like e2-e4 or Qc8++ + //returns something like e2-e4 public String toString() { - return "("+getSourceRow()+","+getSourceCol() + ") -> " + "("+getDestRow()+","+getDestCol() + ")"; + return letterFor(getSourceCol())+(getSourceRow()+1) + "-" + letterFor(getDestCol())+(getDestRow()+1); + } + + public boolean equals(Object obj) { + if (obj instanceof Move) { + Move move = (Move)obj; + return (move.sourceIndex == sourceIndex && move.destIndex == destIndex); + } + else { + return false; //not even a move object + } } } diff --git a/Day 43, 45, 46 (chess application design)/Square.class b/Day 43, 45, 46 (chess application design)/Square.class index 937a38a483d20667f498ae6e6042941999b9e34f..6f730945163856e990bf5abc9364a57025b397c0 100644 GIT binary patch delta 336 zcmZvX%}T>S9K>fgAGK*vDUD*%w2fVBnpOpSD5dmXLA(gP_0WS4pwb5kIriw)i(nB1 zeSo}&kKk3DjfGxh;kUzocV}k5t=v}m&-*8UAub%)h|93h)kqwoS3e|_nekP3mVQn5&s`j0$Ayo;MIBK#5g#`;1a(L@Xgz}D0=(o6BI&(Ct|9eq ztYN(HK%hxhVUjh{gNp;I!@{PO3F*zv?M0_>^Vv2iE<~H00p|uS_KKW9ht&T`2$|48 xnVGw2@#!(z*`tgu6}3WpW2Vasnpp|^K2!Xa)IS4a(#J`*lB&|D?r%#D_u#j(+pZP6;cN?tlZ3zF1 z7lewhgdJL1J;kYd!>Bn>eFiQWNau_yXNzSn-KA*K`Y)q?= -90 && lat <= 90) : "Latitude out of bounds"; + + if (lat > 90) lat = 90; + if (lat < -90) lat = -90; + \ No newline at end of file