/ Roster.java
Roster.java
  1  //FIXME import java.util.HashMap;
  2  import java.util.Random;
  3  
  4  /// Cave - Roster of entities in a cave
  5  ///
  6  /// SPDX-License-Identifier: GPL-3.0-or-later
  7  /// SPDX-FileCopyrightText: 2024 Jonas Smedegaard <dr@jones.dk>
  8  ///
  9  /// Knowledge about placement of entities in a cave.
 10  ///
 11  /// * v0.0.1
 12  ///   * initial release, derived frop delivery "assignment 6"
 13  ///
 14  /// @version 0.0.1
 15  /// @see <https://app.radicle.xyz/nodes/ash.radicle.garden/rad:z3YzABgyz2D36LiKe3YcdJ6PcDCXM/tree/README.md>
 16  /// @see <https://moodle.ruc.dk/course/section.php?id=201713>
 17  public class Roster {
 18  
 19  	/// Roster constructor
 20  	///
 21  	/// Currently implemented only for 2D spaces,
 22  	/// but might in future support more dimensions.
 23  	///
 24  	/// @param bottom  Lowest corner of grid as integer array
 25  	/// @param top     Highest corner of grid as integer array
 26  	public Roster(final int[] bottom, final int[] top) {
 27  
 28  		// store before validation,
 29  		// needed for Java final handling
 30  		this.BOTTOM = bottom;
 31  		this.TOP = top;
 32  
 33  		if (!Utils.fitsDimensions(BOTTOM)
 34  			|| !Utils.fitsDimensions(TOP)
 35  		) {
 36  			throw new IllegalArgumentException(
 37  				"2-dimensional coordinates required"
 38  			);
 39  		}
 40  
 41  		this.LEN = new int[BOTTOM.length];
 42  		for (int i = 0; i < BOTTOM.length; i++) {
 43  			LEN[i] = TOP[i] - BOTTOM[i] + 1;
 44  		}
 45  
 46  		this.places = new Object[LEN[0]][LEN[1]];
 47  	}
 48  
 49  	/// Lowest position in grid
 50  	public final int[] BOTTOM;
 51  
 52  	/// highest position in grid
 53  	public final int[] TOP;
 54  
 55  	/// size of grid
 56  	public final int[] LEN;
 57  
 58  	/// Placement of thing
 59  	///
 60  	/// A "thing" here means either an agent or an inanimate object.
 61  	///
 62  	/// @param place  position in grid
 63  	/// @param thing  positioned thing
 64  	private record Object(Place place, Thing thing) { }
 65  
 66  	/// Physical position in grid
 67  	///
 68  	/// Defined using record type to tersely define as immutable object
 69  	///
 70  	/// @param coords  coordinates as integer array of each Dimension
 71  	record Place(int[] coords) {
 72  
 73  		/// Input validation
 74  		///
 75  		/// Ensure uniform array size compatible with grid.
 76  		///
 77  		/// This simplifies (and optimizes) internal calculations
 78  		/// by not needing array length check at each computation
 79  		///
 80  		/// @throws IllegalArgumentException  must fit Dimension set
 81  		Place {
 82  			if (coords == null || coords.length != 2) {
 83  				throw new IllegalArgumentException(
 84  					"2-dimensional coordinates required"
 85  				);
 86  			}
 87  		}
 88  	}
 89  
 90  	/// Get score
 91  	///
 92  	/// @return  score as integer
 93  	public final int getScore() {
 94  		return score;
 95  	}
 96  
 97  	/// collection of uniquely occupied places in the grid
 98  	private Object[][] places;
 99  
100  	/// initialize random number generator
101  	private static Random rand = new Random();
102  
103  	/// player score
104  	private int score;
105  
106  	/// Place a thing at a specific place in the grid if not occupied
107  	///
108  	/// @param thing  the thing to place
109  	/// @param pos     the position to place it as integer array
110  	/// @return        success of placement as boolean
111  	public final boolean add(final Thing thing, final int[] pos) {
112  		if (places[pos[0]][pos[1]] != null) {
113  			return false;
114  		}
115  		places[pos[0]][pos[1]] = new Object(new Place(pos), thing);
116  		return true;
117  	}
118  
119  	// TODO: avoid risk of endless loop in too crowded space:
120  	//       a) count attempts and bail when exceeding a fair amount
121  	//       b) locate and only pick among vacant spots,
122  	//          and bail when none are left
123  	/// Place a thing at a random empty place in the grid
124  	///
125  	/// @param thing  the thing to place in form of a character
126  	/// @return       success of placement as boolean
127  	public boolean add(final Thing thing) {
128  		int x;
129  		int y;
130  		while (true) {
131  			x = rand.nextInt(LEN[0] - 1);
132  			y = rand.nextInt(LEN[1] - 1);
133  			if (places[x][y] == null) {
134  				places[x][y] = new Object(
135  					new Place(new int[] {x, y}),
136  					thing
137  				);
138  				break;
139  			}
140  		}
141  		return true;
142  	}
143  
144  	/// Get a move of an agent
145  	///
146  	/// Resolve resulting position of one move by some agent,
147  	/// wit current bearing of the agent.
148  	/// An agent is a player, but can in future e extended to cover
149  	/// opponents or scouting devices
150  	/// (scouting is relevant for larger caves
151  	/// and for gameplay with knowledge of places fading over time).
152  	///
153  	/// @param agent  the agent
154  	/// @param pos    starting position of speculative move
155  	/// @return       new next position as integer array
156  	public final int[] virtualMove(final Agent agent, final int[] pos) {
157  		Bearing heading = agent.getHeading();
158  		return new int[] {
159  			pos[0] + 1 * heading.i,
160  			pos[1] + 1 * heading.j
161  		};
162  	}
163  
164  	/// Compose a POSIX ASCII map of grid contents
165  	///
166  	/// @return  ASCII map as a rectangular multi-line string
167  	public final String map() {
168  		StringBuilder sb = new StringBuilder();
169  
170  		for (int i = 0; i < LEN[0]; i++) {
171  			for (int j = 0; j < LEN[1]; j++) {
172  				if (places[i][j] == null) {
173  					sb.append('.');
174  				} else {
175  					sb.append(
176  						places[i][j].thing().seeDress()
177  					);
178  				}
179  
180  				// space-delimit horisontally
181  				if (j < places[i].length - 1) {
182  					sb.append(' ');
183  				}
184  			}
185  
186  			// POSIX-newline-delimit vertically
187  			if (i < places.length - 1) {
188  				sb.append('\n');
189  			}
190  		}
191  		return sb.toString();
192  	}
193  
194  /* TODO
195  	/// Compute a line of sight
196  	///
197  	/// @param agent  the agent viewing
198  	/// @return       visibility distance
199  	public final int view(final Entity.Agent agent) {
200  		int[] pos = Utils.deltaPosition(agent.place().coords, BOTTOM);
201  		int dir = agent.bearing.vector[agent.bearing.axis];
202  		int[] range = (dir > 0)
203  			? Utils.deltaPosition(TOP, agent.place().coords)
204  			: Utils.deltaPosition(agent.place().coords, BOTTOM);
205  		int i = 1;
206  
207  		// TODO: avoid hardcoded height dimension mirroring
208  		final int flip = -1;
209  
210  		// count in each case from observation point towards an object
211  		// TODO: make more DRY
212  		switch (agent.bearing) {
213  		  case NORTH:
214  			for (; i <= range[1]; i++) {
215  				if (places[pos[1] + i * dir * flip][pos[0]] != null) {
216  					return i;
217  				}
218  			}
219  			break;
220  		  case EAST:
221  			for (; i <= range[0]; i++) {
222  				if (places[pos[1]][pos[0] + i * dir] != null) {
223  					return i;
224  				}
225  			}
226  			break;
227  		  case SOUTH:
228  			for (; i <= range[1]; i++) {
229  				if (places[pos[1] + i * dir * flip][pos[0]] != null) {
230  					return i;
231  				}
232  			}
233  			break;
234  		  case WEST:
235  			for (; i <= range[0]; i++) {
236  				if (places[pos[1]][pos[0] + i * dir] != null) {
237  					return i;
238  				}
239  			}
240  			break;
241  
242  		// we should never experience an alien bearing enum
243  		  default:
244  			throw new UnsupportedOperationException(
245  				"Impossible direction: " + agent.bearing.value);
246  		}
247  
248  		// return distance to horizon (or "ice wall")
249  		// @see <https://wiki.tfes.org/The_Ice_Wall>
250  		return i;
251  	}
252  */
253  }