From 7ecd1c4f5bd0784da6cfc94a3f80dfbe7de0d8e3 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 00:11:18 +0300 Subject: [PATCH 01/15] feat: implement Cube interface for RubicsCube --- .../hse/java/practice/task1/EdgePosition.java | 2 +- .../hse/java/practice/task1/EdgeRotation.java | 104 ++++++++++++++++++ .../hse/java/practice/task1/PartsIndex.java | 5 + .../hse/java/practice/task1/RubiksCube.java | 57 +++++++++- 4 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 src/main/java/hse/java/practice/task1/EdgeRotation.java create mode 100644 src/main/java/hse/java/practice/task1/PartsIndex.java diff --git a/src/main/java/hse/java/practice/task1/EdgePosition.java b/src/main/java/hse/java/practice/task1/EdgePosition.java index f96cdf3c..f7ff700d 100644 --- a/src/main/java/hse/java/practice/task1/EdgePosition.java +++ b/src/main/java/hse/java/practice/task1/EdgePosition.java @@ -14,5 +14,5 @@ public enum EdgePosition { LEFT, RIGHT, FRONT, - BACK + BACK; } diff --git a/src/main/java/hse/java/practice/task1/EdgeRotation.java b/src/main/java/hse/java/practice/task1/EdgeRotation.java new file mode 100644 index 00000000..00bfc51d --- /dev/null +++ b/src/main/java/hse/java/practice/task1/EdgeRotation.java @@ -0,0 +1,104 @@ +package hse.java.practice.task1; + +/** + * Рассматриваем кубик Рубика как подгруппу симметрической группы S48, + * которая действует на 48 стикерах (по 8 стикеров на каждой из 6 граней, центры неподвижны). + * Поворот грани = это перестановка, которая циклически меняет 4 стикера на грани и 12 стикеров на соседних гранях. + * Отсюда и получаем такие страшные массивы циклов...Подробнее на вики + */ +public class EdgeRotation { + private static final int STICKERS_ON_EDGE = 8; + + public static final int[][] LEFT_CLOCKWISE = { + {16, 18, 23, 21}, + {17, 20, 22, 19}, + {0, 32, 8, 47}, + {3, 35, 11, 44}, + {5, 37, 13, 42} + }; + public static final int[][] LEFT_COUNTERCLOCKWISE = reverseDirection(LEFT_CLOCKWISE); + + public static final int[][] FRONT_CLOCKWISE = { + {32, 34, 39, 37}, + {33, 36, 38, 35}, + {5, 24, 10, 23}, + {6, 27, 9, 20}, + {7, 29, 8, 18} + }; + public static final int[][] FRONT_COUNTERCLOCKWISE = reverseDirection(FRONT_CLOCKWISE); + + public static final int[][] RIGHT_CLOCKWISE = { + {24, 26, 31, 29}, + {25, 28, 30, 27}, + {2, 34, 10, 45}, + {4, 36, 12, 43}, + {7, 39, 15, 40} + }; + public static final int[][] RIGHT_COUNTERCLOCKWISE = reverseDirection(RIGHT_CLOCKWISE); + + public static final int[][] BACK_CLOCKWISE = { + {40, 42, 47, 45}, + {41, 44, 46, 43}, + {2, 16, 15, 31}, + {1, 19, 14, 28}, + {0, 21, 13, 26} + }; + public static final int[][] BACK_COUNTERCLOCKWISE = reverseDirection(BACK_CLOCKWISE); + + public static final int[][] UP_CLOCKWISE = { + {0, 2, 7, 5}, + {1, 4, 6, 3}, + {40, 24, 32, 16}, + {41, 25, 33, 17}, + {42, 26, 34, 18} + }; + public static final int[][] UP_COUNTERCLOCKWISE = reverseDirection(UP_CLOCKWISE); + + public static final int[][] DOWN_CLOCKWISE = { + {8, 10, 15, 13}, + {9, 12, 14, 11}, + {37, 29, 45, 21}, + {38, 30, 46, 22}, + {39, 31, 47, 23} + }; + public static final int[][] DOWN_COUNTERCLOCKWISE = reverseDirection(DOWN_CLOCKWISE); + + private EdgeRotation() { + } + + private static int[][] reverseDirection(int[][] rotation) { + var result = new int[rotation.length][]; + + for (int i = 0; i < rotation.length; i++) { + var row = rotation[i]; + var reversedRow = new int[row.length]; + for (int j = 0; j < row.length; j++) { + reversedRow[j] = row[(j + 3) % row.length]; + } + result[i] = reversedRow; + } + + return result; + } + + /** + * По индексу стикера возвращает индекс грани, на которой он находится. + * + * @param stickerIndex индекс стикера от 0 до 47 включительно + * @return индекс грани от 0 до 5 включительно + */ + public static int getEdgeIndexByStickerIndex(int stickerIndex) { + return stickerIndex / STICKERS_ON_EDGE; + } + + /** + * По индексу стикера возвращает индекс части грани, на которой он находится. + * + * @param stickerIndex индекс стикера от 0 до 47 включительно + * @return пара индексов, где первый индекс - это строка, а второй - это столбец + */ + public static PartsIndex stickerIndexToPartsIndex(int stickerIndex) { + int stickerIndexOnEdge = stickerIndex % STICKERS_ON_EDGE; + return new PartsIndex(stickerIndexOnEdge / 3, stickerIndex % 3); + } +} diff --git a/src/main/java/hse/java/practice/task1/PartsIndex.java b/src/main/java/hse/java/practice/task1/PartsIndex.java new file mode 100644 index 00000000..1ef0a303 --- /dev/null +++ b/src/main/java/hse/java/practice/task1/PartsIndex.java @@ -0,0 +1,5 @@ +package hse.java.practice.task1; + +/// Позиция внутри грани куба. +public record PartsIndex(int row, int column) { +} diff --git a/src/main/java/hse/java/practice/task1/RubiksCube.java b/src/main/java/hse/java/practice/task1/RubiksCube.java index 2091b657..a9f7d76b 100644 --- a/src/main/java/hse/java/practice/task1/RubiksCube.java +++ b/src/main/java/hse/java/practice/task1/RubiksCube.java @@ -2,11 +2,13 @@ import java.util.Arrays; +import static hse.java.practice.task1.EdgeRotation.*; + /** * Необходимо реализовать интерфейс Cube * При повороте передней грани, меняются верх низ право и лево */ -public class RubiksCube { +public class RubiksCube implements Cube { private static final int EDGES_COUNT = 6; @@ -26,10 +28,61 @@ public RubiksCube() { } } + private void applyCycle(int[] cycle) { + CubeColor temp = getSticker(cycle[cycle.length - 1]); + for (int i = cycle.length - 2; i > 0; i--) { + setSticker(cycle[i], getSticker(cycle[i - 1])); + } + setSticker(cycle[0], temp); + } + + private CubeColor getSticker(int stickerIndex) { + var index = stickerIndexToPartsIndex(stickerIndex); + return edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()]; + } + + private void setSticker(int stickerIndex, CubeColor color) { + var index = stickerIndexToPartsIndex(stickerIndex); + edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()] = color; + } + + private void rotate(RotateDirection direction, int[][] clockwiseRotation, int[][] counterClockwiseRotation) { + int[][] rotation = (direction == RotateDirection.CLOCKWISE) ? clockwiseRotation : counterClockwiseRotation; + for (int[] cycle : rotation) { + applyCycle(cycle); + } + } + + @Override + public void up(RotateDirection direction) { + rotate(direction, EdgeRotation.UP_CLOCKWISE, EdgeRotation.UP_COUNTERCLOCKWISE); + } + + @Override + public void down(RotateDirection direction) { + rotate(direction, EdgeRotation.DOWN_CLOCKWISE, EdgeRotation.DOWN_COUNTERCLOCKWISE); + } + + @Override + public void left(RotateDirection direction) { + rotate(direction, EdgeRotation.LEFT_CLOCKWISE, EdgeRotation.LEFT_COUNTERCLOCKWISE); + } + + @Override + public void right(RotateDirection direction) { + rotate(direction, EdgeRotation.RIGHT_CLOCKWISE, EdgeRotation.RIGHT_COUNTERCLOCKWISE); + } + + @Override public void front(RotateDirection direction) { + rotate(direction, EdgeRotation.FRONT_CLOCKWISE, EdgeRotation.FRONT_COUNTERCLOCKWISE); + } + @Override + public void back(RotateDirection direction) { + rotate(direction, EdgeRotation.BACK_CLOCKWISE, EdgeRotation.BACK_COUNTERCLOCKWISE); } - + public Edge[] getEdges() { return edges; } From 32fdbacda4f89b921b8566c4efa0312816a1c0d8 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 13:14:53 +0300 Subject: [PATCH 02/15] lecture 3 --- .../java/lectures/lecture3/examples/Box.java | 14 + .../lectures/lecture3/examples/Generics.java | 43 +++ .../java/lectures/lecture3/examples/Main.java | 18 ++ .../lectures/lecture3/examples/Methods.java | 10 + .../lecture3/examples/TryCatchThrows.java | 79 +++++ .../java/lectures/lecture3/tasks/atm/Atm.java | 19 +- .../hse/java/practice/task1/RubiksCube.java | 30 +- .../lectures/lecture3/tasks/atm/AtmTest.java | 38 +-- .../java/practice/task1/CubeSimpleTest.java | 277 ++++++++++++++++++ 9 files changed, 495 insertions(+), 33 deletions(-) create mode 100644 src/main/java/hse/java/lectures/lecture3/examples/Box.java create mode 100644 src/main/java/hse/java/lectures/lecture3/examples/Generics.java create mode 100644 src/main/java/hse/java/lectures/lecture3/examples/Main.java create mode 100644 src/main/java/hse/java/lectures/lecture3/examples/Methods.java create mode 100644 src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java create mode 100644 src/test/java/hse/java/practice/task1/CubeSimpleTest.java diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Box.java b/src/main/java/hse/java/lectures/lecture3/examples/Box.java new file mode 100644 index 00000000..6c3ccf57 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Box.java @@ -0,0 +1,14 @@ +package hse.java.lectures.lecture3.examples; + +public class Box { + + private T item; + + public void put(T item) { + this.item = item; + } + + public T get() { + return item; + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Generics.java b/src/main/java/hse/java/lectures/lecture3/examples/Generics.java new file mode 100644 index 00000000..4fe8a270 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Generics.java @@ -0,0 +1,43 @@ +package hse.java.lectures.lecture3.examples; + +import java.util.ArrayList; +import java.util.List; + +public class Generics { + + + + public T getType(T type) { + System.out.println(type.getClass()); + return type; + } + + public static void write(List list, T value) { + list.add(value); + } + + public static > T get(T value) { + return value; + } + + public static void testGenerics() { + // get(1.).compareTo() + write(List.of(1), 1); + write(List.of(), 1.2); + List ints = new ArrayList<>(); + ints.add(1); + ints.add(2); + // PECS + List nums = ints; + List si = ints; + si.add(1); + si.get(1); + } + + public static void main(String[] args) { + List il = new ArrayList<>(); + write(il, 5); + System.out.println(il); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Main.java b/src/main/java/hse/java/lectures/lecture3/examples/Main.java new file mode 100644 index 00000000..d4279be4 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Main.java @@ -0,0 +1,18 @@ +package hse.java.lectures.lecture3.examples; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +public class Main { + public static void main(String[] args) { + Box box = new Box<>(); + Box dBox = new Box<>(); + Callable callable; + + int x = new Integer(3); + Integer y = 5; + + Box nBox = box; + new Methods().get(new ArrayList()).add(1); + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Methods.java b/src/main/java/hse/java/lectures/lecture3/examples/Methods.java new file mode 100644 index 00000000..cac365d7 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Methods.java @@ -0,0 +1,10 @@ +package hse.java.lectures.lecture3.examples; + +public class Methods { + + + public T get(T type) { + return type; + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java b/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java new file mode 100644 index 00000000..0d972ac3 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java @@ -0,0 +1,79 @@ +package hse.java.lectures.lecture3.examples; + +import java.io.*; + +public class TryCatchThrows { + + public void testRuntime() throws RuntimeException { + throw new RuntimeException("Test runtime"); + } + + public void testException() throws Exception { + throw new Exception("test exception"); + } + + public void testError() throws Error { + throw new Error("test error"); + } + + public void testTryRuntime() { + try { + testRuntime(); + } catch (RuntimeException e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public void testTryException() { + try { + testException(); + } catch (Exception e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public void testTryError() { + // testError(); + try { + testError(); + } catch (Error e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public static void foo() { + throw new RuntimeException("My runtime"); + } + + public static void bar() throws Exception { + throw new Exception("My exception"); + } + + public static void main(String[] args) { + // foo(); +// try { +// bar(); +// } catch (Exception e) { +// System.err.println("Hello exception"); +// } finally { +// +// } + +// try (BufferedReader br = new BufferedReader(new FileReader(""))) { +// +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + + // int x = 1/ 0; + int[] a = new int[1]; + System.out.println(a[2]); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java index 3fa91909..08f551e4 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java @@ -1,14 +1,9 @@ package hse.java.lectures.lecture3.tasks.atm; -import java.util.Collections; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; public class Atm { - private enum Denomination { + public enum Denomination { D50(50), D100(100), D500(500), @@ -24,6 +19,12 @@ private enum Denomination { int value() { return value; } + + public static Denomination fromInt(int value) { + return Arrays.stream(values()).filter(v -> v.value == value) + .findFirst() + .orElse(null); + } } private final Map banknotes = new EnumMap<>(Denomination.class); @@ -31,9 +32,9 @@ int value() { public Atm() { } - public void deposit(Map banknotes){} + public void deposit(Map banknotes){} - public Map withdraw(int amount) { + public Map withdraw(int amount) { return Map.of(); } diff --git a/src/main/java/hse/java/practice/task1/RubiksCube.java b/src/main/java/hse/java/practice/task1/RubiksCube.java index 2091b657..d986f9f0 100644 --- a/src/main/java/hse/java/practice/task1/RubiksCube.java +++ b/src/main/java/hse/java/practice/task1/RubiksCube.java @@ -6,7 +6,7 @@ * Необходимо реализовать интерфейс Cube * При повороте передней грани, меняются верх низ право и лево */ -public class RubiksCube { +public class RubiksCube implements Cube { private static final int EDGES_COUNT = 6; @@ -26,10 +26,36 @@ public RubiksCube() { } } + @Override + public void up(RotateDirection direction) { + + } + + @Override + public void down(RotateDirection direction) { + + } + + @Override + public void left(RotateDirection direction) { + + } + + @Override + public void right(RotateDirection direction) { + + } + + @Override public void front(RotateDirection direction) { } - + + @Override + public void back(RotateDirection direction) { + + } + public Edge[] getEdges() { return edges; } diff --git a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java index 9e3994a6..5e83fcae 100644 --- a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java +++ b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.stream.Stream; +import static hse.java.lectures.lecture3.tasks.atm.Atm.Denomination.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -30,18 +31,11 @@ void initialBalanceIsZero() { @Test void depositIncreasesBalance() { Atm atm = new Atm(); - atm.deposit(Map.of(100, 10, 500, 5)); + atm.deposit(Map.of(D100, 10, D500, 5)); assertEquals(3500, atm.getBalance()); } - @Test - void depositRejectsInvalidDenominationAndKeepsState() { - Atm atm = new Atm(); - atm.deposit(Map.of(100, 1)); - assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(30, 1))); - assertEquals(100, atm.getBalance()); - } - + @Test void depositRejectsNullMap() { Atm atm = new Atm(); @@ -51,8 +45,8 @@ void depositRejectsNullMap() { @Test void depositRejectsNonPositiveCountAndKeepsState() { Atm atm = new Atm(); - atm.deposit(Map.of(100, 1)); - assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(100, 0))); + atm.deposit(Map.of(D100, 1)); + assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(D100, 0))); assertEquals(100, atm.getBalance()); } @@ -60,11 +54,11 @@ void depositRejectsNonPositiveCountAndKeepsState() { void withdrawGreedyAndUpdatesBalance() { Atm atm = new Atm(); // 2000 - atm.deposit(Map.of(1000, 1, 500, 1, 100, 5)); + atm.deposit(Map.of(D1000, 1, D500, 1, D100, 5)); - Map result = atm.withdraw(1700); + Map result = atm.withdraw(1700); - assertEquals(Map.of(1000, 1, 500, 1, 100, 2), result); + assertEquals(Map.of(D1000, 1, D500, 1, D100, 2), result); assertEquals(300, atm.getBalance()); } @@ -78,14 +72,14 @@ void withdrawRejectsInvalidAmount() { @Test void withdrawRejectsInsufficientFunds() { Atm atm = new Atm(); - atm.deposit(Map.of(100, 2)); + atm.deposit(Map.of(D100, 2)); assertThrows(InsufficientFundsException.class, () -> atm.withdraw(300)); } @Test void withdrawRejectsUnmakeableAmountAndKeepsState() { Atm atm = new Atm(); - atm.deposit(Map.of(500, 1, 100, 1)); + atm.deposit(Map.of(D500, 1, D100, 1)); assertThrows(CannotDispenseException.class, () -> atm.withdraw(150)); assertEquals(600, atm.getBalance()); } @@ -96,7 +90,7 @@ void additionalTests(AtmCase atmCase) { Atm atm = new Atm(); for (Map deposit : atmCase.deposits) { - atm.deposit(toIntMap(deposit)); + atm.deposit(toMap(deposit)); } if (atmCase.expect.exception != null) { @@ -105,8 +99,8 @@ void additionalTests(AtmCase atmCase) { "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } else { - Map result = atm.withdraw(atmCase.withdraw); - assertEquals(toIntMap(atmCase.expect.dispense), result, "Case: " + atmCase.name); + Map result = atm.withdraw(atmCase.withdraw); + assertEquals(toMap(atmCase.expect.dispense), result, "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } } @@ -126,10 +120,10 @@ private static List loadCases() throws IOException { } } - private Map toIntMap(Map source) { - Map result = new HashMap<>(); + private Map toMap(Map source) { + Map result = new HashMap<>(); for (Map.Entry entry : source.entrySet()) { - result.put(Integer.parseInt(entry.getKey()), entry.getValue()); + result.put(Atm.Denomination.fromInt(Integer.parseInt(entry.getKey())), entry.getValue()); } return result; } diff --git a/src/test/java/hse/java/practice/task1/CubeSimpleTest.java b/src/test/java/hse/java/practice/task1/CubeSimpleTest.java new file mode 100644 index 00000000..b13c4af7 --- /dev/null +++ b/src/test/java/hse/java/practice/task1/CubeSimpleTest.java @@ -0,0 +1,277 @@ +package hse.java.practice.task1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public class CubeSimpleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void printTest() { + RubiksCube cube = new RubiksCube(); + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(color, colors[i]); + } + } + } + } + + @Test + void frontClockwise() { + RubiksCube cube = new RubiksCube(); + cube.front(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("frontClockwieseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void frontCounterclockwise() { + RubiksCube cube = new RubiksCube(); + // Поворот против часовой стрелки = 3 поворота по часовой стрелке + cube.front(RotateDirection.COUNTERCLOCKWISE); + + // Проверяем, что 4 поворота против часовой возвращают в исходное состояние + cube.front(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void upClockwise() { + RubiksCube cube = new RubiksCube(); + cube.up(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("upClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void upCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void downClockwise() { + RubiksCube cube = new RubiksCube(); + cube.down(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("downClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void downCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void leftClockwise() { + RubiksCube cube = new RubiksCube(); + cube.left(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("leftClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void leftCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void rightClockwise() { + RubiksCube cube = new RubiksCube(); + cube.right(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("rightClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void rightCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void backClockwise() { + RubiksCube cube = new RubiksCube(); + cube.back(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("backClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void backCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void combinedRotations() { + RubiksCube cube = new RubiksCube(); + + // Выполняем последовательность поворотов + cube.front(RotateDirection.CLOCKWISE); + cube.right(RotateDirection.CLOCKWISE); + cube.up(RotateDirection.CLOCKWISE); + + // Выполняем обратную последовательность + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + + // Должны вернуться в исходное состояние + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + private CubeColor[][][] readStateFromFile(String fileName) { + String resourcePath = "hse/java/practice/task1/" + fileName; + try (InputStream is = CubeSimpleTest.class.getClassLoader().getResourceAsStream(resourcePath)) { + if (is == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + return MAPPER.readValue(json, CubeColor[][][].class); + } catch (IOException e) { + throw new RuntimeException("Failed to read/parse state file: " + fileName, e); + } + } +} From 4d8f99add7ea332d992260dfd34e3f5a46f1c6a9 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 18:08:33 +0300 Subject: [PATCH 03/15] practice 2: RandomSet --- .github/workflows/pr-tests.yml | 38 ++++++++++++-- commander/commander.iml | 9 ---- pom.xml | 9 +++- .../java/lectures/lecture3/examples/Main.java | 1 + .../practice/randomSet/EmptySetException.java | 7 +++ .../practice/randomSet/RandomSet.java | 21 ++++++++ .../lecture3/practice/randomSet/task.md | 50 +++++++++++++++++++ .../practice/randomSet/RandomSetBaseTest.java | 28 +++++++++++ 8 files changed, 148 insertions(+), 15 deletions(-) delete mode 100644 commander/commander.iml create mode 100644 src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java create mode 100644 src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java create mode 100644 src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md create mode 100644 src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 8c507688..7272e99b 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -36,13 +36,41 @@ jobs: - name: Copy test files run: | - echo "Copying test data" - mkdir -p src/test/resources + echo "Copying hidden tests" + mkdir -p src/test/java src/test/resources + cp -R hidden-tests/src/test/java/. src/test/java/ cp -R hidden-tests/src/test/resources/. src/test/resources/ echo "" echo "Скопированы файлы:" - find src/test/resources -type f | sort | while read f; do echo " $f"; done + find src/test/java src/test/resources -type f | sort | while read f; do echo " $f"; done rm -rf hidden-tests - + + - name: Run honors no-collections test + id: honors_test + continue-on-error: true + run: mvn -B -Dtest=hse.java.lectures.lecture3.practice.randomSet.RandomSetBytecodeTest test + + - name: Comment on PR for honors solution + if: steps.honors_test.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.info('No PR context, skipping comment.'); + return; + } + const body = [ + 'Поздравляем! 🎉', + '', + 'Вы решили сложную версию: решение прошло проверку «без коллекций».' + ].join('\\n'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + - name: Run tests - run: mvn -B test + run: mvn -B -Dtest='!**/RandomSetBytecodeTest' test diff --git a/commander/commander.iml b/commander/commander.iml deleted file mode 100644 index 68a9707e..00000000 --- a/commander/commander.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index ade43fe2..648f8788 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,13 @@ ${junit.version} test + + + org.ow2.asm + asm + 9.8 + test + com.fasterxml.jackson.core @@ -65,4 +72,4 @@ - \ No newline at end of file + diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Main.java b/src/main/java/hse/java/lectures/lecture3/examples/Main.java index d4279be4..116ff543 100644 --- a/src/main/java/hse/java/lectures/lecture3/examples/Main.java +++ b/src/main/java/hse/java/lectures/lecture3/examples/Main.java @@ -14,5 +14,6 @@ public static void main(String[] args) { Box nBox = box; new Methods().get(new ArrayList()).add(1); + } } diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java new file mode 100644 index 00000000..c7820ac5 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java @@ -0,0 +1,7 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +public class EmptySetException extends RuntimeException { + public EmptySetException(String message) { + super(message); + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java new file mode 100644 index 00000000..8af477b5 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java @@ -0,0 +1,21 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +public class RandomSet { + + public boolean insert(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean remove(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean contains(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public T getRandom() { + throw new UnsupportedOperationException("Not implemented"); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md new file mode 100644 index 00000000..05e9740b --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md @@ -0,0 +1,50 @@ +# Случайное множество (RandomSet) + +## Условие + +Реализовать класс `RandomSet`, который хранит множество целых чисел и поддерживает операции вставки, удаления и получения случайного элемента. + +## Требования к асимптотике + +- `insert(x)` — **O(log n) or O(1)** +- `remove(x)` — **O(log n) or O(1)** +- `contains(x)` — **(log n) or O(1)** +- `getRandom()` — **O(1)** + +Не допускается использование стандартных коллекций Java. + +## Публичный API + +1. `RandomSet()` — создаёт пустое множество. +1. `boolean insert(T value)` — добавляет `value` в множество. + - Возвращает `true`, если элемент добавлен. + - Возвращает `false`, если элемент уже был в множестве. +1. `boolean remove(T value)` — удаляет `value` из множества. + - Возвращает `true`, если элемент был удалён. + - Возвращает `false`, если элемента не было. +1. `boolean contains(T value)` — проверяет наличие `value` в множестве. + - Возвращает `true`, если элемент есть. + - Возвращает `false`, если элемента нет. +1. `T getRandom()` — возвращает случайный элемент из множества. + - Если множество пустое, выбрасывает `EmptySetException`. + +## Исключения + +- `EmptySetException` — попытка получить случайный элемент из пустого множества. + +## Пример + +```java +RandomSet set = new RandomSet<>(); +set.insert(10); // true +set.insert(20); // true +set.insert(10); // false + +int x = set.getRandom(); // 10 или 20 + +set.remove(10); // true +set.remove(10); // false + +set.contains(20); // true +set.contains(30); // false +``` diff --git a/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java b/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java new file mode 100644 index 00000000..37c38ab1 --- /dev/null +++ b/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java @@ -0,0 +1,28 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RandomSetBaseTest { + + @Test + void insertRemoveContains() { + RandomSet set = new RandomSet<>(); + + assertTrue(set.insert(10)); + assertTrue(set.insert(20)); + assertFalse(set.insert(10)); + + assertTrue(set.contains(10)); + assertTrue(set.contains(20)); + assertFalse(set.contains(30)); + + assertTrue(set.remove(10)); + assertFalse(set.remove(10)); + + assertFalse(set.contains(10)); + assertTrue(set.contains(20)); + } + +} From 478a8786566f9004c63f67fc06fff6a46519b66a Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 18:25:21 +0300 Subject: [PATCH 04/15] fix ci --- .github/workflows/pr-tests.yml | 59 ++++++++++++++++++- README-commit-format.md | 20 +++++++ .../lecture3/practice/randomSet/task.md | 2 + .../java/lectures/lecture3/tasks/atm/task.md | 2 + .../java/lectures/lecture3/tasks/html/task.md | 2 + src/main/java/hse/java/practice/task1/task.md | 2 + 6 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 README-commit-format.md diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 7272e99b..b296bc45 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -45,9 +45,37 @@ jobs: find src/test/java src/test/resources -type f | sort | while read f; do echo " $f"; done rm -rf hidden-tests + - name: Select test pattern from PR labels + id: select_tests + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.setOutput('pattern', ''); + return; + } + const repo = { owner: context.repo.owner, repo: context.repo.repo }; + const { data: commit } = await github.rest.repos.getCommit({ + ...repo, + ref: pr.head.sha + }); + const msg = (commit.commit.message || '').toLowerCase(); + + const patterns = []; + const add = (p) => { if (!patterns.includes(p)) patterns.push(p); }; + + if (msg.startsWith('atm:')) add('**/atm/*Test'); + if (msg.startsWith('html:')) add('**/html/*Test'); + if (msg.startsWith('randomset:')) add('**/randomSet/*Test'); + + // If no task prefix, run all tests + core.setOutput('pattern', patterns.length ? patterns.join(',') : ''); + - name: Run honors no-collections test id: honors_test continue-on-error: true + if: steps.select_tests.outputs.pattern == '**/randomSet/*Test' run: mvn -B -Dtest=hse.java.lectures.lecture3.practice.randomSet.RandomSetBytecodeTest test - name: Comment on PR for honors solution @@ -73,4 +101,33 @@ jobs: }); - name: Run tests - run: mvn -B -Dtest='!**/RandomSetBytecodeTest' test + run: | + if [ -n "${{ steps.select_tests.outputs.pattern }}" ]; then + echo "Running selected tests: ${{ steps.select_tests.outputs.pattern }}" + mvn -B -Dtest='${{ steps.select_tests.outputs.pattern }}' test + else + echo "No task prefix found. Skipping tests." + fi + + - name: Comment on PR when no task prefix + if: steps.select_tests.outputs.pattern == '' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.info('No PR context, skipping comment.'); + return; + } + const body = [ + 'Тесты не запускались.', + '', + 'Укажите задачу в начале сообщения коммита:', + '`atm: ...`, `html: ...`, `randomset: ...`' + ].join('\\n'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); diff --git a/README-commit-format.md b/README-commit-format.md new file mode 100644 index 00000000..24c170a4 --- /dev/null +++ b/README-commit-format.md @@ -0,0 +1,20 @@ +# Формат коммитов для запуска тестов + +## Зачем это нужно +CI запускает тесты выборочно. Чтобы он знал, какие тесты запускать, сообщение коммита должно начинаться с названия задачи. + +## Формат +Используйте префикс в начале сообщения коммита: + +- `atm: ...` +- `html: ...` +- `randomset: ...` + +Тег задачи указывается в описании каждой задачи + +Пример: +``` +randomset: implement getRandom and contains +``` + +Если префикс не указан, CI **не запускает тесты** и оставляет комментарий в PR. diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md index 05e9740b..fb987780 100644 --- a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md @@ -1,5 +1,7 @@ # Случайное множество (RandomSet) +Tag: randomset + ## Условие Реализовать класс `RandomSet`, который хранит множество целых чисел и поддерживает операции вставки, удаления и получения случайного элемента. diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md b/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md index 3845672e..6d574e19 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md @@ -1,5 +1,7 @@ # Банкомат (ATM) +Tag: atm + ## Условие Реализовать класс `Atm`, который хранит купюры фиксированных номиналов и выполняет операции пополнения и выдачи наличных. При ошибках операции должны завершаться выбросом исключения без изменения состояния банкомата. diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/html/task.md b/src/main/java/hse/java/lectures/lecture3/tasks/html/task.md index 4ec71b18..2160175f 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/html/task.md +++ b/src/main/java/hse/java/lectures/lecture3/tasks/html/task.md @@ -1,5 +1,7 @@ # Валидация HTML-документа +Tag: html + ## Условие Реализовать класс `HtmlDocument`, который читает HTML-текст из файла и проверяет корректность структуры. Если документ невалиден, при создании объекта выбрасывается исключение. diff --git a/src/main/java/hse/java/practice/task1/task.md b/src/main/java/hse/java/practice/task1/task.md index 235cc660..60dc4533 100644 --- a/src/main/java/hse/java/practice/task1/task.md +++ b/src/main/java/hse/java/practice/task1/task.md @@ -1,5 +1,7 @@ Необходимо реализовать методы поворота кубика Рубика. +Tag: cube + Кубик представляет собой классический куб 3×3×3: - 6 граней; - каждая грань состоит из 9 элементов (стикеров); From f38eb12110f50ce3f8b18b525464953b7dcc8d48 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 18:27:32 +0300 Subject: [PATCH 05/15] fix ci --- .github/workflows/pr-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index b296bc45..aa378cae 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -68,6 +68,7 @@ jobs: if (msg.startsWith('atm:')) add('**/atm/*Test'); if (msg.startsWith('html:')) add('**/html/*Test'); if (msg.startsWith('randomset:')) add('**/randomSet/*Test'); + if (msg.startsWith('cube:')) add('**/CubeSimpleTest'); // If no task prefix, run all tests core.setOutput('pattern', patterns.length ? patterns.join(',') : ''); From 96778a4bc9c92207c03f34343ccc272cb2f8c260 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 18:33:08 +0300 Subject: [PATCH 06/15] fix ci --- .github/workflows/pr-tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index aa378cae..da9457f0 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -45,7 +45,7 @@ jobs: find src/test/java src/test/resources -type f | sort | while read f; do echo " $f"; done rm -rf hidden-tests - - name: Select test pattern from PR labels + - name: Select test pattern from commit message id: select_tests uses: actions/github-script@v7 with: @@ -70,7 +70,6 @@ jobs: if (msg.startsWith('randomset:')) add('**/randomSet/*Test'); if (msg.startsWith('cube:')) add('**/CubeSimpleTest'); - // If no task prefix, run all tests core.setOutput('pattern', patterns.length ? patterns.join(',') : ''); - name: Run honors no-collections test @@ -93,7 +92,7 @@ jobs: 'Поздравляем! 🎉', '', 'Вы решили сложную версию: решение прошло проверку «без коллекций».' - ].join('\\n'); + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -107,7 +106,8 @@ jobs: echo "Running selected tests: ${{ steps.select_tests.outputs.pattern }}" mvn -B -Dtest='${{ steps.select_tests.outputs.pattern }}' test else - echo "No task prefix found. Skipping tests." + echo "No task prefix found. Failing." + exit 1 fi - name: Comment on PR when no task prefix @@ -124,8 +124,8 @@ jobs: 'Тесты не запускались.', '', 'Укажите задачу в начале сообщения коммита:', - '`atm: ...`, `html: ...`, `randomset: ...`' - ].join('\\n'); + 'atm: ..., html: ..., randomset: ...' + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 7913bd6d3118c20ae2a286087920953d0fa9f8bf Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 7 Feb 2026 18:37:15 +0300 Subject: [PATCH 07/15] fix CI --- .github/workflows/pr-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index da9457f0..d9a5bdfc 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -104,7 +104,7 @@ jobs: run: | if [ -n "${{ steps.select_tests.outputs.pattern }}" ]; then echo "Running selected tests: ${{ steps.select_tests.outputs.pattern }}" - mvn -B -Dtest='${{ steps.select_tests.outputs.pattern }}' test + mvn -B -Dtest='${{ steps.select_tests.outputs.pattern }},!**/RandomSetBytecodeTest' test else echo "No task prefix found. Failing." exit 1 From c2ee8200794f3a1dfb1629ddde0fd39ebdc520e8 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 22:31:00 +0300 Subject: [PATCH 08/15] atm: solved --- .../java/lectures/lecture3/tasks/atm/Atm.java | 78 +++++++++- .../lecture3/tasks/html/HtmlDocument.java | 143 +++++++++++++++++- 2 files changed, 211 insertions(+), 10 deletions(-) diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java index 3fa91909..e8bbe81c 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java @@ -26,19 +26,85 @@ int value() { } } - private final Map banknotes = new EnumMap<>(Denomination.class); + private Map banknotes = new EnumMap<>(Denomination.class); + + private Denomination getDenominationByValue(int value) { + for (Denomination denomination : Denomination.values()) { + if (denomination.value() == value) { + return denomination; + } + } + throw new IllegalArgumentException("Invalid denomination value: " + value); + } public Atm() { } - public void deposit(Map banknotes){} + public void deposit(Map bills) { + if (bills == null) { + throw new InvalidDepositException("Bills map cannot be null"); + } + + for (Map.Entry entry : bills.entrySet()) { + int denominationValue = entry.getKey(); + int count = entry.getValue(); + + if (count <= 0) { + throw new InvalidDepositException("Count must be positive for denomination: " + denominationValue); + } + + Denomination denomination; + try { + denomination = getDenominationByValue(denominationValue); + } catch (IllegalArgumentException e) { + throw new InvalidDepositException("Invalid denomination: " + denominationValue); + } + + banknotes.put(denomination, banknotes.getOrDefault(denomination, 0) + count); + } + } public Map withdraw(int amount) { - return Map.of(); + if (amount <= 0) { + throw new InvalidAmountException("Amount must be positive"); + } + + if (amount > getBalance()) { + throw new InsufficientFundsException("Not enough funds in the ATM"); + } + + Map result = new HashMap<>(); + var banknoteCopy = new EnumMap<>(banknotes); + + + for (int i = Denomination.values().length - 1; i >= 0; i--) { + Denomination denomination = Denomination.values()[i]; + int availableCount = banknotes.getOrDefault(denomination, 0); + int neededCount = amount / denomination.value(); + int countToDispense = Math.min(availableCount, neededCount); + + if (countToDispense > 0) { + result.put(denomination.value(), countToDispense); + amount -= countToDispense * denomination.value(); + banknotes.put(denomination, availableCount - countToDispense); + } + } + + if (amount != 0) { + banknotes = banknoteCopy; + throw new CannotDispenseException("Not enough funds in the ATM"); + } + + return result; } public int getBalance() { - return 0; + int result = 0; + for (Map.Entry entry : banknotes.entrySet()) { + Denomination denomination = entry.getKey(); + int count = entry.getValue(); + result += count * denomination.value(); + } + return result; } - -} +} \ No newline at end of file diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java index 1ddf27bf..b82e54fc 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java @@ -4,12 +4,18 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Set; +import java.util.*; public class HtmlDocument { + private static final Map allowedTags = Map.of( + "", "", + "", "", + "", "", + "
", "
", + "

", "

" + ); + public HtmlDocument(String filePath) { this(Path.of(filePath)); } @@ -27,6 +33,135 @@ private String readFile(Path filePath) { } } - private void validate(String content){} + private String ignoreAttributes(String tag) { + String lowerTag = tag.toLowerCase(); + for (String allowed : allowedTags.keySet()) { + var expected = allowed.substring(1, allowed.length() - 1); + if (lowerTag.startsWith("<" + expected)) { + return "<" + expected + ">"; + } + if (lowerTag.startsWith(""; + } + } + return null; + } + + private List extractRootTags(String content) { + List rootTags = new ArrayList<>(); + int pos = 0; + int depth = 0; + + while (pos < content.length()) { + int open = content.indexOf('<', pos); + if (open == -1) break; + + int close = content.indexOf('>', open); + if (close == -1) break; + + String tag = content.substring(open, close + 1).toLowerCase(); + pos = close + 1; + + if (tag.startsWith(""); + int htmlClosePos = content.indexOf(""); + + if (htmlOpenPos == -1 || htmlClosePos == -1 || htmlOpenPos >= htmlClosePos) { + throw new InvalidStructureException("Html tag is needed and must be properly opened before closed."); + } + + for (String openTag : allowedTags.keySet()) { + checkTagPosition(content, openTag, htmlOpenPos, htmlClosePos); + } + for (String closeTag : allowedTags.values()) { + checkTagPosition(content, closeTag, htmlOpenPos, htmlClosePos); + } + + String innerHtml = content.substring(htmlOpenPos + "".length(), htmlClosePos); + List rootTags = extractRootTags(innerHtml); + + if (rootTags.size() != 2) { + throw new InvalidStructureException("Inside there must be exactly followed by ."); + } + + if (!"".equals(rootTags.get(0))) { + throw new InvalidStructureException(" must be the first child of ."); + } + + if (!"".equals(rootTags.get(1))) { + throw new InvalidStructureException(" must follow as second child of ."); + } + } + + private void checkTagPosition(String content, String tag, int htmlOpenPos, int htmlClosePos) { + int index = -1; + while ((index = content.indexOf(tag, index + 1)) != -1) { + if (index < htmlOpenPos || index > htmlClosePos) { + throw new InvalidStructureException( + "Tag '" + tag + "' must be inside ..., but found at position " + index); + } + } + } + + private void validate(String content) { + Stack stack = new Stack<>(); + int pos = 0; + + while (pos < content.length()) { + int tagStart = content.indexOf('<', pos); + int tagEnd = content.indexOf('>', tagStart); + if (tagStart == -1 || tagEnd == -1) break; + + String tag = content.substring(tagStart, tagEnd + 1); + + var normalizedTag = ignoreAttributes(tag); + + if (normalizedTag == null) { + throw new UnsupportedTagException(tag); + } + + if (allowedTags.containsKey(normalizedTag)) { + stack.push(normalizedTag.toLowerCase()); + } else if (normalizedTag.startsWith(" Date: Sat, 7 Feb 2026 00:11:18 +0300 Subject: [PATCH 09/15] feat: implement Cube interface for RubicsCube # Conflicts: # src/main/java/hse/java/practice/task1/RubiksCube.java --- .../hse/java/practice/task1/EdgePosition.java | 2 +- .../hse/java/practice/task1/EdgeRotation.java | 104 ++++++++++++++++++ .../hse/java/practice/task1/PartsIndex.java | 5 + .../hse/java/practice/task1/RubiksCube.java | 39 ++++++- 4 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 src/main/java/hse/java/practice/task1/EdgeRotation.java create mode 100644 src/main/java/hse/java/practice/task1/PartsIndex.java diff --git a/src/main/java/hse/java/practice/task1/EdgePosition.java b/src/main/java/hse/java/practice/task1/EdgePosition.java index f96cdf3c..f7ff700d 100644 --- a/src/main/java/hse/java/practice/task1/EdgePosition.java +++ b/src/main/java/hse/java/practice/task1/EdgePosition.java @@ -14,5 +14,5 @@ public enum EdgePosition { LEFT, RIGHT, FRONT, - BACK + BACK; } diff --git a/src/main/java/hse/java/practice/task1/EdgeRotation.java b/src/main/java/hse/java/practice/task1/EdgeRotation.java new file mode 100644 index 00000000..00bfc51d --- /dev/null +++ b/src/main/java/hse/java/practice/task1/EdgeRotation.java @@ -0,0 +1,104 @@ +package hse.java.practice.task1; + +/** + * Рассматриваем кубик Рубика как подгруппу симметрической группы S48, + * которая действует на 48 стикерах (по 8 стикеров на каждой из 6 граней, центры неподвижны). + * Поворот грани = это перестановка, которая циклически меняет 4 стикера на грани и 12 стикеров на соседних гранях. + * Отсюда и получаем такие страшные массивы циклов...Подробнее на вики + */ +public class EdgeRotation { + private static final int STICKERS_ON_EDGE = 8; + + public static final int[][] LEFT_CLOCKWISE = { + {16, 18, 23, 21}, + {17, 20, 22, 19}, + {0, 32, 8, 47}, + {3, 35, 11, 44}, + {5, 37, 13, 42} + }; + public static final int[][] LEFT_COUNTERCLOCKWISE = reverseDirection(LEFT_CLOCKWISE); + + public static final int[][] FRONT_CLOCKWISE = { + {32, 34, 39, 37}, + {33, 36, 38, 35}, + {5, 24, 10, 23}, + {6, 27, 9, 20}, + {7, 29, 8, 18} + }; + public static final int[][] FRONT_COUNTERCLOCKWISE = reverseDirection(FRONT_CLOCKWISE); + + public static final int[][] RIGHT_CLOCKWISE = { + {24, 26, 31, 29}, + {25, 28, 30, 27}, + {2, 34, 10, 45}, + {4, 36, 12, 43}, + {7, 39, 15, 40} + }; + public static final int[][] RIGHT_COUNTERCLOCKWISE = reverseDirection(RIGHT_CLOCKWISE); + + public static final int[][] BACK_CLOCKWISE = { + {40, 42, 47, 45}, + {41, 44, 46, 43}, + {2, 16, 15, 31}, + {1, 19, 14, 28}, + {0, 21, 13, 26} + }; + public static final int[][] BACK_COUNTERCLOCKWISE = reverseDirection(BACK_CLOCKWISE); + + public static final int[][] UP_CLOCKWISE = { + {0, 2, 7, 5}, + {1, 4, 6, 3}, + {40, 24, 32, 16}, + {41, 25, 33, 17}, + {42, 26, 34, 18} + }; + public static final int[][] UP_COUNTERCLOCKWISE = reverseDirection(UP_CLOCKWISE); + + public static final int[][] DOWN_CLOCKWISE = { + {8, 10, 15, 13}, + {9, 12, 14, 11}, + {37, 29, 45, 21}, + {38, 30, 46, 22}, + {39, 31, 47, 23} + }; + public static final int[][] DOWN_COUNTERCLOCKWISE = reverseDirection(DOWN_CLOCKWISE); + + private EdgeRotation() { + } + + private static int[][] reverseDirection(int[][] rotation) { + var result = new int[rotation.length][]; + + for (int i = 0; i < rotation.length; i++) { + var row = rotation[i]; + var reversedRow = new int[row.length]; + for (int j = 0; j < row.length; j++) { + reversedRow[j] = row[(j + 3) % row.length]; + } + result[i] = reversedRow; + } + + return result; + } + + /** + * По индексу стикера возвращает индекс грани, на которой он находится. + * + * @param stickerIndex индекс стикера от 0 до 47 включительно + * @return индекс грани от 0 до 5 включительно + */ + public static int getEdgeIndexByStickerIndex(int stickerIndex) { + return stickerIndex / STICKERS_ON_EDGE; + } + + /** + * По индексу стикера возвращает индекс части грани, на которой он находится. + * + * @param stickerIndex индекс стикера от 0 до 47 включительно + * @return пара индексов, где первый индекс - это строка, а второй - это столбец + */ + public static PartsIndex stickerIndexToPartsIndex(int stickerIndex) { + int stickerIndexOnEdge = stickerIndex % STICKERS_ON_EDGE; + return new PartsIndex(stickerIndexOnEdge / 3, stickerIndex % 3); + } +} diff --git a/src/main/java/hse/java/practice/task1/PartsIndex.java b/src/main/java/hse/java/practice/task1/PartsIndex.java new file mode 100644 index 00000000..1ef0a303 --- /dev/null +++ b/src/main/java/hse/java/practice/task1/PartsIndex.java @@ -0,0 +1,5 @@ +package hse.java.practice.task1; + +/// Позиция внутри грани куба. +public record PartsIndex(int row, int column) { +} diff --git a/src/main/java/hse/java/practice/task1/RubiksCube.java b/src/main/java/hse/java/practice/task1/RubiksCube.java index d986f9f0..a9f7d76b 100644 --- a/src/main/java/hse/java/practice/task1/RubiksCube.java +++ b/src/main/java/hse/java/practice/task1/RubiksCube.java @@ -2,6 +2,8 @@ import java.util.Arrays; +import static hse.java.practice.task1.EdgeRotation.*; + /** * Необходимо реализовать интерфейс Cube * При повороте передней грани, меняются верх низ право и лево @@ -26,34 +28,59 @@ public RubiksCube() { } } + private void applyCycle(int[] cycle) { + CubeColor temp = getSticker(cycle[cycle.length - 1]); + for (int i = cycle.length - 2; i > 0; i--) { + setSticker(cycle[i], getSticker(cycle[i - 1])); + } + setSticker(cycle[0], temp); + } + + private CubeColor getSticker(int stickerIndex) { + var index = stickerIndexToPartsIndex(stickerIndex); + return edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()]; + } + + private void setSticker(int stickerIndex, CubeColor color) { + var index = stickerIndexToPartsIndex(stickerIndex); + edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()] = color; + } + + private void rotate(RotateDirection direction, int[][] clockwiseRotation, int[][] counterClockwiseRotation) { + int[][] rotation = (direction == RotateDirection.CLOCKWISE) ? clockwiseRotation : counterClockwiseRotation; + for (int[] cycle : rotation) { + applyCycle(cycle); + } + } + @Override public void up(RotateDirection direction) { - + rotate(direction, EdgeRotation.UP_CLOCKWISE, EdgeRotation.UP_COUNTERCLOCKWISE); } @Override public void down(RotateDirection direction) { - + rotate(direction, EdgeRotation.DOWN_CLOCKWISE, EdgeRotation.DOWN_COUNTERCLOCKWISE); } @Override public void left(RotateDirection direction) { - + rotate(direction, EdgeRotation.LEFT_CLOCKWISE, EdgeRotation.LEFT_COUNTERCLOCKWISE); } @Override public void right(RotateDirection direction) { - + rotate(direction, EdgeRotation.RIGHT_CLOCKWISE, EdgeRotation.RIGHT_COUNTERCLOCKWISE); } @Override public void front(RotateDirection direction) { - + rotate(direction, EdgeRotation.FRONT_CLOCKWISE, EdgeRotation.FRONT_COUNTERCLOCKWISE); } @Override public void back(RotateDirection direction) { - + rotate(direction, EdgeRotation.BACK_CLOCKWISE, EdgeRotation.BACK_COUNTERCLOCKWISE); } public Edge[] getEdges() { From 5d4af8cd1449d4d28f003ca139c1aafb9b6d37e9 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 22:31:00 +0300 Subject: [PATCH 10/15] atm: solved --- .../java/lectures/lecture3/tasks/atm/Atm.java | 93 ++++++++++-- .../lecture3/tasks/html/HtmlDocument.java | 143 +++++++++++++++++- 2 files changed, 218 insertions(+), 18 deletions(-) diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java index 08f551e4..e8bbe81c 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java @@ -1,9 +1,14 @@ package hse.java.lectures.lecture3.tasks.atm; -import java.util.*; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; public class Atm { - public enum Denomination { + private enum Denomination { D50(50), D100(100), D500(500), @@ -19,27 +24,87 @@ public enum Denomination { int value() { return value; } + } + + private Map banknotes = new EnumMap<>(Denomination.class); - public static Denomination fromInt(int value) { - return Arrays.stream(values()).filter(v -> v.value == value) - .findFirst() - .orElse(null); + private Denomination getDenominationByValue(int value) { + for (Denomination denomination : Denomination.values()) { + if (denomination.value() == value) { + return denomination; + } } + throw new IllegalArgumentException("Invalid denomination value: " + value); } - private final Map banknotes = new EnumMap<>(Denomination.class); - public Atm() { } - public void deposit(Map banknotes){} + public void deposit(Map bills) { + if (bills == null) { + throw new InvalidDepositException("Bills map cannot be null"); + } + + for (Map.Entry entry : bills.entrySet()) { + int denominationValue = entry.getKey(); + int count = entry.getValue(); + + if (count <= 0) { + throw new InvalidDepositException("Count must be positive for denomination: " + denominationValue); + } - public Map withdraw(int amount) { - return Map.of(); + Denomination denomination; + try { + denomination = getDenominationByValue(denominationValue); + } catch (IllegalArgumentException e) { + throw new InvalidDepositException("Invalid denomination: " + denominationValue); + } + + banknotes.put(denomination, banknotes.getOrDefault(denomination, 0) + count); + } } - public int getBalance() { - return 0; + public Map withdraw(int amount) { + if (amount <= 0) { + throw new InvalidAmountException("Amount must be positive"); + } + + if (amount > getBalance()) { + throw new InsufficientFundsException("Not enough funds in the ATM"); + } + + Map result = new HashMap<>(); + var banknoteCopy = new EnumMap<>(banknotes); + + + for (int i = Denomination.values().length - 1; i >= 0; i--) { + Denomination denomination = Denomination.values()[i]; + int availableCount = banknotes.getOrDefault(denomination, 0); + int neededCount = amount / denomination.value(); + int countToDispense = Math.min(availableCount, neededCount); + + if (countToDispense > 0) { + result.put(denomination.value(), countToDispense); + amount -= countToDispense * denomination.value(); + banknotes.put(denomination, availableCount - countToDispense); + } + } + + if (amount != 0) { + banknotes = banknoteCopy; + throw new CannotDispenseException("Not enough funds in the ATM"); + } + + return result; } -} + public int getBalance() { + int result = 0; + for (Map.Entry entry : banknotes.entrySet()) { + Denomination denomination = entry.getKey(); + int count = entry.getValue(); + result += count * denomination.value(); + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java index 1ddf27bf..b82e54fc 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java @@ -4,12 +4,18 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Set; +import java.util.*; public class HtmlDocument { + private static final Map allowedTags = Map.of( + "", "", + "", "", + "", "", + "
", "
", + "

