Skip to content

Instantly share code, notes, and snippets.

@niaeashes
Created December 29, 2019 01:46
Show Gist options
  • Save niaeashes/2005b0292bd7aee6af53727c7fae3893 to your computer and use it in GitHub Desktop.
Save niaeashes/2005b0292bd7aee6af53727c7fae3893 to your computer and use it in GitHub Desktop.
ゲーム作る.Hex座標系.log
class HexCell {
let coordinate: HexCoordinate
private(set) var field: Field = .none
private var isFreezed: Bool = false
init(coordinate: HexCoordinate) {
self.coordinate = coordinate
}
convenience init(q: Int, r: Int) {
self.init(coordinate: HexCoordinate(q: q, r: r))
}
func freeze() {
isFreezed = true
}
func setField(_ field: Field) {
guard isFreezed == false else {
assertionFailure()
return
}
self.field = field
}
}
private let HEX_SIZE: Double = 3
private let HEX_WIDTH: Double = sqrt(3) * HEX_SIZE
private let HEX_HEIGHT: Double = 2 * HEX_SIZE
private let HEX_HORIZONTAL_SPACE: Double = HEX_WIDTH
private let HEX_VERTICAL_SPACE: Double = HEX_HEIGHT * 3.0 / 4.0
private let HEX_CENTER_DISTANCE: Double = HEX_WIDTH
private let ε: Double = 0.001
private let ANGLE_REAL_DEGREE: Double = 60
private let WALL_SIZE: Double = HEX_SIZE
/// Cross product of Vector.
infix operator **
private typealias Vector = SIMD2<Double>
private typealias Point = SIMD2<Double>
extension SIMD2 where Scalar == Double {
fileprivate init(from s: Point, to t: Point) {
self = t - s
}
fileprivate init(from c: HexCoordinate) {
let x = c.qd * HEX_HORIZONTAL_SPACE + c.rd * HEX_HORIZONTAL_SPACE / 2.0
let y = HEX_VERTICAL_SPACE * -c.rd
self.init(x, y)
}
fileprivate static func **(lhs: SIMD2<Scalar>, rhs: SIMD2<Scalar>) -> Scalar {
return lhs.x * rhs.y - lhs.y * rhs.x
}
fileprivate func roundHexCoordinate() -> HexCoordinate {
let r = Darwin.round(-y / HEX_VERTICAL_SPACE)
let q = Darwin.round((x - r * HEX_HORIZONTAL_SPACE / 2.0) / HEX_HORIZONTAL_SPACE)
return HexCoordinate(q: Int(q), r: Int(r))
}
fileprivate var vectorLength: Scalar { return sqrt(pow(x, 2) + pow(y, 2)) }
}
private struct Segment {
let s: Point
let v: Vector
init(_ s: Point, _ t: Point) {
self.s = s
self.v = Vector(from: s, to: t)
}
// return true, when segment is crossing.
static func ^(lhs: Segment, rhs: Segment) -> Bool {
let v = Vector(from: lhs.s, to: rhs.s)
let t1 = (v ** lhs.v) / (lhs.v ** rhs.v)
let t2 = (v ** rhs.v) / (lhs.v ** rhs.v)
return 0.0 <= t1 && t1 <= 1.0 && 0.0 <= t2 && t2 <= 1.0
}
}
extension HexWallBlock {
func isInterrupting(from sourceCoordinate: HexCoordinate, to targetCoordinate: HexCoordinate) -> Bool {
let distance = sourceCoordinate.hexDistance(from: targetCoordinate)
let sReal = Point(from: sourceCoordinate)
let tReal = Point(from: targetCoordinate)
let diff = Vector(from: sReal, to: tReal)
let count = distance
let targets = Set((0...count).map { ( sReal + ( diff / Double(count) * Double($0) ) ).roundHexCoordinate() })
.filter { self.contains($0) }
if targets.count == 0 {
return false
}
return targets.contains { target in
let cReal = Point(from: target)
let vector = Vector(from: cReal, to: sReal)
// conflict the center circle of wall.
if (vector.vectorLength * sin(atan2(vector.x, vector.y))).magnitude < WALL_SIZE / 2 {
return true
}
let arounds = target.aroundCoordinates.filter { self.contains($0) }
return arounds.contains { around in
let s1 = Segment(sReal, tReal)
let s2 = Segment(cReal, Point(from: around))
return s1 ^ s2 // return true, when segment is crossing.
}
}
}
}
extension HexCoordinate {
func hexDistance(from another: HexCoordinate) -> Int {
return (abs(q - another.q) + abs(q + r - another.q - another.r) + abs(r - another.r)) / 2
}
func isNearBy(_ target: HexCoordinate, hexRange range: Int) -> Bool {
return hexDistance(from: target) <= range
}
private func radian(to target: HexCoordinate) -> Double {
let sReal = Point(from: self)
let tReal = Point(from: target)
return atan2(tReal.y - sReal.y, tReal.x - sReal.x)
}
fileprivate var qd: Double { Double(q) }
fileprivate var rd: Double { Double(r) }
func isInSight(_ sight: HexSpaceSight, source: HexCoordinate) -> Bool {
if source == self {
return true
}
if sight.angle <= 0 { // no sight.
return false
}
if sight.angle >= 6 { // 360° over viewing angle.
return true
}
let realRadian = source.radian(to: self)
let directionRadian = HexCoordinate.zero.radian(to: HexCoordinate(sight.direction))
let viewingAngleRadian = (Double(sight.angle) * ANGLE_REAL_DEGREE / 360.0 * 2.0 * .pi) // angle is Int, 1 angle equals 60°
let lhs = directionRadian - viewingAngleRadian / 2.0
let rhs = directionRadian + viewingAngleRadian / 2.0
if lhs <= realRadian + ε, realRadian - ε <= rhs {
return true
}
if .pi * 2.0 < rhs { // Really necessary?
let test = rhs - .pi * 2.0
if 0.0 <= realRadian, realRadian <= test + ε {
return true
}
}
if .pi * -2.0 > lhs { // Really necessary?
let test = lhs + .pi * 2.0
if test - ε <= realRadian, realRadian <= 0.0 {
return true
}
}
return false
}
}
typealias HexPath = Array<HexCoordinate>
typealias HexWallBlock = Set<HexCoordinate>
extension HexWallBlock {
func isTouched(by coordinate: HexCoordinate) -> Bool {
return contains { $0.isTouching(to: coordinate) }
}
}
class HexSpace {
let cells: Array<HexCell>
var objects: Array<HexSpaceObject> = []
var positions: Dictionary<Int, HexCoordinate> = [:]
var nextHexId = 1
init(_ cells: Array<HexCell>) {
cells.forEach { $0.freeze() }
self.cells = cells
}
convenience init(use factory: HexSpaceFactory) {
self.init(factory.build())
}
func checkCellExists(at coordinate: HexCoordinate) -> Bool {
return cells.contains { $0.coordinate == coordinate }
}
func coordinate(of object: HexSpaceObject) -> HexCoordinate {
return positions[object.hexId]!
}
func findObjects(in sight: HexSpaceSight, from object: HexSpaceObject) -> Array<HexSpaceObject> {
return objects
.filter { self.coordinate(of: $0).isVisible(in: sight, source: self.coordinate(of: object)) }
}
func findPaths(maxRange: Int, from sourceCoordinate: HexCoordinate, to targetCoordinate: HexCoordinate) -> Array<HexPath>? {
guard checkCellExists(at: sourceCoordinate) else { return nil }
if sourceCoordinate.hexDistance(from: targetCoordinate) > maxRange {
return nil
}
var fringes: Array<Array<HexCoordinate>> = [[sourceCoordinate]]
var visited: Set<HexCoordinate> = []
var step = 0
while visited.contains(targetCoordinate) == false && step <= maxRange {
defer { step += 1 }
fringes.append([])
fringes[step].forEach { coordinate in
coordinate.aroundCoordinates
.filter { self.checkCellExists(at: $0) }
.forEach { coordinate in
visited.insert(coordinate)
fringes[step + 1].append(coordinate)
}
}
}
guard visited.contains(targetCoordinate) else { return nil }
let lastStep = step
do {
var paths: Array<HexPath> = [[targetCoordinate]]
var step = lastStep - 1
while step >= 0 {
defer { step -= 1 }
let range = 0..<paths.count
for index in range {
let path = paths[index]
let nextCoordinates = fringes[step].filter { $0.isTouching(to: path.last!) }
paths[index] = path + [nextCoordinates.first!]
if nextCoordinates.count > 1 {
for i in 1..<nextCoordinates.count {
paths.append(path + [nextCoordinates[i]])
}
}
}
}
return paths.map { $0.reversed() }
}
}
func findWallBlocks() -> Array<HexWallBlock> {
let walls = cells
.filter { $0.field.isWall }
.map { $0.coordinate }
var results: Array<HexWallBlock> = []
var usedList: Set<HexCoordinate> = []
let use: (HexCoordinate) -> Void = { usedList.insert($0) }
let used: (HexCoordinate) -> Bool = { usedList.contains($0) }
while usedList.count < walls.count {
guard let target = walls.filter({ !used($0) }).first else { break }
use(target)
var wallBlock: HexWallBlock = [target]
while let target = walls.first(where: { wallBlock.isTouched(by: $0) && !used($0) }) {
wallBlock.insert(target)
use(target)
}
results.append(wallBlock)
}
return results
}
}
extension HexCoordinate {
func isVisible(in sight: HexSpaceSight, source: HexCoordinate) -> Bool {
if self.isNearBy(source, hexRange: sight.hexRange) == false {
return false // target is out of viewing range.
}
if self.isInSight(sight, source: source) == false {
return false // target is not in viewing angle.
}
return true
}
}
class HexSpaceFactory {
typealias Builder = (HexCell) -> Void
var builders: Array<(HexCoordinate, Builder?)> = []
func set(at coordinate: HexCoordinate, builder: Builder? = nil) {
if let index = builders.firstIndex(where: { $0.0.q == coordinate.q && $0.0.r == coordinate.r }) {
builders[index] = (coordinate, builder)
} else {
builders.append((coordinate, builder))
}
}
func addLine(start: HexCoordinate, direction: HexDirection, length: Int, builder: Builder? = nil) {
(0..<length).forEach {
self.set(at: start + (direction.vector * $0), builder: builder)
}
}
func addTriangle(point: HexCoordinate, direction: HexDirection, length: Int, rotation: HexRotation = .clockwise, builder: Builder? = nil) {
var l = length
var s = point
let ad = direction.rotate(rotation)
while (l > 0) {
defer {
l -= 1
s = s + HexCoordinate(ad)
}
addLine(start: s, direction: direction, length: l, builder: builder)
}
}
func addHexagram(center: HexCoordinate, range: Int, builder: Builder? = nil) {
(0...5).forEach { i in // [!] Each HexDirection values.
let d = HexDirection(rawValue: i)!
self.addTriangle(point: center, direction: d, length: range, builder: builder)
}
}
func build() -> Array<HexCell> {
return builders.map { args in
let (coordinate, builder) = args
let cell = HexCell(coordinate: coordinate)
builder?(cell)
return cell
}
}
}
protocol HexSpaceObject: AnyObject {
// Unique ID on some hex space.
var hexId: Int { get set }
// The token exclusively holds a cell, another tokens can't pass and stop holded cells.
var isToken: Bool { get }
}
extension HexSpaceObject {
var isOrnament: Bool { !isToken }
}
// MARK: - HexSpace Methods for managing HexSpaceObject.
extension HexSpace {
@discardableResult
func place(_ object: HexSpaceObject, at coordinate: HexCoordinate) -> Bool {
guard contains(object) == false else {
assertionFailure()
return false
}
defer { nextHexId += 1 }
assert(object.hexId <= 0)
object.hexId = nextHexId
objects.append(object)
if checkCellExists(at: coordinate) == false {
assertionFailure()
return false
}
if checkTokenExists(at: coordinate) == true {
assertionFailure()
return false
}
positions[object.hexId] = coordinate
return true
}
func remove(_ object: HexSpaceObject) {
guard let index = index(of: object) else {
assertionFailure()
return
}
defer { object.hexId = 0 }
if let index = positions.index(forKey: object.hexId) {
positions.remove(at: index)
}
objects.remove(at: index)
}
func checkTokenExists(at coordinate: HexCoordinate) -> Bool {
return objects
.filter { $0.isToken }
.map { positions[$0.hexId] }
.contains(coordinate)
}
func contains(_ object: HexSpaceObject) -> Bool {
return objects.contains { $0 === object }
}
private func index(of object: HexSpaceObject) -> Array<HexSpaceObject>.Index? {
return objects.firstIndex { $0 === object }
}
}
enum HexDirection: Int, Hashable, Equatable {
case right = 0
case upRight = 1
case upLeft = 2
case left = 3
case downLeft = 4
case downRight = 5
func rotate(_ rotation: HexRotation) -> HexDirection {
switch rotation {
case .clockwise:
return HexDirection(rawValue: (rawValue + 5) % 6)!
case .anticlockwise:
return HexDirection(rawValue: (rawValue + 1) % 6)!
}
}
}
enum HexRotation {
case clockwise
case anticlockwise
}
struct HexCoordinate: CustomDebugStringConvertible, Hashable, Equatable {
var q: Int
var r: Int
init(q: Int, r: Int) {
self.q = q
self.r = r
}
init(_ direction: HexDirection, range: Int = 1) {
self = HexCoordinate.unitVectors[direction]! * range
}
var debugDescription: String {
return "[q: \(q), r: \(r)]"
}
var aroundCoordinates: Array<HexCoordinate> {
return HexCoordinate.unitVectors.values.map { self + $0 }
}
func hash(into hasher: inout Hasher) {
hasher.combine("\(q),\(r)")
}
func isTouching(to: HexCoordinate) -> Bool {
return self.hexDistance(from: to) <= 1
}
static func +(lhs: HexCoordinate, rhs: HexCoordinate) -> HexCoordinate {
return HexCoordinate(q: lhs.q + rhs.q, r: lhs.r + rhs.r)
}
static func *(lhs: HexCoordinate, rhs: Int) -> HexCoordinate {
return HexCoordinate(q: lhs.q * rhs, r: lhs.r * rhs)
}
static func ==(lhs: HexCoordinate, rhs: HexCoordinate) -> Bool {
return lhs.q == rhs.q && lhs.r == rhs.r
}
static let zero: HexCoordinate = HexCoordinate(q: 0, r: 0)
static let unitVectors: Dictionary<HexDirection, HexCoordinate> = [
.right: HexCoordinate(q: 1, r: 0),
.upRight: HexCoordinate(q: 1, r: -1),
.upLeft: HexCoordinate(q: 0, r: -1),
.left: HexCoordinate(q: -1, r: 0),
.downLeft: HexCoordinate(q: -1, r: 1),
.downRight: HexCoordinate(q: 0, r: 1),
]
}
struct Field {
let isWall: Bool
let floorHeight: Int
static let none = Field(isWall: false, floorHeight: 0)
}
struct HexSpaceSight {
var direction: HexDirection
var angle: Int // 0 ~ 6, means viewing angle. (calc angle * 60°)
var hexRange: Int
}
@niaeashes
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment