@@ -62,15 +62,75 @@ const isValidPoint = (
6262 return true
6363}
6464
65+ type Side = "top" | "right" | "bottom" | "left"
66+
67+ /**
68+ * Generates a random point on a specific side of the bounds
69+ */
70+ const getPointOnSide = (
71+ bounds : { minX : number ; maxX : number ; minY : number ; maxY : number } ,
72+ side : Side ,
73+ t : number , // 0-1 position along the side
74+ ) : { x : number ; y : number } => {
75+ const width = bounds . maxX - bounds . minX
76+ const height = bounds . maxY - bounds . minY
77+
78+ switch ( side ) {
79+ case "top" :
80+ return { x : bounds . minX + t * width , y : bounds . maxY }
81+ case "right" :
82+ return { x : bounds . maxX , y : bounds . maxY - t * height }
83+ case "bottom" :
84+ return { x : bounds . maxX - t * width , y : bounds . minY }
85+ case "left" :
86+ return { x : bounds . minX , y : bounds . minY + t * height }
87+ }
88+ }
89+
90+ /**
91+ * Returns the length of a side
92+ */
93+ const getSideLength = (
94+ bounds : { minX : number ; maxX : number ; minY : number ; maxY : number } ,
95+ side : Side ,
96+ ) : number => {
97+ const width = bounds . maxX - bounds . minX
98+ const height = bounds . maxY - bounds . minY
99+ return side === "top" || side === "bottom" ? width : height
100+ }
101+
65102/**
66103 * Generates a random point on the perimeter of the given bounds
104+ * If allowedSides is provided, only generates points on those sides
67105 */
68106const getRandomPerimeterPoint = (
69107 bounds : { minX : number ; maxX : number ; minY : number ; maxY : number } ,
70108 random : ( ) => number ,
109+ allowedSides ?: Side [ ] ,
71110) : { x : number ; y : number } => {
72111 const width = bounds . maxX - bounds . minX
73112 const height = bounds . maxY - bounds . minY
113+
114+ if ( allowedSides && allowedSides . length > 0 ) {
115+ // Calculate total length of allowed sides
116+ const sideLengths = allowedSides . map ( ( side ) => getSideLength ( bounds , side ) )
117+ const totalLength = sideLengths . reduce ( ( sum , len ) => sum + len , 0 )
118+
119+ // Pick a random position along the combined length
120+ let pos = random ( ) * totalLength
121+
122+ for ( let i = 0 ; i < allowedSides . length ; i ++ ) {
123+ if ( pos < sideLengths [ i ] ) {
124+ const t = pos / sideLengths [ i ]
125+ return getPointOnSide ( bounds , allowedSides [ i ] , t )
126+ }
127+ pos -= sideLengths [ i ]
128+ }
129+
130+ // Fallback to last side (shouldn't happen due to floating point)
131+ return getPointOnSide ( bounds , allowedSides [ allowedSides . length - 1 ] , random ( ) )
132+ }
133+
74134 const perimeter = 2 * width + 2 * height
75135
76136 // Pick a random position along the perimeter
@@ -92,6 +152,20 @@ const getRandomPerimeterPoint = (
92152 return { x : bounds . minX , y : bounds . minY + ( pos - 2 * width - height ) }
93153}
94154
155+ const ALL_SIDES : Side [ ] = [ "top" , "right" , "bottom" , "left" ]
156+
157+ /**
158+ * Picks two random different sides
159+ */
160+ const pickTwoRandomSides = ( random : ( ) => number ) : [ Side , Side ] => {
161+ const firstIndex = Math . floor ( random ( ) * 4 )
162+ let secondIndex = Math . floor ( random ( ) * 3 )
163+ if ( secondIndex >= firstIndex ) {
164+ secondIndex ++
165+ }
166+ return [ ALL_SIDES [ firstIndex ] , ALL_SIDES [ secondIndex ] ]
167+ }
168+
95169/**
96170 * Generates a connection ID from an index (0 -> "A", 1 -> "B", etc.)
97171 */
@@ -103,6 +177,7 @@ export type CreateProblemFromBaseGraphParams = {
103177 baseGraph : JumperGraph
104178 numCrossings : number
105179 randomSeed : number
180+ twoSided ?: boolean
106181}
107182
108183/**
@@ -114,10 +189,14 @@ export const createProblemFromBaseGraph = ({
114189 baseGraph,
115190 numCrossings,
116191 randomSeed,
192+ twoSided = false ,
117193} : CreateProblemFromBaseGraphParams ) : JumperGraphWithConnections => {
118194 const random = createSeededRandom ( randomSeed )
119195 const graphBounds = calculateGraphBounds ( baseGraph . regions )
120196
197+ // If twoSided, pick two random sides to use for all points in this problem
198+ const allowedSides = twoSided ? pickTwoRandomSides ( random ) : undefined
199+
121200 // Start with minimum connections needed for the desired crossings
122201 // For n connections, max crossings is n*(n-1)/2, so we need at least
123202 // ceil((1 + sqrt(1 + 8*numCrossings)) / 2) connections
@@ -136,7 +215,7 @@ export const createProblemFromBaseGraph = ({
136215 // Try to find a valid start point
137216 let start : { x : number ; y : number } | null = null
138217 for ( let tryCount = 0 ; tryCount < 100 ; tryCount ++ ) {
139- const candidate = getRandomPerimeterPoint ( graphBounds , random )
218+ const candidate = getRandomPerimeterPoint ( graphBounds , random , allowedSides )
140219 if ( isValidPoint ( candidate , allPoints ) ) {
141220 start = candidate
142221 break
@@ -151,7 +230,7 @@ export const createProblemFromBaseGraph = ({
151230 // Try to find a valid end point
152231 let end : { x : number ; y : number } | null = null
153232 for ( let tryCount = 0 ; tryCount < 100 ; tryCount ++ ) {
154- const candidate = getRandomPerimeterPoint ( graphBounds , random )
233+ const candidate = getRandomPerimeterPoint ( graphBounds , random , allowedSides )
155234 if ( isValidPoint ( candidate , allPoints ) ) {
156235 end = candidate
157236 break
0 commit comments