", "

" + ); + public HtmlDocument(String filePath) { this(Path.of(filePath)); } @@ -27,6 +33,135 @@ private String readFile(Path filePath) { } } - private void validate(String content){} + private String ignoreAttributes(String tag) { + String lowerTag = tag.toLowerCase(); + for (String allowed : allowedTags.keySet()) { + var expected = allowed.substring(1, allowed.length() - 1); + if (lowerTag.startsWith("<" + expected)) { + return "<" + expected + ">"; + } + if (lowerTag.startsWith(""; + } + } + return null; + } + + private List extractRootTags(String content) { + List rootTags = new ArrayList<>(); + int pos = 0; + int depth = 0; + + while (pos < content.length()) { + int open = content.indexOf('<', pos); + if (open == -1) break; + + int close = content.indexOf('>', open); + if (close == -1) break; + + String tag = content.substring(open, close + 1).toLowerCase(); + pos = close + 1; + + if (tag.startsWith(""); + int htmlClosePos = content.indexOf(""); + + if (htmlOpenPos == -1 || htmlClosePos == -1 || htmlOpenPos >= htmlClosePos) { + throw new InvalidStructureException("Html tag is needed and must be properly opened before closed."); + } + + for (String openTag : allowedTags.keySet()) { + checkTagPosition(content, openTag, htmlOpenPos, htmlClosePos); + } + for (String closeTag : allowedTags.values()) { + checkTagPosition(content, closeTag, htmlOpenPos, htmlClosePos); + } + + String innerHtml = content.substring(htmlOpenPos + "".length(), htmlClosePos); + List rootTags = extractRootTags(innerHtml); + + if (rootTags.size() != 2) { + throw new InvalidStructureException("Inside there must be exactly followed by ."); + } + + if (!"".equals(rootTags.get(0))) { + throw new InvalidStructureException(" must be the first child of ."); + } + + if (!"".equals(rootTags.get(1))) { + throw new InvalidStructureException(" must follow as second child of ."); + } + } + + private void checkTagPosition(String content, String tag, int htmlOpenPos, int htmlClosePos) { + int index = -1; + while ((index = content.indexOf(tag, index + 1)) != -1) { + if (index < htmlOpenPos || index > htmlClosePos) { + throw new InvalidStructureException( + "Tag '" + tag + "' must be inside ..., but found at position " + index); + } + } + } + + private void validate(String content) { + Stack stack = new Stack<>(); + int pos = 0; + + while (pos < content.length()) { + int tagStart = content.indexOf('<', pos); + int tagEnd = content.indexOf('>', tagStart); + if (tagStart == -1 || tagEnd == -1) break; + + String tag = content.substring(tagStart, tagEnd + 1); + + var normalizedTag = ignoreAttributes(tag); + + if (normalizedTag == null) { + throw new UnsupportedTagException(tag); + } + + if (allowedTags.containsKey(normalizedTag)) { + stack.push(normalizedTag.toLowerCase()); + } else if (normalizedTag.startsWith(" Date: Sat, 7 Feb 2026 22:33:57 +0300 Subject: [PATCH 11/15] cube: rerun tests --- src/main/java/hse/java/practice/task1/EdgeRotation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hse/java/practice/task1/EdgeRotation.java b/src/main/java/hse/java/practice/task1/EdgeRotation.java index 00bfc51d..0804ef26 100644 --- a/src/main/java/hse/java/practice/task1/EdgeRotation.java +++ b/src/main/java/hse/java/practice/task1/EdgeRotation.java @@ -4,7 +4,7 @@ * Рассматриваем кубик Рубика как подгруппу симметрической группы S48, * которая действует на 48 стикерах (по 8 стикеров на каждой из 6 граней, центры неподвижны). * Поворот грани = это перестановка, которая циклически меняет 4 стикера на грани и 12 стикеров на соседних гранях. - * Отсюда и получаем такие страшные массивы циклов...Подробнее на вики + * Отсюда и получаем такие страшные массивы циклов...Подробнее на вики. */ public class EdgeRotation { private static final int STICKERS_ON_EDGE = 8; From 52f83c172acf95c523a384657c44553b01cd9ce4 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 22:38:52 +0300 Subject: [PATCH 12/15] cube: try one more time --- src/main/java/hse/java/practice/task1/PartsIndex.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/hse/java/practice/task1/PartsIndex.java b/src/main/java/hse/java/practice/task1/PartsIndex.java index 1ef0a303..a7457a52 100644 --- a/src/main/java/hse/java/practice/task1/PartsIndex.java +++ b/src/main/java/hse/java/practice/task1/PartsIndex.java @@ -1,5 +1,7 @@ package hse.java.practice.task1; -/// Позиция внутри грани куба. +/** + * Позиция внутри грани куба. + */ public record PartsIndex(int row, int column) { } From 01f4ccea627ff69810a48d6b315757cc50372259 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 22:46:47 +0300 Subject: [PATCH 13/15] cube: fix atm tests --- .../java/lectures/lecture3/tasks/atm/Atm.java | 2 +- .../lectures/lecture3/tasks/atm/AtmTest.java | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java index e8bbe81c..af541449 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java @@ -8,7 +8,7 @@ import java.util.TreeSet; public class Atm { - private enum Denomination { + public enum Denomination { D50(50), D100(100), D500(500), diff --git a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java index 5e83fcae..c74a91c0 100644 --- a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java +++ b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java @@ -31,7 +31,7 @@ void initialBalanceIsZero() { @Test void depositIncreasesBalance() { Atm atm = new Atm(); - atm.deposit(Map.of(D100, 10, D500, 5)); + atm.deposit(Map.of(100, 10, 500, 5)); assertEquals(3500, atm.getBalance()); } @@ -45,8 +45,8 @@ void depositRejectsNullMap() { @Test void depositRejectsNonPositiveCountAndKeepsState() { Atm atm = new Atm(); - atm.deposit(Map.of(D100, 1)); - assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(D100, 0))); + atm.deposit(Map.of(100, 1)); + assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(100, 0))); assertEquals(100, atm.getBalance()); } @@ -54,11 +54,11 @@ void depositRejectsNonPositiveCountAndKeepsState() { void withdrawGreedyAndUpdatesBalance() { Atm atm = new Atm(); // 2000 - atm.deposit(Map.of(D1000, 1, D500, 1, D100, 5)); + atm.deposit(Map.of(1000, 1, 500, 1, 100, 5)); - Map result = atm.withdraw(1700); + Map result = atm.withdraw(1700); - assertEquals(Map.of(D1000, 1, D500, 1, D100, 2), result); + assertEquals(Map.of(1000, 1, 500, 1, 100, 2), result); assertEquals(300, atm.getBalance()); } @@ -72,14 +72,14 @@ void withdrawRejectsInvalidAmount() { @Test void withdrawRejectsInsufficientFunds() { Atm atm = new Atm(); - atm.deposit(Map.of(D100, 2)); + atm.deposit(Map.of(100, 2)); assertThrows(InsufficientFundsException.class, () -> atm.withdraw(300)); } @Test void withdrawRejectsUnmakeableAmountAndKeepsState() { Atm atm = new Atm(); - atm.deposit(Map.of(D500, 1, D100, 1)); + atm.deposit(Map.of(500, 1, 100, 1)); assertThrows(CannotDispenseException.class, () -> atm.withdraw(150)); assertEquals(600, atm.getBalance()); } @@ -99,7 +99,7 @@ void additionalTests(AtmCase atmCase) { "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } else { - Map result = atm.withdraw(atmCase.withdraw); + var result = atm.withdraw(atmCase.withdraw); assertEquals(toMap(atmCase.expect.dispense), result, "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } @@ -120,10 +120,10 @@ private static List loadCases() throws IOException { } } - private Map toMap(Map source) { - Map result = new HashMap<>(); + private Map toMap(Map source) { + Map result = new HashMap<>(); for (Map.Entry entry : source.entrySet()) { - result.put(Atm.Denomination.fromInt(Integer.parseInt(entry.getKey())), entry.getValue()); + result.put(Integer.parseInt(entry.getKey()), entry.getValue()); } return result; } From 84c0ef5dc2b0205cc613daff692057a68e7c442d Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 23:00:26 +0300 Subject: [PATCH 14/15] cube: fix reverse direction for permutation matrixes --- .../hse/java/practice/task1/EdgeRotation.java | 20 ++++++++++++++++--- .../hse/java/practice/task1/RubiksCube.java | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/hse/java/practice/task1/EdgeRotation.java b/src/main/java/hse/java/practice/task1/EdgeRotation.java index 2a84297e..65c4b476 100644 --- a/src/main/java/hse/java/practice/task1/EdgeRotation.java +++ b/src/main/java/hse/java/practice/task1/EdgeRotation.java @@ -4,6 +4,7 @@ * Рассматриваем кубик Рубика как подгруппу симметрической группы S48, * которая действует на 48 стикерах (по 8 стикеров на каждой из 6 граней, центры неподвижны). * Поворот грани = это перестановка, которая циклически меняет 4 стикера на грани и 12 стикеров на соседних гранях. + * НА ВИКИ ПОДРОБНЕЕ. */ public class EdgeRotation { private static final int STICKERS_ON_EDGE = 8; @@ -71,8 +72,9 @@ private static int[][] reverseDirection(int[][] rotation) { for (int i = 0; i < rotation.length; i++) { var row = rotation[i]; var reversedRow = new int[row.length]; - for (int j = 0; j < row.length; j++) { - reversedRow[j] = row[(j + 3) % row.length]; + reversedRow[0] = row[0]; + for (int j = 1; j < row.length; j++) { + reversedRow[j] = row[row.length - j]; } result[i] = reversedRow; } @@ -98,6 +100,18 @@ public static int getEdgeIndexByStickerIndex(int stickerIndex) { */ public static PartsIndex stickerIndexToPartsIndex(int stickerIndex) { int stickerIndexOnEdge = stickerIndex % STICKERS_ON_EDGE; - return new PartsIndex(stickerIndexOnEdge / 3, stickerIndex % 3); + int row; + int column; + if (stickerIndexOnEdge < 3) { + row = 0; + column = stickerIndexOnEdge; + } else if (stickerIndexOnEdge < 5) { + row = 1; + column = (stickerIndexOnEdge == 3) ? 0 : 2; + } else { + row = 2; + column = stickerIndexOnEdge - 5; + } + return new PartsIndex(row, column); } } diff --git a/src/main/java/hse/java/practice/task1/RubiksCube.java b/src/main/java/hse/java/practice/task1/RubiksCube.java index a9f7d76b..6f4927f5 100644 --- a/src/main/java/hse/java/practice/task1/RubiksCube.java +++ b/src/main/java/hse/java/practice/task1/RubiksCube.java @@ -30,7 +30,7 @@ public RubiksCube() { private void applyCycle(int[] cycle) { CubeColor temp = getSticker(cycle[cycle.length - 1]); - for (int i = cycle.length - 2; i > 0; i--) { + for (int i = cycle.length - 1; i > 0; i--) { setSticker(cycle[i], getSticker(cycle[i - 1])); } setSticker(cycle[0], temp); From 534303a2fcca2cbc6760c3d76a1400e771427606 Mon Sep 17 00:00:00 2001 From: illidvn Date: Sat, 7 Feb 2026 23:10:47 +0300 Subject: [PATCH 15/15] cube: fix right clockwise --- src/main/java/hse/java/practice/task1/EdgeRotation.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/hse/java/practice/task1/EdgeRotation.java b/src/main/java/hse/java/practice/task1/EdgeRotation.java index 65c4b476..f98209c5 100644 --- a/src/main/java/hse/java/practice/task1/EdgeRotation.java +++ b/src/main/java/hse/java/practice/task1/EdgeRotation.java @@ -30,9 +30,9 @@ public class EdgeRotation { public static final int[][] RIGHT_CLOCKWISE = { {24, 26, 31, 29}, {25, 28, 30, 27}, - {2, 34, 10, 45}, - {4, 36, 12, 43}, - {7, 39, 15, 40} + {2, 45, 10, 34}, + {4, 43, 12, 36}, + {7, 40, 15, 39} }; public static final int[][] RIGHT_COUNTERCLOCKWISE = reverseDirection(RIGHT_CLOCKWISE);