From 8770d1f423ba860963953720543604e122d2ee34 Mon Sep 17 00:00:00 2001 From: Mathieu GROUSSEAU Date: Sat, 8 Feb 2025 16:03:27 +0100 Subject: [PATCH] serde.rs joy and pain back again --- CLI/CLI/main.swift | 11 ++ Model/Sources/Model/Board.swift | 27 +++- Model/Sources/Model/Coords.swift | 4 + Model/Sources/Model/Game.swift | 28 +++- Model/Sources/Model/Players/HumanPlayer.swift | 2 + Model/Sources/Model/Players/Player.swift | 7 + .../Sources/Model/Players/RandomPlayer.swift | 1 + .../Sources/Model/Rules/FourInARowRules.swift | 4 +- Model/Sources/Model/Rules/Rules.swift | 15 +- .../Sources/Model/Rules/TicTacToeRules.swift | 4 +- Persistance/Package.swift | 3 +- Persistance/Sources/Persistance/Board.swift | 53 +++++++ Persistance/Sources/Persistance/Enums.swift | 25 ++++ Persistance/Sources/Persistance/Game.swift | 109 +++++++++++++++ .../Sources/Persistance/Persistance.swift | 26 ++++ Persistance/Sources/Persistance/Piece.swift | 14 ++ Persistance/Sources/Persistance/Player.swift | 131 ++++++++++++++++++ Persistance/Sources/Persistance/Rules.swift | 95 +++++++++++++ 18 files changed, 543 insertions(+), 16 deletions(-) create mode 100644 Persistance/Sources/Persistance/Board.swift create mode 100644 Persistance/Sources/Persistance/Enums.swift create mode 100644 Persistance/Sources/Persistance/Game.swift create mode 100644 Persistance/Sources/Persistance/Piece.swift create mode 100644 Persistance/Sources/Persistance/Player.swift create mode 100644 Persistance/Sources/Persistance/Rules.swift diff --git a/CLI/CLI/main.swift b/CLI/CLI/main.swift index 2c88e62..89c78f5 100644 --- a/CLI/CLI/main.swift +++ b/CLI/CLI/main.swift @@ -2,6 +2,7 @@ import Foundation import Model import CustomTypes +import Persistance enum EnumRules: String, CaseIterable { case FourInARow = "Four in a row" @@ -89,7 +90,17 @@ game.gameStateChanged.add { state in print(board.display(winCells: cells)) case .Draw: print("Its a daw.") + case .Invalid: + fatalError("Game is invalid!") } } game.play() + +var encoder = JSONEncoder() +encoder.outputFormatting = .prettyPrinted +encoder.userInfo[.gameDecodingContext] = CodableContext() + +let data = try encoder.encode(CodableGameWrapper(of: game)) + +print(String(data: data, encoding: .utf8)!) diff --git a/Model/Sources/Model/Board.swift b/Model/Sources/Model/Board.swift index ce52f9b..04a0d8a 100644 --- a/Model/Sources/Model/Board.swift +++ b/Model/Sources/Model/Board.swift @@ -42,10 +42,29 @@ public struct Board: Equatable { } } + public var cells: any Sequence { + self.grid.lazy.flatMap { (row: [Piece?]) -> LazySequence<[Piece?]> in row.lazy } + } + + public var coords: any Sequence { + (0.. Void) { - for column in grid { - for piece in column { - consumer(piece) + self.cells.forEach(consumer) + } + + @available(*, deprecated, renamed: "cells", message: "Use cells sequence instead") + public mutating func forEach(mutating consumer: (Coords, inout Piece?) -> Void) { + for column in 0.. Bool = { piece in true }) -> Int { var count = 0 - self.forEach { + self.cells.forEach { if let piece = $0 { if filter(piece) { count += 1 diff --git a/Model/Sources/Model/Coords.swift b/Model/Sources/Model/Coords.swift index ddf83d6..0547f69 100644 --- a/Model/Sources/Model/Coords.swift +++ b/Model/Sources/Model/Coords.swift @@ -12,6 +12,10 @@ public struct Coords: Equatable { self.init(pair.0, pair.1) } + public func toIndex(width: Int) -> Int { + col + row * width + } + /* static func +(lhs: Self, rhs: Self) -> Self { Self(lhs.col + rhs.col, lhs.row + rhs.row) } diff --git a/Model/Sources/Model/Game.swift b/Model/Sources/Model/Game.swift index a6703fc..5932c02 100644 --- a/Model/Sources/Model/Game.swift +++ b/Model/Sources/Model/Game.swift @@ -9,14 +9,38 @@ public class Game { public let players: [Player] public let rules: Rules + /// Has to be public for serialization purposes + public var board: Board + /// Has to be public for serialization purposes + public var state: GameState + public init(players: [Player], rules: Rules) { self.players = players self.rules = rules + + // Initialize state + self.board = rules.createBoard() + self.state = rules.gameState(board: board, last_turn: nil) } + public init?(players: [Player], rules: Rules, board: Board, state: GameState?) { + guard rules.isValid(board: board) else { return nil } + + self.state = state ?? rules.gameState(board: board, last_turn: nil) + + self.players = players + self.rules = rules + self.board = board + } + public func play() { - var board = rules.createBoard() - var state = rules.gameState(board: board, last_turn: nil) + switch state { + case .Playing: + break + + default: + return + } boardChanged(board) gameStateChanged(state) diff --git a/Model/Sources/Model/Players/HumanPlayer.swift b/Model/Sources/Model/Players/HumanPlayer.swift index 8147d50..9eceed4 100644 --- a/Model/Sources/Model/Players/HumanPlayer.swift +++ b/Model/Sources/Model/Players/HumanPlayer.swift @@ -2,6 +2,8 @@ public typealias MoveCallback = ([Move.Action], Board) -> Move.Action public class HumanPlayer : Player { private let callback: MoveCallback + + public override var type: PlayerType { .Human } public init(name: String, piece_type: PieceType, callback: @escaping MoveCallback) { self.callback = callback diff --git a/Model/Sources/Model/Players/Player.swift b/Model/Sources/Model/Players/Player.swift index 58e90cb..d5a7b2d 100644 --- a/Model/Sources/Model/Players/Player.swift +++ b/Model/Sources/Model/Players/Player.swift @@ -1,6 +1,8 @@ public class Player : Equatable { public let name: String public let piece_type: PieceType + + public var type: PlayerType { fatalError("abstract property not implemented") } public init(name: String, piece_type: PieceType) { self.name = name @@ -21,3 +23,8 @@ public class Player : Equatable { lhs === rhs } } + +public enum PlayerType: CaseIterable { + case Human + case AIRandom +} diff --git a/Model/Sources/Model/Players/RandomPlayer.swift b/Model/Sources/Model/Players/RandomPlayer.swift index eaa7ce1..3ec76c0 100644 --- a/Model/Sources/Model/Players/RandomPlayer.swift +++ b/Model/Sources/Model/Players/RandomPlayer.swift @@ -1,4 +1,5 @@ public class RandomPlayer : Player { + public override var type: PlayerType { .AIRandom } public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action { moves[Int.random(in: moves.indices)] } diff --git a/Model/Sources/Model/Rules/FourInARowRules.swift b/Model/Sources/Model/Rules/FourInARowRules.swift index e893463..a48e5ad 100644 --- a/Model/Sources/Model/Rules/FourInARowRules.swift +++ b/Model/Sources/Model/Rules/FourInARowRules.swift @@ -11,7 +11,9 @@ public struct FourInARowRules: Rules { // private(set) public var history: [Move] = [] - private let columns: Int, rows: Int, minAligned: Int + public let columns: Int, rows: Int, minAligned: Int + + public var type: RulesTypes = .FourInARow public init?(players: [Player]) { self.init(columns: Self.COLUMNS_DEFAULT, rows: Self.ROWS_DEFAULT, players: players) diff --git a/Model/Sources/Model/Rules/Rules.swift b/Model/Sources/Model/Rules/Rules.swift index ab3aad8..1f0a506 100644 --- a/Model/Sources/Model/Rules/Rules.swift +++ b/Model/Sources/Model/Rules/Rules.swift @@ -1,7 +1,5 @@ public protocol Rules { - // var state: GameState { get } - - // var history: [Move] { get } + var type: RulesTypes { get } func createBoard() -> Board @@ -14,14 +12,17 @@ public protocol Rules { func validMoves(board: Board, for_player player: Player) -> [Move.Action] func gameState(board: Board, last_turn: Player?) -> GameState - - // mutating func onMoveDone(move: Move, board: Board) -> Void } public enum GameState: Equatable { case Playing(turn: Player) - case Win(winner: Player, board: Board, cells: [Coords]) - case Draw + + /// In rare cases, the state may be invalid because the game was incorrectly reconstructed (serialization, ...) + case Invalid +} + +public enum RulesTypes: CaseIterable { + case FourInARow, TicTacToe } diff --git a/Model/Sources/Model/Rules/TicTacToeRules.swift b/Model/Sources/Model/Rules/TicTacToeRules.swift index e3050c7..b577abb 100644 --- a/Model/Sources/Model/Rules/TicTacToeRules.swift +++ b/Model/Sources/Model/Rules/TicTacToeRules.swift @@ -1,6 +1,8 @@ public struct TicTacToeRules: Rules { private let players: [Player] - private let columns: Int, rows: Int, minAligned: Int + public let columns: Int, rows: Int, minAligned: Int + + public var type: RulesTypes = .TicTacToe public init?(players: [Player]) { self.init(columns: 3, rows: 3, players: players) diff --git a/Persistance/Package.swift b/Persistance/Package.swift index ff1182b..eae8e7d 100644 --- a/Persistance/Package.swift +++ b/Persistance/Package.swift @@ -18,6 +18,7 @@ let package = Package( // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "Persistance"), + name: "Persistance", + dependencies: ["Model"]), ] ) diff --git a/Persistance/Sources/Persistance/Board.swift b/Persistance/Sources/Persistance/Board.swift new file mode 100644 index 0000000..b1ae953 --- /dev/null +++ b/Persistance/Sources/Persistance/Board.swift @@ -0,0 +1,53 @@ +import Model + +extension Board: Codable { + enum CodingKeys: CodingKey { + case width, height, content + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let width = try container.decode(Int.self, forKey: .width) + let height = try container.decode(Int.self, forKey: .height) + let pieces = try container.decode([Piece?].self, forKey: .content) + + if pieces.count != width * height { + throw DeError.InvalidData + } + + guard var zelf = Self.init(columns: width, rows: height) else { + throw DeError.InitializationFailed + } + + zelf.coords.forEach { coord in + zelf[coord] = pieces[coord.toIndex(width: width)] + } + + self = zelf + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.columns, forKey: .width) + try container.encode(self.rows, forKey: .height) + // Array is flattened because Board inner cell data structure is private and only accessible via a sequence + try container.encode(Array(self.cells), forKey: .content) + } +} + +extension Coords: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.unkeyedContainer() + + self.init(try container.decode(Int.self), try container.decode(Int.self)) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.unkeyedContainer() + + try container.encode(self.col) + try container.encode(self.row) + } +} diff --git a/Persistance/Sources/Persistance/Enums.swift b/Persistance/Sources/Persistance/Enums.swift new file mode 100644 index 0000000..9b0987c --- /dev/null +++ b/Persistance/Sources/Persistance/Enums.swift @@ -0,0 +1,25 @@ +internal struct CodableEnum: Codable { + public let variant: E + + init(of variant: E) { + self.variant = variant + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let name = try container.decode(String.self) + + guard let variant = E.allCases.first(where: { variant in + variant.stringValue == name + }) else { + throw DeError.InvalidVariant + } + + self.variant = variant + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.variant.stringValue) + } +} diff --git a/Persistance/Sources/Persistance/Game.swift b/Persistance/Sources/Persistance/Game.swift new file mode 100644 index 0000000..e56bce0 --- /dev/null +++ b/Persistance/Sources/Persistance/Game.swift @@ -0,0 +1,109 @@ +import Model + +/// This struct is a workaround as it seems codeable required initializer to be required, which is not possible from an extension +public struct CodableGameWrapper: Codable { + public let game: Game + + public init(of game: Game) { + self.game = game + } + + enum CodingKeys: CodingKey { + case players, rules, board, state + } + + public init(from decoder: any Decoder) throws { + guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext + else { throw DeError.CodableContext } + + let container = try decoder.container(keyedBy: CodingKeys.self) + + context.players = (try container.decode([CodablePlayerWrapper].self, forKey: .players)).map { $0.player } + + context.board = try container.decode(Board.self, forKey: .board) + + let rules = (try container.decode(CodableRulesWrapper.self, forKey: .rules)).rules + + let state = try container.decode(GameState.self, forKey: .state) + + guard let game = Game.init(players: context.players!, rules: rules, board: context.board!, state: state) + else { throw DeError.InitializationFailed } + + self.game = game + } + + public func encode(to encoder: any Encoder) throws { + guard let context = encoder.userInfo[.gameDecodingContext] as? CodableContext + else { throw DeError.CodableContext } + + var container = encoder.container(keyedBy: CodingKeys.self) + + context.players = self.game.players + context.board = self.game.board + + try container.encode(self.game.players.map { CodablePlayerWrapper(of: $0) }, forKey: .players) + + try container.encode(self.game.board, forKey: .board) + + try container.encode(CodableRulesWrapper(of: self.game.rules), forKey: .rules) + + try container.encode(self.game.state, forKey: .state) + } +} + +extension GameState: Codable { + enum VariantCodingKeys: CaseIterable, CodingKey { + case Playing, Win, Draw, Invalid + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self) + + self = switch (try container.decode(CodableEnum.self, forKey: .type)).variant { + case .Playing: + .Playing(turn: (try CodablePlayerWrapper(from: decoder)).player) + case .Win: + try Self.decodeWin(from: decoder) + case .Draw: + .Draw + case .Invalid: + .Invalid + } + } + + private enum ExtraCodingKeys: CodingKey { + case player, winner, cells + } + + private static func decodeWin(from decoder: any Decoder) throws -> Self { + guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext + else { throw DeError.InvalidVariant } + + let container = try decoder.container(keyedBy: ExtraCodingKeys.self) + + let winner = (try container.decode(CodablePlayerReferenceWrapper.self, forKey: .winner)).player + + let cells = try container.decode([Coords].self, forKey: .cells) + + return .Win(winner: winner, board: context.board!, cells: cells) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: EnumCommonCodingKeys.self) + var container2 = encoder.container(keyedBy: ExtraCodingKeys.self) + + switch self { + case .Playing(let turn): + try container.encode(CodableEnum(of: VariantCodingKeys.Playing), forKey: .type) + try container2.encode(CodablePlayerReferenceWrapper(of: turn), forKey: .player) + case .Win(let winner, _, let cells): + try container.encode(CodableEnum(of: VariantCodingKeys.Win), forKey: .type) + try container2.encode(CodablePlayerReferenceWrapper(of: winner), forKey: .winner) + try container2.encode(cells, forKey: .cells) + case .Draw: + try container.encode(CodableEnum(of: VariantCodingKeys.Draw), forKey: .type) + case .Invalid: + try container.encode(CodableEnum(of: VariantCodingKeys.Invalid), forKey: .type) + } + } +} diff --git a/Persistance/Sources/Persistance/Persistance.swift b/Persistance/Sources/Persistance/Persistance.swift index 8b13789..5df89cd 100644 --- a/Persistance/Sources/Persistance/Persistance.swift +++ b/Persistance/Sources/Persistance/Persistance.swift @@ -1 +1,27 @@ +import Foundation +import Model +public enum DeError: Error { + case CodableContext + case InvalidVariant + case InvalidData + case InitializationFailed +} + +extension CodingUserInfoKey { + public static let gameDecodingContext = CodingUserInfoKey(rawValue: "gameDecodingContext")! +} + +public class CodableContext { + internal var callbackFactory: ((_ name: String, _ type: PieceType) -> MoveCallback)? + internal var players: [Player]? = nil + internal var board: Board? = nil + + public init(creatingCallbacks callbackFactory: ((_ name: String, _ type: PieceType) -> MoveCallback)? = nil) { + self.callbackFactory = callbackFactory + } +} + +enum EnumCommonCodingKeys: CodingKey { + case type +} diff --git a/Persistance/Sources/Persistance/Piece.swift b/Persistance/Sources/Persistance/Piece.swift new file mode 100644 index 0000000..5629d2e --- /dev/null +++ b/Persistance/Sources/Persistance/Piece.swift @@ -0,0 +1,14 @@ +import Model + +extension Piece: Codable { + public init(from decoder: any Decoder) throws { + let player = (try CodablePlayerReferenceWrapper.init(from: decoder)).player + self = Self.init(owner: player) + } + + public func encode(to encoder: any Encoder) throws { + try CodablePlayerReferenceWrapper(of: self.owner).encode(to: encoder) + } +} + +extension PieceType: CodingKey { } diff --git a/Persistance/Sources/Persistance/Player.swift b/Persistance/Sources/Persistance/Player.swift new file mode 100644 index 0000000..0a8a233 --- /dev/null +++ b/Persistance/Sources/Persistance/Player.swift @@ -0,0 +1,131 @@ +import Model + +public struct CodablePlayerWrapper: Codable { + public let player: Player + + public init(of player: Player) { + self.player = player + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self) + + self.player = try (try container.decode(PlayerType.self, forKey: .type)).decodePlayer(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: EnumCommonCodingKeys.self) + let type = self.player.type + try container.encode(type, forKey: .type) + switch type { + case .Human: + try CodableHumanPlayerWrapper(of: self.player as! HumanPlayer).encode(to: encoder) + case .AIRandom: + try CodableRandomAIPlayerWrapper(of: self.player as! RandomPlayer).encode(to: encoder) + } + } +} + +extension PlayerType: CodingKey, Codable { + public init(from decoder: any Decoder) throws { + self = (try CodableEnum(from: decoder)).variant + } + + public func encode(to encoder: any Encoder) throws { + try CodableEnum(of: self).encode(to: encoder) + } + + public func decodePlayer(from decoder: any Decoder) throws -> Player { + switch self { + case .Human: (try CodableHumanPlayerWrapper(from: decoder)).player + case .AIRandom: (try CodableRandomAIPlayerWrapper(from: decoder)).player + } + } +} + +private enum CommonPlayerCodingKeys: CodingKey { + case name, pieces +} + +private struct CodableHumanPlayerWrapper: Codable { + let player: HumanPlayer + + init(of player: HumanPlayer) { + self.player = player + } + + init(from decoder: any Decoder) throws { + guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext + else { throw DeError.CodableContext } + + let container = try decoder.container(keyedBy: CommonPlayerCodingKeys.self) + + let name = try container.decode(String.self, forKey: .name) + let pieces = try container.decode(CodableEnum.self, forKey: .pieces) + + self.init(of: HumanPlayer(name: name, piece_type: pieces.variant, callback: context.callbackFactory!(name, pieces.variant))) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CommonPlayerCodingKeys.self) + + try container.encode(self.player.name, forKey: .name) + try container.encode(CodableEnum(of: self.player.piece_type), forKey: .pieces) + } +} + +private struct CodableRandomAIPlayerWrapper: Codable { + let player: RandomPlayer + + init(of player: RandomPlayer) { + self.player = player + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CommonPlayerCodingKeys.self) + + let name = try container.decode(String.self, forKey: .name) + let pieces = try container.decode(CodableEnum.self, forKey: .pieces) + + self.init(of: RandomPlayer(name: name, piece_type: pieces.variant)) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CommonPlayerCodingKeys.self) + + try container.encode(self.player.name, forKey: .name) + try container.encode(CodableEnum(of: self.player.piece_type), forKey: .pieces) + } +} + +public struct CodablePlayerReferenceWrapper: Codable { + public let player: Player + + public init(of player: Player) { + self.player = player + } + + public init(from decoder: any Decoder) throws { + guard + let context = decoder.userInfo[.gameDecodingContext] as? CodableContext, + let players = context.players + else { throw DeError.CodableContext } + + let container = try decoder.singleValueContainer() + + let id = try container.decode(Int.self) + guard id < players.count else { throw DeError.InvalidData } + + self.player = players[id] + } + + public func encode(to encoder: any Encoder) throws { + guard + let context = encoder.userInfo[.gameDecodingContext] as? CodableContext, + let players = context.players + else { throw DeError.CodableContext } + + var container = encoder.singleValueContainer() + try container.encode(players.firstIndex(where: { self.player === $0 })!) + } +} diff --git a/Persistance/Sources/Persistance/Rules.swift b/Persistance/Sources/Persistance/Rules.swift new file mode 100644 index 0000000..24bfa3b --- /dev/null +++ b/Persistance/Sources/Persistance/Rules.swift @@ -0,0 +1,95 @@ +import Model + +public struct CodableRulesWrapper: Codable { + public let rules: Rules + + public init(of rules: Rules) { + self.rules = rules + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self) + + self.rules = try (try container.decode(RulesTypes.self, forKey: .type)).decodeRules(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: EnumCommonCodingKeys.self) + let type = self.rules.type + try container.encode(type, forKey: .type) + try (self.rules as! Encodable).encode(to: encoder) + } +} + +extension RulesTypes: CodingKey, Codable { + public init(from decoder: any Decoder) throws { + self = (try CodableEnum(from: decoder)).variant + } + + public func encode(to encoder: any Encoder) throws { + try CodableEnum(of: self).encode(to: encoder) + } + + public func decodeRules(from decoder: any Decoder) throws -> Rules { + switch self { + case .FourInARow: try FourInARowRules(from: decoder) + case .TicTacToe: try TicTacToeRules(from: decoder) + } + } +} + +private enum CommonRulesCodingKeys: CodingKey { + case minAligned +} + +extension FourInARowRules: Codable { + public init(from decoder: any Decoder) throws { + guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext else { + throw DeError.CodableContext + } + + let container = try decoder.container(keyedBy: CommonRulesCodingKeys.self) + + guard let zelf = Self.init( + columns: context.board!.columns, + rows: context.board!.rows, + minAligned: try container.decode(Int.self, forKey: .minAligned), + players: context.players! + ) else { + throw DeError.InitializationFailed + } + + self = zelf + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CommonRulesCodingKeys.self) + try container.encode(self.minAligned, forKey: .minAligned) + } +} + +extension TicTacToeRules: Codable { + public init(from decoder: any Decoder) throws { + guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext else { + throw DeError.CodableContext + } + + let container = try decoder.container(keyedBy: CommonRulesCodingKeys.self) + + guard let zelf = Self.init( + columns: context.board!.columns, + rows: context.board!.rows, + minAligned: try container.decode(Int.self, forKey: .minAligned), + players: context.players! + ) else { + throw DeError.InitializationFailed + } + + self = zelf + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CommonRulesCodingKeys.self) + try container.encode(self.minAligned, forKey: .minAligned) + } +}